@vellumai/assistant 0.5.3 → 0.5.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.
- package/Dockerfile +18 -27
- package/docs/architecture/memory.md +105 -0
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/archive-recall.test.ts +560 -0
- package/src/__tests__/conversation-clear-safety.test.ts +259 -0
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +474 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +3 -0
- package/src/__tests__/memory-reducer-job.test.ts +538 -0
- package/src/__tests__/memory-reducer-scheduling.test.ts +473 -0
- package/src/__tests__/memory-reducer-types.test.ts +12 -4
- package/src/__tests__/memory-reducer.test.ts +7 -1
- package/src/__tests__/memory-regressions.test.ts +24 -4
- package/src/__tests__/memory-simplified-config.test.ts +4 -4
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/simplified-memory-e2e.test.ts +666 -0
- package/src/__tests__/simplified-memory-runtime.test.ts +616 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +18 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +0 -1
- package/src/config/schemas/memory-simplified.ts +1 -1
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/config-watcher.ts +4 -1
- package/src/daemon/conversation-memory.ts +117 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +11 -0
- package/src/daemon/lifecycle.ts +51 -2
- package/src/daemon/providers-setup.ts +2 -1
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/archive-recall.ts +516 -0
- package/src/memory/brief-time.ts +5 -4
- package/src/memory/conversation-crud.ts +210 -0
- package/src/memory/conversation-key-store.ts +33 -4
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/job-handlers/backfill-simplified-memory.ts +462 -0
- package/src/memory/job-handlers/conversation-starters.ts +24 -30
- package/src/memory/job-handlers/reduce-conversation-memory.ts +229 -0
- package/src/memory/jobs-store.ts +2 -0
- package/src/memory/jobs-worker.ts +8 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +49 -14
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +9 -1
- package/src/memory/migrations/141-rename-verification-table.ts +8 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +7 -2
- package/src/memory/migrations/174-rename-thread-starters-table.ts +8 -0
- package/src/memory/migrations/188-schedule-quiet-flag.ts +13 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/reducer-scheduler.ts +242 -0
- package/src/memory/reducer-types.ts +9 -2
- package/src/memory/reducer.ts +25 -11
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/trust-client.ts +343 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +523 -36
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +10 -1
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +88 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +19 -7
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/secret-routes.ts +5 -1
- package/src/schedule/schedule-store.ts +7 -0
- package/src/schedule/scheduler.ts +6 -2
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +22 -20
- package/src/tools/filesystem/edit.ts +6 -1
- package/src/tools/filesystem/read.ts +6 -1
- package/src/tools/filesystem/write.ts +6 -1
- package/src/tools/memory/handlers.ts +129 -1
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +5 -1
- package/src/tools/schedule/update.ts +6 -0
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backfill job handler: migrates legacy memory rows into the simplified memory
|
|
3
|
+
* system without deleting the old tables.
|
|
4
|
+
*
|
|
5
|
+
* Migration mapping:
|
|
6
|
+
* - `memory_segments` -> `memory_chunks` (via `memory_observations`)
|
|
7
|
+
* - `memory_summaries` -> `memory_episodes`
|
|
8
|
+
* - Active/high-confidence `memory_items` -> `memory_observations`,
|
|
9
|
+
* plus `time_contexts` or `open_loops` when the mapping is unambiguous.
|
|
10
|
+
*
|
|
11
|
+
* The handler is idempotent: content-hash deduplication on chunks and
|
|
12
|
+
* checkpoint tracking prevent double-writes on re-runs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { eq } from "drizzle-orm";
|
|
16
|
+
import { v4 as uuid } from "uuid";
|
|
17
|
+
|
|
18
|
+
import { estimateTextTokens } from "../../context/token-estimator.js";
|
|
19
|
+
import { getLogger } from "../../util/logger.js";
|
|
20
|
+
import {
|
|
21
|
+
computeChunkContentHash,
|
|
22
|
+
insertObservation,
|
|
23
|
+
} from "../archive-store.js";
|
|
24
|
+
import { getMemoryCheckpoint, setMemoryCheckpoint } from "../checkpoints.js";
|
|
25
|
+
import { getDb, rawAll } from "../db.js";
|
|
26
|
+
import type { MemoryJob } from "../jobs-store.js";
|
|
27
|
+
import { enqueueMemoryJob } from "../jobs-store.js";
|
|
28
|
+
import {
|
|
29
|
+
conversations,
|
|
30
|
+
memoryChunks,
|
|
31
|
+
memoryEpisodes,
|
|
32
|
+
memoryObservations,
|
|
33
|
+
openLoops,
|
|
34
|
+
timeContexts,
|
|
35
|
+
} from "../schema.js";
|
|
36
|
+
|
|
37
|
+
const log = getLogger("backfill-simplified-memory");
|
|
38
|
+
|
|
39
|
+
/** Checkpoint keys for tracking backfill progress. */
|
|
40
|
+
const CHECKPOINT_SEGMENTS = "simplified_backfill:segments:last_id";
|
|
41
|
+
const CHECKPOINT_SUMMARIES = "simplified_backfill:summaries:last_id";
|
|
42
|
+
const CHECKPOINT_ITEMS = "simplified_backfill:items:last_id";
|
|
43
|
+
const CHECKPOINT_COMPLETE = "simplified_backfill:complete";
|
|
44
|
+
|
|
45
|
+
/** Batch size for each migration pass. */
|
|
46
|
+
const BATCH_SIZE = 200;
|
|
47
|
+
|
|
48
|
+
// ── Legacy row types ──────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
interface LegacySegment {
|
|
51
|
+
id: string;
|
|
52
|
+
message_id: string;
|
|
53
|
+
conversation_id: string;
|
|
54
|
+
role: string;
|
|
55
|
+
text: string;
|
|
56
|
+
token_estimate: number;
|
|
57
|
+
scope_id: string;
|
|
58
|
+
content_hash: string | null;
|
|
59
|
+
created_at: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface LegacySummary {
|
|
63
|
+
id: string;
|
|
64
|
+
scope: string;
|
|
65
|
+
scope_key: string;
|
|
66
|
+
summary: string;
|
|
67
|
+
token_estimate: number;
|
|
68
|
+
scope_id: string;
|
|
69
|
+
start_at: number;
|
|
70
|
+
end_at: number;
|
|
71
|
+
created_at: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface LegacyItem {
|
|
75
|
+
id: string;
|
|
76
|
+
kind: string;
|
|
77
|
+
subject: string;
|
|
78
|
+
statement: string;
|
|
79
|
+
status: string;
|
|
80
|
+
confidence: number;
|
|
81
|
+
scope_id: string;
|
|
82
|
+
first_seen_at: number;
|
|
83
|
+
last_seen_at: number;
|
|
84
|
+
valid_from: number | null;
|
|
85
|
+
invalid_at: number | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Entry point ───────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export async function backfillSimplifiedMemoryJob(
|
|
91
|
+
job: MemoryJob,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const force = job.payload.force === true;
|
|
94
|
+
|
|
95
|
+
if (!force) {
|
|
96
|
+
const complete = getMemoryCheckpoint(CHECKPOINT_COMPLETE);
|
|
97
|
+
if (complete === "true") {
|
|
98
|
+
log.debug("Simplified memory backfill already complete, skipping");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (force) {
|
|
104
|
+
// Reset all checkpoints so the backfill restarts from scratch
|
|
105
|
+
setMemoryCheckpoint(CHECKPOINT_SEGMENTS, "");
|
|
106
|
+
setMemoryCheckpoint(CHECKPOINT_SUMMARIES, "");
|
|
107
|
+
setMemoryCheckpoint(CHECKPOINT_ITEMS, "");
|
|
108
|
+
setMemoryCheckpoint(CHECKPOINT_COMPLETE, "false");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let hasMore = false;
|
|
112
|
+
|
|
113
|
+
// ── Phase 1: memory_segments -> memory_observations + memory_chunks
|
|
114
|
+
hasMore = migrateSegments();
|
|
115
|
+
if (hasMore) {
|
|
116
|
+
enqueueMemoryJob("backfill_simplified_memory", {});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Phase 2: memory_summaries -> memory_episodes
|
|
121
|
+
hasMore = migrateSummaries();
|
|
122
|
+
if (hasMore) {
|
|
123
|
+
enqueueMemoryJob("backfill_simplified_memory", {});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Phase 3: active memory_items -> memory_observations (+ brief-state)
|
|
128
|
+
hasMore = migrateItems();
|
|
129
|
+
if (hasMore) {
|
|
130
|
+
enqueueMemoryJob("backfill_simplified_memory", {});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// All phases complete
|
|
135
|
+
setMemoryCheckpoint(CHECKPOINT_COMPLETE, "true");
|
|
136
|
+
log.info("Simplified memory backfill completed");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Phase 1: Segments ─────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function migrateSegments(): boolean {
|
|
142
|
+
const lastId = getMemoryCheckpoint(CHECKPOINT_SEGMENTS) ?? "";
|
|
143
|
+
|
|
144
|
+
const segments = rawAll<LegacySegment>(
|
|
145
|
+
`SELECT id, message_id, conversation_id, role, text, token_estimate,
|
|
146
|
+
scope_id, content_hash, created_at
|
|
147
|
+
FROM memory_segments
|
|
148
|
+
WHERE id > ?
|
|
149
|
+
ORDER BY id ASC
|
|
150
|
+
LIMIT ?`,
|
|
151
|
+
lastId,
|
|
152
|
+
BATCH_SIZE,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (segments.length === 0) return false;
|
|
156
|
+
|
|
157
|
+
for (const seg of segments) {
|
|
158
|
+
try {
|
|
159
|
+
// Insert as an observation — insertObservation handles chunk dedup
|
|
160
|
+
insertObservation({
|
|
161
|
+
conversationId: seg.conversation_id,
|
|
162
|
+
messageId: seg.message_id,
|
|
163
|
+
role: seg.role,
|
|
164
|
+
content: seg.text,
|
|
165
|
+
scopeId: seg.scope_id,
|
|
166
|
+
modality: "text",
|
|
167
|
+
source: "backfill:segment",
|
|
168
|
+
});
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// Log and continue — individual failures should not block the batch
|
|
171
|
+
log.warn(
|
|
172
|
+
{ err, segmentId: seg.id },
|
|
173
|
+
"Failed to migrate segment, skipping",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lastSegment = segments[segments.length - 1];
|
|
179
|
+
setMemoryCheckpoint(CHECKPOINT_SEGMENTS, lastSegment.id);
|
|
180
|
+
|
|
181
|
+
log.debug(
|
|
182
|
+
{ migrated: segments.length, lastId: lastSegment.id },
|
|
183
|
+
"Migrated segment batch",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return segments.length === BATCH_SIZE;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Phase 2: Summaries ────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
function migrateSummaries(): boolean {
|
|
192
|
+
const lastId = getMemoryCheckpoint(CHECKPOINT_SUMMARIES) ?? "";
|
|
193
|
+
|
|
194
|
+
const summaries = rawAll<LegacySummary>(
|
|
195
|
+
`SELECT id, scope, scope_key, summary, token_estimate, scope_id,
|
|
196
|
+
start_at, end_at, created_at
|
|
197
|
+
FROM memory_summaries
|
|
198
|
+
WHERE id > ?
|
|
199
|
+
ORDER BY id ASC
|
|
200
|
+
LIMIT ?`,
|
|
201
|
+
lastId,
|
|
202
|
+
BATCH_SIZE,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (summaries.length === 0) return false;
|
|
206
|
+
|
|
207
|
+
const db = getDb();
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
|
|
210
|
+
for (const sum of summaries) {
|
|
211
|
+
try {
|
|
212
|
+
// Derive a conversation ID from the scope_key if it looks like a conversation summary.
|
|
213
|
+
// scope_key format: "conversation:<conversationId>" or "<scope>:<key>"
|
|
214
|
+
const conversationId = extractConversationId(sum.scope, sum.scope_key);
|
|
215
|
+
if (!conversationId) {
|
|
216
|
+
log.debug(
|
|
217
|
+
{ summaryId: sum.id, scope: sum.scope, scopeKey: sum.scope_key },
|
|
218
|
+
"Skipping non-conversation summary",
|
|
219
|
+
);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const episodeId = uuid();
|
|
224
|
+
const title = buildEpisodeTitle(sum.scope, sum.scope_key);
|
|
225
|
+
|
|
226
|
+
db.insert(memoryEpisodes)
|
|
227
|
+
.values({
|
|
228
|
+
id: episodeId,
|
|
229
|
+
scopeId: sum.scope_id,
|
|
230
|
+
conversationId,
|
|
231
|
+
title,
|
|
232
|
+
summary: sum.summary,
|
|
233
|
+
tokenEstimate: sum.token_estimate,
|
|
234
|
+
source: "backfill:summary",
|
|
235
|
+
startAt: sum.start_at,
|
|
236
|
+
endAt: sum.end_at,
|
|
237
|
+
createdAt: now,
|
|
238
|
+
updatedAt: now,
|
|
239
|
+
})
|
|
240
|
+
.onConflictDoNothing()
|
|
241
|
+
.run();
|
|
242
|
+
|
|
243
|
+
// Enqueue embedding for the new episode
|
|
244
|
+
enqueueMemoryJob("embed_episode", { episodeId });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
log.warn(
|
|
247
|
+
{ err, summaryId: sum.id },
|
|
248
|
+
"Failed to migrate summary, skipping",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const lastSummary = summaries[summaries.length - 1];
|
|
254
|
+
setMemoryCheckpoint(CHECKPOINT_SUMMARIES, lastSummary.id);
|
|
255
|
+
|
|
256
|
+
log.debug(
|
|
257
|
+
{ migrated: summaries.length, lastId: lastSummary.id },
|
|
258
|
+
"Migrated summary batch",
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
return summaries.length === BATCH_SIZE;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Phase 3: Items ────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/** Sentinel conversation ID for legacy items that have no conversation linkage. */
|
|
267
|
+
const LEGACY_SENTINEL_CONVERSATION_ID = "__legacy_backfill__";
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Ensure the legacy sentinel conversation row exists. This is needed because
|
|
271
|
+
* memory_observations has a FK constraint on conversation_id.
|
|
272
|
+
*/
|
|
273
|
+
function ensureLegacySentinelConversation(): void {
|
|
274
|
+
const db = getDb();
|
|
275
|
+
const existing = db
|
|
276
|
+
.select({ id: conversations.id })
|
|
277
|
+
.from(conversations)
|
|
278
|
+
.where(eq(conversations.id, LEGACY_SENTINEL_CONVERSATION_ID))
|
|
279
|
+
.get();
|
|
280
|
+
if (existing) return;
|
|
281
|
+
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
db.insert(conversations)
|
|
284
|
+
.values({
|
|
285
|
+
id: LEGACY_SENTINEL_CONVERSATION_ID,
|
|
286
|
+
title: "[Legacy Memory Backfill]",
|
|
287
|
+
createdAt: now,
|
|
288
|
+
updatedAt: now,
|
|
289
|
+
})
|
|
290
|
+
.run();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function migrateItems(): boolean {
|
|
294
|
+
const lastId = getMemoryCheckpoint(CHECKPOINT_ITEMS) ?? "";
|
|
295
|
+
|
|
296
|
+
const items = rawAll<LegacyItem>(
|
|
297
|
+
`SELECT id, kind, subject, statement, status, confidence, scope_id,
|
|
298
|
+
first_seen_at, last_seen_at, valid_from, invalid_at
|
|
299
|
+
FROM memory_items
|
|
300
|
+
WHERE id > ?
|
|
301
|
+
AND status = 'active'
|
|
302
|
+
AND confidence >= 0.5
|
|
303
|
+
AND invalid_at IS NULL
|
|
304
|
+
ORDER BY id ASC
|
|
305
|
+
LIMIT ?`,
|
|
306
|
+
lastId,
|
|
307
|
+
BATCH_SIZE,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (items.length === 0) return false;
|
|
311
|
+
|
|
312
|
+
// Ensure the sentinel conversation exists for items without conversation linkage
|
|
313
|
+
ensureLegacySentinelConversation();
|
|
314
|
+
|
|
315
|
+
const db = getDb();
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
|
|
318
|
+
for (const item of items) {
|
|
319
|
+
try {
|
|
320
|
+
// Every active item becomes an observation
|
|
321
|
+
const observationId = uuid();
|
|
322
|
+
const observationContent = `[${item.kind}] ${item.subject}: ${item.statement}`;
|
|
323
|
+
|
|
324
|
+
db.insert(memoryObservations)
|
|
325
|
+
.values({
|
|
326
|
+
id: observationId,
|
|
327
|
+
scopeId: item.scope_id,
|
|
328
|
+
conversationId: LEGACY_SENTINEL_CONVERSATION_ID,
|
|
329
|
+
role: "user",
|
|
330
|
+
content: observationContent,
|
|
331
|
+
modality: "text",
|
|
332
|
+
source: "backfill:item",
|
|
333
|
+
createdAt: now,
|
|
334
|
+
})
|
|
335
|
+
.run();
|
|
336
|
+
|
|
337
|
+
// Create a chunk for the observation (with dedup)
|
|
338
|
+
const contentHash = computeChunkContentHash(
|
|
339
|
+
item.scope_id,
|
|
340
|
+
observationContent,
|
|
341
|
+
);
|
|
342
|
+
const chunkId = uuid();
|
|
343
|
+
const tokenEstimate = estimateTextTokens(observationContent);
|
|
344
|
+
|
|
345
|
+
db.insert(memoryChunks)
|
|
346
|
+
.values({
|
|
347
|
+
id: chunkId,
|
|
348
|
+
scopeId: item.scope_id,
|
|
349
|
+
observationId,
|
|
350
|
+
content: observationContent,
|
|
351
|
+
tokenEstimate,
|
|
352
|
+
contentHash,
|
|
353
|
+
createdAt: now,
|
|
354
|
+
})
|
|
355
|
+
.onConflictDoNothing({
|
|
356
|
+
target: [memoryChunks.scopeId, memoryChunks.contentHash],
|
|
357
|
+
})
|
|
358
|
+
.run();
|
|
359
|
+
|
|
360
|
+
// Enqueue embedding for the observation's chunk
|
|
361
|
+
enqueueMemoryJob("embed_chunk", { chunkId, scopeId: item.scope_id });
|
|
362
|
+
|
|
363
|
+
// ── Brief-state: map unambiguous items to time_contexts or open_loops
|
|
364
|
+
mapItemToBriefState(item, now);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
log.warn({ err, itemId: item.id }, "Failed to migrate item, skipping");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const lastItem = items[items.length - 1];
|
|
371
|
+
setMemoryCheckpoint(CHECKPOINT_ITEMS, lastItem.id);
|
|
372
|
+
|
|
373
|
+
log.debug(
|
|
374
|
+
{ migrated: items.length, lastId: lastItem.id },
|
|
375
|
+
"Migrated item batch",
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
return items.length === BATCH_SIZE;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Brief-state mapping ───────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Map a legacy memory item to `time_contexts` or `open_loops` when the
|
|
385
|
+
* mapping is unambiguous.
|
|
386
|
+
*
|
|
387
|
+
* - Items with `valid_from` and a future `invalid_at` -> time_context
|
|
388
|
+
* - `event` kind items with future timestamps -> open_loop
|
|
389
|
+
*/
|
|
390
|
+
function mapItemToBriefState(item: LegacyItem, now: number): void {
|
|
391
|
+
const db = getDb();
|
|
392
|
+
|
|
393
|
+
// Time-bounded items -> time_contexts
|
|
394
|
+
if (
|
|
395
|
+
item.valid_from != null &&
|
|
396
|
+
item.invalid_at != null &&
|
|
397
|
+
item.invalid_at > now
|
|
398
|
+
) {
|
|
399
|
+
db.insert(timeContexts)
|
|
400
|
+
.values({
|
|
401
|
+
id: uuid(),
|
|
402
|
+
scopeId: item.scope_id,
|
|
403
|
+
summary: `${item.subject}: ${item.statement}`,
|
|
404
|
+
source: "backfill:item",
|
|
405
|
+
activeFrom: item.valid_from,
|
|
406
|
+
activeUntil: item.invalid_at,
|
|
407
|
+
createdAt: now,
|
|
408
|
+
updatedAt: now,
|
|
409
|
+
})
|
|
410
|
+
.run();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Event items with future last_seen_at -> open_loops
|
|
415
|
+
if (item.kind === "event" && item.last_seen_at > now) {
|
|
416
|
+
db.insert(openLoops)
|
|
417
|
+
.values({
|
|
418
|
+
id: uuid(),
|
|
419
|
+
scopeId: item.scope_id,
|
|
420
|
+
summary: `${item.subject}: ${item.statement}`,
|
|
421
|
+
source: "backfill:item",
|
|
422
|
+
status: "open",
|
|
423
|
+
dueAt: item.last_seen_at,
|
|
424
|
+
createdAt: now,
|
|
425
|
+
updatedAt: now,
|
|
426
|
+
})
|
|
427
|
+
.run();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Extract a conversation ID from the summary's scope and scope_key.
|
|
435
|
+
* Returns null for non-conversation summaries.
|
|
436
|
+
*/
|
|
437
|
+
function extractConversationId(scope: string, scopeKey: string): string | null {
|
|
438
|
+
// Conversation summaries use scope "conversation" with scope_key as the ID
|
|
439
|
+
if (scope === "conversation") return scopeKey;
|
|
440
|
+
|
|
441
|
+
// Some summaries use "conversation:<id>" as scope_key
|
|
442
|
+
const match = scopeKey.match(/^conversation:(.+)$/);
|
|
443
|
+
if (match) return match[1];
|
|
444
|
+
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Build a human-readable episode title from the summary's scope metadata.
|
|
450
|
+
*/
|
|
451
|
+
function buildEpisodeTitle(scope: string, scopeKey: string): string {
|
|
452
|
+
if (scope === "conversation") {
|
|
453
|
+
return `Conversation summary`;
|
|
454
|
+
}
|
|
455
|
+
if (scope === "weekly") {
|
|
456
|
+
return `Weekly summary (${scopeKey})`;
|
|
457
|
+
}
|
|
458
|
+
if (scope === "monthly") {
|
|
459
|
+
return `Monthly summary (${scopeKey})`;
|
|
460
|
+
}
|
|
461
|
+
return `${scope} summary`;
|
|
462
|
+
}
|
|
@@ -176,11 +176,11 @@ async function generateStarters(scopeId: string): Promise<GeneratedStarter[]> {
|
|
|
176
176
|
? truncate(rawIdentityContext, 2000, "\n…[truncated]")
|
|
177
177
|
: null;
|
|
178
178
|
|
|
179
|
-
const systemPrompt = `You are generating 4 conversation starters for a personal assistant app. These appear as clickable chips on the empty conversation page — the first thing the user sees when they open the app.
|
|
179
|
+
const systemPrompt = `You are generating 4 conversation starters for a personal assistant app. These appear as clickable chips on the empty conversation page — the first thing the user sees when they open the app. Clicking a chip sends its prompt as a message from the user.
|
|
180
180
|
|
|
181
181
|
${timeContext}
|
|
182
182
|
|
|
183
|
-
Your goal:
|
|
183
|
+
Your goal: suggest the 4 most useful things this person could ask you to do right now.
|
|
184
184
|
|
|
185
185
|
${identityContext ? `## Assistant identity & user profile\n\n${identityContext}\n\n` : ""}## What you know
|
|
186
186
|
|
|
@@ -188,7 +188,9 @@ ${rollup}
|
|
|
188
188
|
${diff}
|
|
189
189
|
${skills}
|
|
190
190
|
|
|
191
|
-
##
|
|
191
|
+
## Selection
|
|
192
|
+
|
|
193
|
+
Generate exactly 4 starters, ranked #1 (best) to #4.
|
|
192
194
|
|
|
193
195
|
Start from the user's situation, not from the skill list. Ask yourself:
|
|
194
196
|
- What is this person likely dealing with right now (given the day/time and their context)?
|
|
@@ -197,11 +199,7 @@ Start from the user's situation, not from the skill list. Ask yourself:
|
|
|
197
199
|
|
|
198
200
|
The skills list tells you what the assistant CAN do — use it to filter out suggestions the assistant can't actually help with, not as a menu to generate suggestions from.
|
|
199
201
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Generate exactly 4 starters, ranked #1 (best) to #4.
|
|
203
|
-
|
|
204
|
-
For each, you must be able to clearly answer:
|
|
202
|
+
For each starter, you must clearly answer:
|
|
205
203
|
- Why now? (timing — day of week, recent activity, upcoming deadline)
|
|
206
204
|
- Why this user? (grounded in their specific context, not generic)
|
|
207
205
|
- Why would they be glad I suggested this? (genuine usefulness, not just relevance)
|
|
@@ -218,38 +216,34 @@ Favor what is live over what is merely true. Recent changes matter more than old
|
|
|
218
216
|
|
|
219
217
|
## Output format
|
|
220
218
|
|
|
221
|
-
Return exactly 4 starters in rank order (best first).
|
|
222
|
-
|
|
223
219
|
Each starter has:
|
|
224
|
-
- label: 3-6 words, max 40 chars, starts with a verb.
|
|
225
|
-
- prompt: 1-2 natural sentences,
|
|
220
|
+
- label: 3-6 words, max 40 chars, starts with a verb. Written in the user's voice — something they'd want to do, not something the assistant is offering.
|
|
221
|
+
- prompt: 1-2 natural sentences, as the user would actually say them.
|
|
226
222
|
- category: one of ${CONVERSATION_STARTER_CATEGORIES.join(", ")}
|
|
227
223
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Never include a chip whose primary meaning is configuration, setup, workflow creation, or "set up X for Y" unless it solves an urgent pain the user is actively feeling right now. Prefer the outcome over the mechanism — "Catch the emails that matter" beats "Set up a playbook for inbox."
|
|
224
|
+
## Constraints
|
|
231
225
|
|
|
232
|
-
|
|
226
|
+
**Voice**: The user clicks these chips to send a message. Every label must read as something the user is asking to do, never something the assistant is saying to the user.
|
|
233
227
|
|
|
234
|
-
|
|
228
|
+
**Coherence**: The 4 starters should feel like one set — similar abstraction level, no jarring mix of mundane chores and life strategy.
|
|
235
229
|
|
|
236
|
-
|
|
230
|
+
**Diversity**: Each chip covers a distinct topic. Never two chips about the same tool, project, or theme. Four topics, four chips.
|
|
237
231
|
|
|
238
|
-
|
|
232
|
+
**No setup chips**: Never include a chip whose primary meaning is configuration or "set up X for Y" unless it solves an urgent pain the user is actively feeling. Prefer the outcome over the mechanism.
|
|
239
233
|
|
|
240
|
-
|
|
234
|
+
**Natural language**: No jargon, project names, or raw memory phrases in labels unless they already sound natural in conversation. If a label sounds like a ticket title or backlog item, rewrite it as something the user would actually say.
|
|
241
235
|
|
|
242
|
-
|
|
236
|
+
## Examples
|
|
243
237
|
|
|
244
|
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
- BAD: "Restore outgoing Slack messages" → GOOD: "Get Slack messages flowing"
|
|
250
|
-
- BAD: "Set up a playbook for inbox" → GOOD: "Catch the emails that matter"
|
|
238
|
+
Bad → Good (ticket-speak → natural):
|
|
239
|
+
- "Fix Slack Socket Mode blocker" → "Fix Slack so it just works"
|
|
240
|
+
- "Restore outgoing Slack messages" → "Get Slack messages flowing"
|
|
241
|
+
- "Review this week's calendar" → "Protect this week's focus"
|
|
242
|
+
- "Set up a playbook for inbox" → "Triage my inbox"
|
|
251
243
|
|
|
252
|
-
|
|
244
|
+
Bad → Good (assistant voice → user voice):
|
|
245
|
+
- "You've got a busy week ahead" → "Plan my week ahead"
|
|
246
|
+
- "Let me check your calendar" → "Check my Thursday schedule"`;
|
|
253
247
|
|
|
254
248
|
const { signal, cleanup } = createTimeout(20000);
|
|
255
249
|
try {
|
|
@@ -274,7 +268,7 @@ The good versions emphasize the user's payoff, not the internal mechanism.`;
|
|
|
274
268
|
label: {
|
|
275
269
|
type: "string",
|
|
276
270
|
description:
|
|
277
|
-
"
|
|
271
|
+
"User-voice chip label (2-7 words, max 40 chars, verb-first)",
|
|
278
272
|
},
|
|
279
273
|
prompt: {
|
|
280
274
|
type: "string",
|