@vellumai/assistant 0.5.7 → 0.5.9
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 +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/eslint.config.mjs +0 -31
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-agent-loop.ts +6 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-runtime-assembly.ts +61 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/inline-command-expansions.ts +7 -7
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/sensitive-output-placeholders.ts +2 -2
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -19,18 +19,25 @@ import { memorySegments, memorySummaries } from "../schema.js";
|
|
|
19
19
|
const log = getLogger("memory-jobs-worker");
|
|
20
20
|
|
|
21
21
|
const SUMMARY_LLM_TIMEOUT_MS = 20_000;
|
|
22
|
-
const SUMMARY_MAX_TOKENS =
|
|
22
|
+
const SUMMARY_MAX_TOKENS = 500;
|
|
23
23
|
|
|
24
24
|
const CONVERSATION_SUMMARY_SYSTEM_PROMPT = [
|
|
25
|
-
"You
|
|
25
|
+
"You compress conversation transcripts into compact summaries for semantic search and memory retrieval.",
|
|
26
|
+
"Focus on durable facts, not transient discussion.",
|
|
27
|
+
"Preserve: goals, decisions, constraints, preferences, names, technical details, actions taken.",
|
|
28
|
+
"Remove: filler, pleasantries, tool invocation details, transient status updates.",
|
|
26
29
|
"",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
30
|
+
"Return concise markdown:",
|
|
31
|
+
"## Topic",
|
|
32
|
+
"One-line description of what the conversation is about.",
|
|
33
|
+
"## Key Facts",
|
|
34
|
+
"Bullet points of concrete facts, names, decisions, preferences.",
|
|
35
|
+
"## Outcomes",
|
|
36
|
+
"What was decided, resolved, or accomplished.",
|
|
37
|
+
"## Open Items",
|
|
38
|
+
"Unresolved questions, pending tasks, or follow-ups (omit section if none).",
|
|
39
|
+
"",
|
|
40
|
+
"Target 200-400 tokens. Be dense.",
|
|
34
41
|
].join("\n");
|
|
35
42
|
|
|
36
43
|
export async function buildConversationSummaryJob(
|
|
@@ -62,9 +69,8 @@ export async function buildConversationSummaryJob(
|
|
|
62
69
|
|
|
63
70
|
// Build segment text for LLM input (chronological order)
|
|
64
71
|
const segmentTexts = rows
|
|
65
|
-
.slice(0, 30)
|
|
66
72
|
.reverse()
|
|
67
|
-
.map((row) => `[${row.role}] ${truncate(row.text,
|
|
73
|
+
.map((row) => `[${row.role}] ${truncate(row.text, 600)}`)
|
|
68
74
|
.join("\n\n");
|
|
69
75
|
|
|
70
76
|
const summaryText = await summarizeWithLLM(
|
|
@@ -208,14 +214,18 @@ async function summarizeWithLLM(
|
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
function buildFallbackSummary(
|
|
211
|
-
|
|
217
|
+
existingSummary: string | null,
|
|
212
218
|
newContent: string,
|
|
213
219
|
label: string,
|
|
214
220
|
): string {
|
|
215
221
|
const lines = newContent.split("\n").filter((l) => l.trim().length > 0);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
if (lines.length === 0) return existingSummary ?? `${label} (no content)`;
|
|
223
|
+
const head = lines.slice(0, 3).map((l) => `- ${truncate(l.trim(), 200)}`);
|
|
224
|
+
const tail =
|
|
225
|
+
lines.length > 6
|
|
226
|
+
? lines.slice(-3).map((l) => `- ${truncate(l.trim(), 200)}`)
|
|
227
|
+
: [];
|
|
228
|
+
const parts = [`${label} summary`, "", ...head];
|
|
229
|
+
if (tail.length > 0) parts.push("", "...", "", ...tail);
|
|
220
230
|
return parts.join("\n");
|
|
221
231
|
}
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, asc, eq, inArray, lte, notInArray } from "drizzle-orm";
|
|
1
|
+
import { and, asc, eq, inArray, lte, notInArray, sql } from "drizzle-orm";
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import { getLogger } from "../util/logger.js";
|
|
@@ -86,6 +86,38 @@ export function enqueueMemoryJob(
|
|
|
86
86
|
return id;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Upsert a debounced job: if a pending job of the same type and conversation
|
|
91
|
+
* already exists, push its `runAfter` forward instead of creating a duplicate.
|
|
92
|
+
* This prevents rapid message indexing from spawning redundant jobs.
|
|
93
|
+
*/
|
|
94
|
+
export function upsertDebouncedJob(
|
|
95
|
+
type: MemoryJobType,
|
|
96
|
+
payload: { conversationId: string },
|
|
97
|
+
runAfter: number,
|
|
98
|
+
): void {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
const existing = db
|
|
101
|
+
.select()
|
|
102
|
+
.from(memoryJobs)
|
|
103
|
+
.where(
|
|
104
|
+
and(
|
|
105
|
+
eq(memoryJobs.type, type),
|
|
106
|
+
eq(memoryJobs.status, "pending"),
|
|
107
|
+
sql`json_extract(${memoryJobs.payload}, '$.conversationId') = ${payload.conversationId}`,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
.get();
|
|
111
|
+
if (existing) {
|
|
112
|
+
db.update(memoryJobs)
|
|
113
|
+
.set({ runAfter, updatedAt: Date.now() })
|
|
114
|
+
.where(eq(memoryJobs.id, existing.id))
|
|
115
|
+
.run();
|
|
116
|
+
} else {
|
|
117
|
+
enqueueMemoryJob(type, payload, runAfter);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
export function enqueueCleanupStaleSupersededItemsJob(
|
|
90
122
|
retentionMs?: number,
|
|
91
123
|
): string {
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { and, eq } from "drizzle-orm";
|
|
5
|
+
import { v4 as uuid } from "uuid";
|
|
6
|
+
|
|
7
|
+
import { getLogger } from "../util/logger.js";
|
|
8
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
9
|
+
import { type DrizzleDb, getDb } from "./db.js";
|
|
10
|
+
import { computeMemoryFingerprint } from "./fingerprint.js";
|
|
11
|
+
import { enqueueMemoryJob } from "./jobs-store.js";
|
|
12
|
+
import { memoryItems, memoryItemSources } from "./schema.js";
|
|
13
|
+
|
|
14
|
+
const log = getLogger("memory-journal");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Process a single journal `.md` file: read content, derive subject, compute
|
|
18
|
+
* fingerprint, upsert to DB, and enqueue an embed job.
|
|
19
|
+
*
|
|
20
|
+
* Returns `true` if a new memory item was inserted, `false` if it already
|
|
21
|
+
* existed (or was skipped).
|
|
22
|
+
*/
|
|
23
|
+
function upsertSingleJournalFile(
|
|
24
|
+
filepath: string,
|
|
25
|
+
filename: string,
|
|
26
|
+
messageCreatedAt: number,
|
|
27
|
+
scopeId: string,
|
|
28
|
+
messageId: string,
|
|
29
|
+
db: DrizzleDb,
|
|
30
|
+
): boolean {
|
|
31
|
+
const content = readFileSync(filepath, "utf-8");
|
|
32
|
+
|
|
33
|
+
// Derive subject from filename:
|
|
34
|
+
// strip .md extension, strip leading date prefix, replace hyphens with spaces, capitalize first letter
|
|
35
|
+
const basename = filename.replace(/\.md$/, "");
|
|
36
|
+
const withoutDate = basename.replace(/^\d{4}-\d{2}-\d{2}-?/, "");
|
|
37
|
+
const withSpaces = withoutDate.replace(/-/g, " ");
|
|
38
|
+
const subject =
|
|
39
|
+
withSpaces.length > 0
|
|
40
|
+
? withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1)
|
|
41
|
+
: basename;
|
|
42
|
+
|
|
43
|
+
const fingerprint = computeMemoryFingerprint(
|
|
44
|
+
scopeId,
|
|
45
|
+
"journal",
|
|
46
|
+
subject,
|
|
47
|
+
content,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const existing = db
|
|
51
|
+
.select()
|
|
52
|
+
.from(memoryItems)
|
|
53
|
+
.where(
|
|
54
|
+
and(
|
|
55
|
+
eq(memoryItems.fingerprint, fingerprint),
|
|
56
|
+
eq(memoryItems.scopeId, scopeId),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
.get();
|
|
60
|
+
|
|
61
|
+
let memoryItemId: string;
|
|
62
|
+
let inserted = false;
|
|
63
|
+
|
|
64
|
+
if (existing) {
|
|
65
|
+
memoryItemId = existing.id;
|
|
66
|
+
db.update(memoryItems)
|
|
67
|
+
.set({
|
|
68
|
+
lastSeenAt: messageCreatedAt,
|
|
69
|
+
status: "active",
|
|
70
|
+
})
|
|
71
|
+
.where(eq(memoryItems.id, existing.id))
|
|
72
|
+
.run();
|
|
73
|
+
} else {
|
|
74
|
+
memoryItemId = uuid();
|
|
75
|
+
db.insert(memoryItems)
|
|
76
|
+
.values({
|
|
77
|
+
id: memoryItemId,
|
|
78
|
+
kind: "journal",
|
|
79
|
+
subject,
|
|
80
|
+
statement: content,
|
|
81
|
+
status: "active",
|
|
82
|
+
confidence: 0.95,
|
|
83
|
+
importance: 0.8,
|
|
84
|
+
fingerprint,
|
|
85
|
+
sourceType: "extraction",
|
|
86
|
+
sourceMessageRole: "assistant",
|
|
87
|
+
verificationState: "assistant_inferred",
|
|
88
|
+
scopeId,
|
|
89
|
+
firstSeenAt: messageCreatedAt,
|
|
90
|
+
lastSeenAt: messageCreatedAt,
|
|
91
|
+
lastUsedAt: null,
|
|
92
|
+
supersedes: null,
|
|
93
|
+
overrideConfidence: null,
|
|
94
|
+
})
|
|
95
|
+
.run();
|
|
96
|
+
inserted = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
db.insert(memoryItemSources)
|
|
100
|
+
.values({
|
|
101
|
+
memoryItemId,
|
|
102
|
+
messageId,
|
|
103
|
+
evidence: content,
|
|
104
|
+
createdAt: Date.now(),
|
|
105
|
+
})
|
|
106
|
+
.onConflictDoNothing()
|
|
107
|
+
.run();
|
|
108
|
+
|
|
109
|
+
enqueueMemoryJob("embed_item", { itemId: memoryItemId });
|
|
110
|
+
|
|
111
|
+
return inserted;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Scan the journal directory for `.md` files created during (or after) the
|
|
116
|
+
* given message timestamp and upsert them as journal memory items with the
|
|
117
|
+
* raw, unedited file content as the `statement`.
|
|
118
|
+
*
|
|
119
|
+
* Also scans immediate subdirectories (e.g. per-user folders like
|
|
120
|
+
* `journal/sidd/`) so that user-scoped journal entries are indexed alongside
|
|
121
|
+
* root-level files.
|
|
122
|
+
*
|
|
123
|
+
* This bypasses the LLM extraction layer entirely — journal memories are
|
|
124
|
+
* stored verbatim so they are never summarised or rewritten.
|
|
125
|
+
*
|
|
126
|
+
* Returns the number of newly inserted items.
|
|
127
|
+
*/
|
|
128
|
+
export function upsertJournalMemoriesFromDisk(
|
|
129
|
+
messageCreatedAt: number,
|
|
130
|
+
scopeId: string,
|
|
131
|
+
messageId: string,
|
|
132
|
+
): number {
|
|
133
|
+
try {
|
|
134
|
+
const journalDir = join(getWorkspaceDir(), "journal");
|
|
135
|
+
|
|
136
|
+
let files: string[];
|
|
137
|
+
try {
|
|
138
|
+
files = readdirSync(journalDir);
|
|
139
|
+
} catch {
|
|
140
|
+
// Directory doesn't exist — no journal entries
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Filter for .md files, excluding readme.md (case-insensitive)
|
|
145
|
+
const mdFiles = files.filter(
|
|
146
|
+
(f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
let upserted = 0;
|
|
150
|
+
const db = getDb();
|
|
151
|
+
|
|
152
|
+
for (const filename of mdFiles) {
|
|
153
|
+
try {
|
|
154
|
+
const filepath = join(journalDir, filename);
|
|
155
|
+
const stat = statSync(filepath);
|
|
156
|
+
if (!stat.isFile()) continue;
|
|
157
|
+
|
|
158
|
+
// Only process files created during or after this message
|
|
159
|
+
if (stat.birthtimeMs < messageCreatedAt) continue;
|
|
160
|
+
|
|
161
|
+
if (upsertSingleJournalFile(filepath, filename, messageCreatedAt, scopeId, messageId, db)) {
|
|
162
|
+
upserted += 1;
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log.warn(
|
|
166
|
+
{ filename, err: err instanceof Error ? err.message : String(err) },
|
|
167
|
+
"Failed to process journal file for memory — skipping",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Scan per-user journal subdirectories
|
|
173
|
+
for (const entry of files) {
|
|
174
|
+
try {
|
|
175
|
+
const subdirPath = join(journalDir, entry);
|
|
176
|
+
if (!statSync(subdirPath).isDirectory()) continue;
|
|
177
|
+
|
|
178
|
+
const subFiles = readdirSync(subdirPath).filter(
|
|
179
|
+
(f) => f.endsWith(".md") && f.toLowerCase() !== "readme.md",
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
for (const filename of subFiles) {
|
|
183
|
+
try {
|
|
184
|
+
const filepath = join(subdirPath, filename);
|
|
185
|
+
const stat = statSync(filepath);
|
|
186
|
+
if (!stat.isFile()) continue;
|
|
187
|
+
|
|
188
|
+
// Only process files created during or after this message
|
|
189
|
+
if (stat.birthtimeMs < messageCreatedAt) continue;
|
|
190
|
+
|
|
191
|
+
if (upsertSingleJournalFile(filepath, filename, messageCreatedAt, scopeId, messageId, db)) {
|
|
192
|
+
upserted += 1;
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
log.warn(
|
|
196
|
+
{ filename, err: err instanceof Error ? err.message : String(err) },
|
|
197
|
+
"Failed to process journal file for memory — skipping",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// Skip unreadable subdirectories
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return upserted;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
log.warn(
|
|
209
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
210
|
+
"Failed to scan journal directory for memories",
|
|
211
|
+
);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
4
|
+
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Add source_type and source_message_role columns to memory_items.
|
|
8
|
+
*
|
|
9
|
+
* - source_type: "extraction" (default) or "tool" — distinguishes how the
|
|
10
|
+
* memory was created (LLM/pattern extraction vs explicit tool/API save).
|
|
11
|
+
* - source_message_role: the role of the source message (e.g. "user",
|
|
12
|
+
* "assistant") when the item was created via extraction.
|
|
13
|
+
*
|
|
14
|
+
* Backfills:
|
|
15
|
+
* 1. Items with verification_state = "user_confirmed" → source_type = "tool"
|
|
16
|
+
* 2. source_message_role from the earliest source message's role via subquery
|
|
17
|
+
*/
|
|
18
|
+
export function migrateAddSourceTypeColumns(database: DrizzleDb): void {
|
|
19
|
+
withCrashRecovery(database, "migration_add_source_type_columns_v1", () => {
|
|
20
|
+
const raw = getSqliteFrom(database);
|
|
21
|
+
|
|
22
|
+
// Add source_type column if it doesn't exist
|
|
23
|
+
if (!tableHasColumn(database, "memory_items", "source_type")) {
|
|
24
|
+
raw.exec(
|
|
25
|
+
/*sql*/ `ALTER TABLE memory_items ADD COLUMN source_type TEXT NOT NULL DEFAULT 'extraction'`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Add source_message_role column if it doesn't exist
|
|
30
|
+
if (!tableHasColumn(database, "memory_items", "source_message_role")) {
|
|
31
|
+
raw.exec(
|
|
32
|
+
/*sql*/ `ALTER TABLE memory_items ADD COLUMN source_message_role TEXT`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Backfill source_type = 'tool' for items that were explicitly saved
|
|
37
|
+
raw.exec(
|
|
38
|
+
/*sql*/ `UPDATE memory_items SET source_type = 'tool' WHERE verification_state = 'user_confirmed'`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Backfill source_message_role from the earliest source message's role.
|
|
42
|
+
// Only backfill where source_message_role is currently NULL and a source
|
|
43
|
+
// message exists.
|
|
44
|
+
raw.exec(/*sql*/ `
|
|
45
|
+
UPDATE memory_items
|
|
46
|
+
SET source_message_role = (
|
|
47
|
+
SELECT m.role
|
|
48
|
+
FROM memory_item_sources mis
|
|
49
|
+
JOIN messages m ON m.id = mis.message_id
|
|
50
|
+
WHERE mis.memory_item_id = memory_items.id
|
|
51
|
+
ORDER BY mis.created_at ASC
|
|
52
|
+
LIMIT 1
|
|
53
|
+
)
|
|
54
|
+
WHERE source_message_role IS NULL
|
|
55
|
+
AND EXISTS (
|
|
56
|
+
SELECT 1
|
|
57
|
+
FROM memory_item_sources mis
|
|
58
|
+
WHERE mis.memory_item_id = memory_items.id
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reverse: drop source_type and source_message_role columns.
|
|
66
|
+
*
|
|
67
|
+
* SQLite doesn't support DROP COLUMN on older versions, but modern SQLite
|
|
68
|
+
* (3.35.0+) does. Since Bun bundles a modern SQLite, this is safe.
|
|
69
|
+
*/
|
|
70
|
+
export function migrateAddSourceTypeColumnsDown(database: DrizzleDb): void {
|
|
71
|
+
const raw = getSqliteFrom(database);
|
|
72
|
+
|
|
73
|
+
if (tableHasColumn(database, "memory_items", "source_type")) {
|
|
74
|
+
raw.exec(/*sql*/ `ALTER TABLE memory_items DROP COLUMN source_type`);
|
|
75
|
+
}
|
|
76
|
+
if (tableHasColumn(database, "memory_items", "source_message_role")) {
|
|
77
|
+
raw.exec(
|
|
78
|
+
/*sql*/ `ALTER TABLE memory_items DROP COLUMN source_message_role`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -131,6 +131,7 @@ export { migrateDropSimplifiedMemory } from "./189-drop-simplified-memory.js";
|
|
|
131
131
|
export { migrateCallSessionSkipDisclosure } from "./190-call-session-skip-disclosure.js";
|
|
132
132
|
export { migrateBackfillAudioAttachmentMimeTypes } from "./191-backfill-audio-attachment-mime-types.js";
|
|
133
133
|
export { migrateContactsUserFileColumn } from "./192-contacts-user-file-column.js";
|
|
134
|
+
export { migrateAddSourceTypeColumns } from "./193-add-source-type-columns.js";
|
|
134
135
|
export {
|
|
135
136
|
MIGRATION_REGISTRY,
|
|
136
137
|
type MigrationRegistryEntry,
|
|
@@ -37,6 +37,7 @@ import { migrateDropCapabilityCardStateDown } from "./176-drop-capability-card-s
|
|
|
37
37
|
import { migrateBackfillInlineAttachmentsToDiskDown } from "./180-backfill-inline-attachments-to-disk.js";
|
|
38
38
|
import { migrateRenameThreadStartersCheckpointsDown } from "./181-rename-thread-starters-checkpoints.js";
|
|
39
39
|
import { migrateBackfillAudioAttachmentMimeTypesDown } from "./191-backfill-audio-attachment-mime-types.js";
|
|
40
|
+
import { migrateAddSourceTypeColumnsDown } from "./193-add-source-type-columns.js";
|
|
40
41
|
|
|
41
42
|
export interface MigrationRegistryEntry {
|
|
42
43
|
/** The checkpoint key written to memory_checkpoints on completion. */
|
|
@@ -325,6 +326,13 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
|
|
|
325
326
|
"Backfill correct MIME types for audio attachments stored as application/octet-stream due to missing extension map entries",
|
|
326
327
|
down: migrateBackfillAudioAttachmentMimeTypesDown,
|
|
327
328
|
},
|
|
329
|
+
{
|
|
330
|
+
key: "migration_add_source_type_columns_v1",
|
|
331
|
+
version: 37,
|
|
332
|
+
description:
|
|
333
|
+
"Add source_type and source_message_role columns to memory_items with backfill from verification_state and source messages",
|
|
334
|
+
down: migrateAddSourceTypeColumnsDown,
|
|
335
|
+
},
|
|
328
336
|
];
|
|
329
337
|
|
|
330
338
|
export function getMaxMigrationVersion(): number {
|
|
@@ -337,10 +337,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
337
337
|
expect(result.tier1Count).toBeDefined();
|
|
338
338
|
expect(result.tier2Count).toBeDefined();
|
|
339
339
|
expect(result.hybridSearchMs).toBeDefined();
|
|
340
|
-
//
|
|
341
|
-
expect(result.recencyHits).toBeGreaterThan(0);
|
|
342
|
-
// …but they are filtered out because they belong to the active
|
|
343
|
-
// conversation and are already present in the conversation history.
|
|
340
|
+
// Without semantic search, no candidates are found.
|
|
344
341
|
expect(result.mergedCount).toBe(0);
|
|
345
342
|
});
|
|
346
343
|
|
|
@@ -403,11 +400,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
403
400
|
);
|
|
404
401
|
|
|
405
402
|
expect(result.enabled).toBe(true);
|
|
406
|
-
//
|
|
407
|
-
expect(result.recencyHits).toBeGreaterThan(0);
|
|
408
|
-
// But they are filtered out of merged results; only other-conversation
|
|
409
|
-
// segments would survive (none in this case since recency is scoped to
|
|
410
|
-
// the active conversation).
|
|
403
|
+
// Without semantic search, no candidates are found.
|
|
411
404
|
expect(result.mergedCount).toBe(0);
|
|
412
405
|
});
|
|
413
406
|
|
|
@@ -506,10 +499,6 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
506
499
|
);
|
|
507
500
|
|
|
508
501
|
expect(result.enabled).toBe(true);
|
|
509
|
-
// Recency search finds segments from this conversation
|
|
510
|
-
expect(result.recencyHits).toBeGreaterThan(0);
|
|
511
|
-
// Compacted segments survive filtering — they are no longer in context
|
|
512
|
-
expect(result.mergedCount).toBeGreaterThan(0);
|
|
513
502
|
});
|
|
514
503
|
|
|
515
504
|
// -----------------------------------------------------------------------
|
|
@@ -716,9 +705,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
716
705
|
expect(result.enabled).toBe(true);
|
|
717
706
|
// Semantic/hybrid search should be skipped
|
|
718
707
|
expect(result.semanticHits).toBe(0);
|
|
719
|
-
//
|
|
720
|
-
expect(result.recencyHits).toBeGreaterThan(0);
|
|
721
|
-
// …but current-conversation segments are filtered out
|
|
708
|
+
// Without semantic search, no candidates are found.
|
|
722
709
|
expect(result.mergedCount).toBe(0);
|
|
723
710
|
});
|
|
724
711
|
|
|
@@ -752,7 +739,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
752
739
|
expect(result.degradation).toBeDefined();
|
|
753
740
|
expect(result.degradation!.semanticUnavailable).toBe(true);
|
|
754
741
|
expect(result.degradation!.reason).toBe("embedding_provider_down");
|
|
755
|
-
expect(result.degradation!.fallbackSources).
|
|
742
|
+
expect(result.degradation!.fallbackSources).toEqual([]);
|
|
756
743
|
});
|
|
757
744
|
|
|
758
745
|
// -----------------------------------------------------------------------
|
|
@@ -864,9 +851,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
864
851
|
// pipeline proceeds non-degraded end-to-end.
|
|
865
852
|
expect(result.enabled).toBe(true);
|
|
866
853
|
expect(result.degraded).toBe(false);
|
|
867
|
-
//
|
|
868
|
-
expect(result.recencyHits).toBeGreaterThan(0);
|
|
869
|
-
// Current-conversation segments are filtered out of merged results
|
|
854
|
+
// Without semantic search, no candidates are found.
|
|
870
855
|
expect(result.mergedCount).toBe(0);
|
|
871
856
|
});
|
|
872
857
|
|
|
@@ -1142,8 +1127,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
1142
1127
|
);
|
|
1143
1128
|
|
|
1144
1129
|
// Simulate Qdrant returning the parent-conversation segment as a
|
|
1145
|
-
// semantic hit so it enters the candidate map
|
|
1146
|
-
// to forkConv and would never find it).
|
|
1130
|
+
// semantic hit so it enters the candidate map.
|
|
1147
1131
|
mockQdrantResults.push({
|
|
1148
1132
|
id: "qdrant-fork-1",
|
|
1149
1133
|
score: 0.9,
|
|
@@ -1264,6 +1248,35 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
1264
1248
|
now - 50_000,
|
|
1265
1249
|
);
|
|
1266
1250
|
|
|
1251
|
+
// Simulate Qdrant returning both segments as semantic hits so they
|
|
1252
|
+
// enter the candidate map (recency search was removed).
|
|
1253
|
+
mockQdrantResults.push(
|
|
1254
|
+
{
|
|
1255
|
+
id: "qdrant-compact-fork-1",
|
|
1256
|
+
score: 0.9,
|
|
1257
|
+
payload: {
|
|
1258
|
+
target_type: "segment",
|
|
1259
|
+
target_id: "seg-compact-fork",
|
|
1260
|
+
text: "compacted parent topic detail",
|
|
1261
|
+
created_at: now - 100_000,
|
|
1262
|
+
message_id: "fork-compact-msg-1",
|
|
1263
|
+
conversation_id: forkConv,
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
id: "qdrant-compact-fork-2",
|
|
1268
|
+
score: 0.85,
|
|
1269
|
+
payload: {
|
|
1270
|
+
target_type: "segment",
|
|
1271
|
+
target_id: "seg-in-context-fork",
|
|
1272
|
+
text: "recent fork topic detail",
|
|
1273
|
+
created_at: now - 50_000,
|
|
1274
|
+
message_id: "fork-compact-msg-3",
|
|
1275
|
+
conversation_id: forkConv,
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1267
1280
|
const result = await buildMemoryRecall(
|
|
1268
1281
|
"compacted parent topic",
|
|
1269
1282
|
forkConv,
|
|
@@ -1273,7 +1286,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
1273
1286
|
expect(result.enabled).toBe(true);
|
|
1274
1287
|
// The segment from the compacted fork message survives filtering
|
|
1275
1288
|
// (its source message is no longer in context). The in-context segment
|
|
1276
|
-
// is filtered out.
|
|
1289
|
+
// is filtered out. Semantic search returns both, but only the compacted
|
|
1277
1290
|
// one survives step 5b.
|
|
1278
1291
|
expect(result.mergedCount).toBeGreaterThan(0);
|
|
1279
1292
|
});
|
|
@@ -1342,8 +1355,7 @@ describe("Memory Retriever Pipeline", () => {
|
|
|
1342
1355
|
);
|
|
1343
1356
|
|
|
1344
1357
|
// Simulate Qdrant returning the grandparent segment as a semantic hit
|
|
1345
|
-
// so it enters the candidate map
|
|
1346
|
-
// and would never find it).
|
|
1358
|
+
// so it enters the candidate map.
|
|
1347
1359
|
mockQdrantResults.push({
|
|
1348
1360
|
id: "qdrant-gp-1",
|
|
1349
1361
|
score: 0.9,
|