@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
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { and, eq, like, sql } from "drizzle-orm";
|
|
1
|
+
import { and, desc, eq, like, sql } from "drizzle-orm";
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import { getConfig } from "../config/loader.js";
|
|
5
5
|
import type { MemoryExtractionConfig } from "../config/types.js";
|
|
6
|
+
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
6
7
|
import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
|
|
7
8
|
import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
|
|
8
9
|
import {
|
|
9
|
-
createTimeout,
|
|
10
10
|
extractToolUse,
|
|
11
11
|
getConfiguredProvider,
|
|
12
12
|
userMessage,
|
|
13
13
|
} from "../providers/provider-send-message.js";
|
|
14
|
+
import { BackendUnavailableError } from "../util/errors.js";
|
|
14
15
|
import { getLogger } from "../util/logger.js";
|
|
15
16
|
import { truncate } from "../util/truncate.js";
|
|
16
17
|
import { maybeEnqueueConversationStartersJob } from "./conversation-starters-cadence.js";
|
|
17
18
|
import { getDb } from "./db.js";
|
|
18
19
|
import { computeMemoryFingerprint } from "./fingerprint.js";
|
|
19
20
|
import { enqueueMemoryJob } from "./jobs-store.js";
|
|
21
|
+
import { upsertJournalMemoriesFromDisk } from "./journal-memory.js";
|
|
20
22
|
import { extractTextFromStoredMessageContent } from "./message-content.js";
|
|
21
23
|
import { withQdrantBreaker } from "./qdrant-circuit-breaker.js";
|
|
22
24
|
import { getQdrantClient } from "./qdrant-client.js";
|
|
@@ -32,7 +34,8 @@ export type MemoryItemKind =
|
|
|
32
34
|
| "project"
|
|
33
35
|
| "decision"
|
|
34
36
|
| "constraint"
|
|
35
|
-
| "event"
|
|
37
|
+
| "event"
|
|
38
|
+
| "journal";
|
|
36
39
|
|
|
37
40
|
export type OverrideConfidence = "explicit" | "tentative" | "inferred";
|
|
38
41
|
|
|
@@ -56,8 +59,16 @@ const VALID_KINDS = new Set<string>([
|
|
|
56
59
|
"decision",
|
|
57
60
|
"constraint",
|
|
58
61
|
"event",
|
|
62
|
+
"journal",
|
|
59
63
|
]);
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Kinds the LLM is allowed to produce during extraction. Excludes "journal"
|
|
67
|
+
* because journal memories are created directly from disk files — any
|
|
68
|
+
* LLM-produced journal items would be silently dropped, wasting tokens.
|
|
69
|
+
*/
|
|
70
|
+
const EXTRACTION_KINDS = [...VALID_KINDS].filter((k) => k !== "journal");
|
|
71
|
+
|
|
61
72
|
/** Maps old kind names to their new equivalents for graceful migration. */
|
|
62
73
|
const KIND_MIGRATION_MAP: Record<string, MemoryItemKind> = {
|
|
63
74
|
profile: "identity",
|
|
@@ -184,17 +195,57 @@ For each item, provide:
|
|
|
184
195
|
Rules:
|
|
185
196
|
- Only extract genuinely memorable information. Skip pleasantries, filler, and transient discussion.
|
|
186
197
|
- Do NOT extract information about what tools the assistant used or what files it read — only extract substantive facts about the user, their projects, and their preferences.
|
|
187
|
-
- Do NOT extract claims about actions the assistant performed, outcomes it achieved, or progress it reported (e.g., "I booked an appointment", "I sent the email"). Only extract facts stated by the user or from external sources — the assistant's self-reports are not reliable memory material.
|
|
188
198
|
- Do NOT extract raw code snippets, JSON fragments, YAML, configuration values, log output, or data structures. Only extract the human-readable meaning or intent behind such content, not the literal syntax.
|
|
189
199
|
- Prefer fewer high-quality items over many low-quality ones.
|
|
190
|
-
- If the message contains no memorable information, return an empty array
|
|
200
|
+
- If the message contains no memorable information, return an empty array.
|
|
201
|
+
- The preceding conversation context (if provided) is for disambiguation only. Extract items ONLY from the final message after the --- separator, not from the context messages.`;
|
|
202
|
+
|
|
203
|
+
// Try to extract user name from persona text
|
|
204
|
+
let userName = "the user";
|
|
205
|
+
if (userPersona) {
|
|
206
|
+
const nameMatch = userPersona.match(/\*\*Name:\*\*\s*(.+)/);
|
|
207
|
+
if (nameMatch) {
|
|
208
|
+
userName = nameMatch[1].trim();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
191
211
|
|
|
192
212
|
if (messageRole === "assistant") {
|
|
193
213
|
instructions += `
|
|
194
214
|
|
|
195
|
-
IMPORTANT: The message below is from the ASSISTANT
|
|
215
|
+
IMPORTANT: The message below is from the ASSISTANT. You may extract facts about actions taken, decisions made, and outcomes achieved. However, do NOT attribute the assistant's own identity, personality, or self-descriptions to the user. If the assistant is just introducing itself or expressing uncertainty about its own nature, extract nothing.`;
|
|
196
216
|
}
|
|
197
217
|
|
|
218
|
+
instructions += `
|
|
219
|
+
|
|
220
|
+
## Examples
|
|
221
|
+
|
|
222
|
+
Good extractions from user messages:
|
|
223
|
+
- "I'm a backend engineer at Acme Corp, mostly working with Go and PostgreSQL"
|
|
224
|
+
→ kind: identity, subject: "Role at Acme Corp", statement: "${userName} is a backend engineer at Acme Corp, works primarily with Go and PostgreSQL"
|
|
225
|
+
|
|
226
|
+
- "Always use semantic commits in this repo. I hate squash merges."
|
|
227
|
+
→ kind: constraint, subject: "Git conventions", statement: "${userName} requires semantic commit messages. Strongly dislikes squash merges."
|
|
228
|
+
|
|
229
|
+
- "We decided to go with Redis for the cache layer because DynamoDB was too expensive at our read volume"
|
|
230
|
+
→ kind: decision, subject: "Cache layer choice", statement: "${userName} chose Redis over DynamoDB for caching due to cost at high read volumes"
|
|
231
|
+
|
|
232
|
+
Good extractions from assistant messages:
|
|
233
|
+
- "Based on your earlier mention, I see you're using Next.js 14 with the app router for the dashboard project."
|
|
234
|
+
→ kind: project, subject: "Dashboard tech stack", statement: "${userName}'s dashboard project uses Next.js 14 with the app router"
|
|
235
|
+
|
|
236
|
+
- "Since you mentioned your team follows trunk-based development, I'll keep the changes in a single commit."
|
|
237
|
+
→ kind: constraint, subject: "Team branching strategy", statement: "${userName}'s team follows trunk-based development"
|
|
238
|
+
|
|
239
|
+
- "I've refactored the auth middleware to use JWT validation and added rate limiting to the login endpoint."
|
|
240
|
+
→ kind: project, subject: "Auth middleware changes", statement: "Auth middleware was refactored to use JWT validation with rate limiting on the login endpoint"
|
|
241
|
+
|
|
242
|
+
Do NOT extract:
|
|
243
|
+
- "I'll check that file for you" → assistant operational statement with no lasting information
|
|
244
|
+
- "I think the best approach would be to refactor this" → speculative, no action taken yet
|
|
245
|
+
- "The tests passed" → transient status
|
|
246
|
+
- "Sure, sounds good" → filler
|
|
247
|
+
- "\`\`\`json {"key": "val"} \`\`\`" → raw code/data, extract meaning not syntax`;
|
|
248
|
+
|
|
198
249
|
if (existingItems.length > 0) {
|
|
199
250
|
instructions += `\n\nExisting memory items (use these to identify supersession targets — set \`supersedes\` to the item ID if the new information replaces one of these):\n`;
|
|
200
251
|
for (const item of existingItems) {
|
|
@@ -206,8 +257,7 @@ IMPORTANT: The message below is from the ASSISTANT, not the user. Do NOT attribu
|
|
|
206
257
|
// generic "User ..." labels. Budget is dynamically computed: whatever
|
|
207
258
|
// remains after the fixed instructions fits within the system prompt
|
|
208
259
|
// ceiling, preventing oversized prompts from exceeding the provider input
|
|
209
|
-
// window (which would cause sendMessage to error
|
|
210
|
-
// lower-quality pattern-based extraction).
|
|
260
|
+
// window (which would cause sendMessage to error).
|
|
211
261
|
const rawIdentityContext = buildCoreIdentityContext(
|
|
212
262
|
userPersona ? { userPersona } : undefined,
|
|
213
263
|
);
|
|
@@ -320,188 +370,200 @@ async function extractItemsWithLLM(
|
|
|
320
370
|
extractionConfig: MemoryExtractionConfig,
|
|
321
371
|
scopeId: string,
|
|
322
372
|
messageRole: string,
|
|
373
|
+
precedingMessages: Array<{ role: string; content: string }>,
|
|
323
374
|
userPersona?: string | null,
|
|
324
375
|
): Promise<ExtractedItem[]> {
|
|
325
376
|
const provider = await getConfiguredProvider();
|
|
326
377
|
if (!provider) {
|
|
327
|
-
|
|
328
|
-
"
|
|
378
|
+
throw new BackendUnavailableError(
|
|
379
|
+
"Provider unavailable for memory extraction",
|
|
329
380
|
);
|
|
330
|
-
return extractItemsPatternBased(text, scopeId);
|
|
331
381
|
}
|
|
332
382
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
383
|
+
// Query existing items to give the LLM supersession context
|
|
384
|
+
const existingItems = queryExistingItemsForContext(scopeId, text);
|
|
385
|
+
const systemPrompt = buildExtractionSystemPrompt(
|
|
386
|
+
existingItems,
|
|
387
|
+
messageRole,
|
|
388
|
+
userPersona,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const assistantName = getAssistantName() ?? "the assistant";
|
|
392
|
+
const messagePrefix =
|
|
393
|
+
messageRole === "assistant"
|
|
394
|
+
? `[This message is from ${assistantName}]\n\n`
|
|
395
|
+
: `[This message is from the user]\n\n`;
|
|
396
|
+
|
|
397
|
+
// Build user content with optional preceding conversation context
|
|
398
|
+
const contextParts: string[] = [];
|
|
399
|
+
for (const msg of precedingMessages) {
|
|
400
|
+
const msgText = extractTextFromStoredMessageContent(msg.content);
|
|
401
|
+
if (msgText.length === 0) continue;
|
|
402
|
+
const roleLabel =
|
|
403
|
+
msg.role === "assistant"
|
|
404
|
+
? (getAssistantName() ?? "assistant")
|
|
405
|
+
: "user";
|
|
406
|
+
contextParts.push(`[${roleLabel}]: ${msgText}`);
|
|
407
|
+
}
|
|
408
|
+
let userContent = `${messagePrefix}${text}`;
|
|
409
|
+
if (contextParts.length > 0) {
|
|
410
|
+
userContent = `Preceding conversation context:\n${contextParts.join("\n\n")}\n\n---\n\nMessage to extract from:\n${messagePrefix}${text}`;
|
|
411
|
+
}
|
|
344
412
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
},
|
|
393
|
-
overrideConfidence: {
|
|
394
|
-
type: "string",
|
|
395
|
-
enum: ["explicit", "tentative", "inferred"],
|
|
396
|
-
description:
|
|
397
|
-
"How confident you are that this overrides an existing item: explicit (clear override), tentative (ambiguous), inferred (weak signal)",
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
|
-
required: [
|
|
401
|
-
"kind",
|
|
402
|
-
"subject",
|
|
403
|
-
"statement",
|
|
404
|
-
"confidence",
|
|
405
|
-
"importance",
|
|
406
|
-
"supersedes",
|
|
407
|
-
"overrideConfidence",
|
|
408
|
-
],
|
|
413
|
+
const response = await provider.sendMessage(
|
|
414
|
+
[userMessage(userContent)],
|
|
415
|
+
[
|
|
416
|
+
{
|
|
417
|
+
name: "store_memory_items",
|
|
418
|
+
description: "Store extracted memory items from the message",
|
|
419
|
+
input_schema: {
|
|
420
|
+
type: "object" as const,
|
|
421
|
+
properties: {
|
|
422
|
+
items: {
|
|
423
|
+
type: "array",
|
|
424
|
+
items: {
|
|
425
|
+
type: "object",
|
|
426
|
+
properties: {
|
|
427
|
+
kind: {
|
|
428
|
+
type: "string",
|
|
429
|
+
enum: EXTRACTION_KINDS,
|
|
430
|
+
description: "Category of memory item",
|
|
431
|
+
},
|
|
432
|
+
subject: {
|
|
433
|
+
type: "string",
|
|
434
|
+
description:
|
|
435
|
+
"Short label (2-8 words) for what this is about",
|
|
436
|
+
},
|
|
437
|
+
statement: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description:
|
|
440
|
+
"Relationship-rich factual statement to remember (1-2 sentences). Include relational context.",
|
|
441
|
+
},
|
|
442
|
+
confidence: {
|
|
443
|
+
type: "number",
|
|
444
|
+
description: "Confidence that this is accurate (0.0-1.0)",
|
|
445
|
+
},
|
|
446
|
+
importance: {
|
|
447
|
+
type: "number",
|
|
448
|
+
description: "How valuable this is to remember (0.0-1.0)",
|
|
449
|
+
},
|
|
450
|
+
supersedes: {
|
|
451
|
+
type: ["string", "null"],
|
|
452
|
+
description:
|
|
453
|
+
"ID of the existing memory item this replaces, or null if not replacing anything",
|
|
454
|
+
},
|
|
455
|
+
overrideConfidence: {
|
|
456
|
+
type: "string",
|
|
457
|
+
enum: ["explicit", "tentative", "inferred"],
|
|
458
|
+
description:
|
|
459
|
+
"How confident you are that this overrides an existing item: explicit (clear override), tentative (ambiguous), inferred (weak signal)",
|
|
409
460
|
},
|
|
410
461
|
},
|
|
462
|
+
required: [
|
|
463
|
+
"kind",
|
|
464
|
+
"subject",
|
|
465
|
+
"statement",
|
|
466
|
+
"confidence",
|
|
467
|
+
"importance",
|
|
468
|
+
"supersedes",
|
|
469
|
+
"overrideConfidence",
|
|
470
|
+
],
|
|
411
471
|
},
|
|
412
|
-
required: ["items"],
|
|
413
472
|
},
|
|
414
473
|
},
|
|
415
|
-
|
|
416
|
-
systemPrompt,
|
|
417
|
-
{
|
|
418
|
-
config: {
|
|
419
|
-
modelIntent: extractionConfig.modelIntent,
|
|
420
|
-
max_tokens: 1024,
|
|
421
|
-
tool_choice: { type: "tool" as const, name: "store_memory_items" },
|
|
422
|
-
},
|
|
423
|
-
signal,
|
|
474
|
+
required: ["items"],
|
|
424
475
|
},
|
|
425
|
-
|
|
426
|
-
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
systemPrompt,
|
|
479
|
+
{
|
|
480
|
+
config: {
|
|
481
|
+
modelIntent: extractionConfig.modelIntent,
|
|
482
|
+
tool_choice: { type: "tool" as const, name: "store_memory_items" },
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
);
|
|
427
486
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
433
|
-
return extractItemsPatternBased(text, scopeId);
|
|
434
|
-
}
|
|
487
|
+
const toolBlock = extractToolUse(response);
|
|
488
|
+
if (!toolBlock) {
|
|
489
|
+
throw new Error("No tool_use block in LLM extraction response");
|
|
490
|
+
}
|
|
435
491
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
441
|
-
return extractItemsPatternBased(text, scopeId);
|
|
442
|
-
}
|
|
492
|
+
const input = toolBlock.input as { items?: LLMExtractedItem[] };
|
|
493
|
+
if (!Array.isArray(input.items)) {
|
|
494
|
+
throw new Error("Invalid items structure in LLM extraction response");
|
|
495
|
+
}
|
|
443
496
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const items: ExtractedItem[] = [];
|
|
448
|
-
for (const raw of input.items) {
|
|
449
|
-
// Apply kind migration map for old kind names, then validate
|
|
450
|
-
const resolvedKind = KIND_MIGRATION_MAP[raw.kind] ?? raw.kind;
|
|
451
|
-
if (!VALID_KINDS.has(resolvedKind)) continue;
|
|
452
|
-
if (!raw.subject || !raw.statement) continue;
|
|
453
|
-
const subject = truncate(String(raw.subject), 80, "");
|
|
454
|
-
const statement = truncate(String(raw.statement), 500, "");
|
|
455
|
-
const confidence = clampUnitInterval(parseScore(raw.confidence, 0.5));
|
|
456
|
-
const importance = clampUnitInterval(parseScore(raw.importance, 0.5));
|
|
457
|
-
const fingerprint = computeMemoryFingerprint(
|
|
458
|
-
scopeId,
|
|
459
|
-
resolvedKind,
|
|
460
|
-
subject,
|
|
461
|
-
statement,
|
|
462
|
-
);
|
|
497
|
+
// Build set of known existing item IDs for supersession validation
|
|
498
|
+
const existingItemIds = new Set(existingItems.map((e) => e.id));
|
|
463
499
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
items.push({
|
|
482
|
-
kind: resolvedKind as MemoryItemKind,
|
|
483
|
-
subject,
|
|
484
|
-
statement,
|
|
485
|
-
confidence,
|
|
486
|
-
importance,
|
|
487
|
-
fingerprint,
|
|
488
|
-
supersedes,
|
|
489
|
-
overrideConfidence,
|
|
490
|
-
supersedesRejected,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
500
|
+
const items: ExtractedItem[] = [];
|
|
501
|
+
for (const raw of input.items) {
|
|
502
|
+
// Apply kind migration map for old kind names, then validate
|
|
503
|
+
const resolvedKind = KIND_MIGRATION_MAP[raw.kind] ?? raw.kind;
|
|
504
|
+
if (resolvedKind === "journal") continue; // journal memories created directly from disk
|
|
505
|
+
if (!VALID_KINDS.has(resolvedKind)) continue;
|
|
506
|
+
if (!raw.subject || !raw.statement) continue;
|
|
507
|
+
const subject = String(raw.subject).trim();
|
|
508
|
+
const statement = String(raw.statement).trim();
|
|
509
|
+
const confidence = clampUnitInterval(parseScore(raw.confidence, 0.5));
|
|
510
|
+
const importance = clampUnitInterval(parseScore(raw.importance, 0.5));
|
|
511
|
+
const fingerprint = computeMemoryFingerprint(
|
|
512
|
+
scopeId,
|
|
513
|
+
resolvedKind,
|
|
514
|
+
subject,
|
|
515
|
+
statement,
|
|
516
|
+
);
|
|
493
517
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
518
|
+
// Validate supersedes: must reference a known existing item ID.
|
|
519
|
+
// Reject hallucinated IDs that don't match any item we showed the LLM.
|
|
520
|
+
const rawSupersedes =
|
|
521
|
+
typeof raw.supersedes === "string" && raw.supersedes.length > 0
|
|
522
|
+
? raw.supersedes
|
|
523
|
+
: null;
|
|
524
|
+
const supersedes =
|
|
525
|
+
rawSupersedes && existingItemIds.has(rawSupersedes)
|
|
526
|
+
? rawSupersedes
|
|
527
|
+
: null;
|
|
528
|
+
const supersedesRejected = !!rawSupersedes && !supersedes;
|
|
529
|
+
const overrideConfidence = VALID_OVERRIDE_CONFIDENCES.has(
|
|
530
|
+
raw.overrideConfidence,
|
|
531
|
+
)
|
|
532
|
+
? (raw.overrideConfidence as OverrideConfidence)
|
|
533
|
+
: "inferred";
|
|
534
|
+
|
|
535
|
+
items.push({
|
|
536
|
+
kind: resolvedKind as MemoryItemKind,
|
|
537
|
+
subject,
|
|
538
|
+
statement,
|
|
539
|
+
confidence,
|
|
540
|
+
importance,
|
|
541
|
+
fingerprint,
|
|
542
|
+
supersedes,
|
|
543
|
+
overrideConfidence,
|
|
544
|
+
supersedesRejected,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return deduplicateItems(items);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Fire conversation starters generation when journal memories were created.
|
|
553
|
+
* Wrapped in try/catch so failures never propagate to the caller.
|
|
554
|
+
*/
|
|
555
|
+
function triggerConversationStartersIfNeeded(
|
|
556
|
+
count: number,
|
|
557
|
+
scopeId: string,
|
|
558
|
+
): void {
|
|
559
|
+
if (count <= 0) return;
|
|
560
|
+
try {
|
|
561
|
+
maybeEnqueueConversationStartersJob(scopeId);
|
|
498
562
|
} catch (err) {
|
|
499
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
500
563
|
log.warn(
|
|
501
|
-
{ err: message },
|
|
502
|
-
"
|
|
564
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
565
|
+
"Failed to check conversation starters cadence",
|
|
503
566
|
);
|
|
504
|
-
return extractItemsPatternBased(text, scopeId);
|
|
505
567
|
}
|
|
506
568
|
}
|
|
507
569
|
|
|
@@ -519,6 +581,7 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
519
581
|
role: messages.role,
|
|
520
582
|
content: messages.content,
|
|
521
583
|
createdAt: messages.createdAt,
|
|
584
|
+
conversationId: messages.conversationId,
|
|
522
585
|
})
|
|
523
586
|
.from(messages)
|
|
524
587
|
.where(eq(messages.id, messageId))
|
|
@@ -526,18 +589,52 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
526
589
|
|
|
527
590
|
if (!message) return 0;
|
|
528
591
|
|
|
592
|
+
// Fetch up to 6 preceding messages from the same conversation for
|
|
593
|
+
// disambiguation context (e.g. resolving "that framework" or "yes, do it").
|
|
594
|
+
const effectiveConversationId = conversationId ?? message.conversationId;
|
|
595
|
+
const precedingMessages = effectiveConversationId
|
|
596
|
+
? db
|
|
597
|
+
.select({ role: messages.role, content: messages.content })
|
|
598
|
+
.from(messages)
|
|
599
|
+
.where(
|
|
600
|
+
and(
|
|
601
|
+
eq(messages.conversationId, effectiveConversationId),
|
|
602
|
+
sql`${messages.createdAt} < ${message.createdAt}`,
|
|
603
|
+
),
|
|
604
|
+
)
|
|
605
|
+
.orderBy(desc(messages.createdAt))
|
|
606
|
+
.limit(6)
|
|
607
|
+
.all()
|
|
608
|
+
.reverse()
|
|
609
|
+
: [];
|
|
610
|
+
|
|
611
|
+
const effectiveScopeId = scopeId ?? "default";
|
|
612
|
+
|
|
613
|
+
// Directly create journal memories from any journal files written during
|
|
614
|
+
// this message, bypassing LLM extraction (which would summarize/rewrite them).
|
|
615
|
+
// This must run before the extraction guards (semantic density, useLLM, etc.)
|
|
616
|
+
// because journal disk scanning is independent of LLM extraction.
|
|
617
|
+
let journalUpserted = 0;
|
|
618
|
+
if (message.role === "assistant") {
|
|
619
|
+
journalUpserted = upsertJournalMemoriesFromDisk(
|
|
620
|
+
message.createdAt,
|
|
621
|
+
effectiveScopeId,
|
|
622
|
+
messageId,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
529
626
|
const text = extractTextFromStoredMessageContent(message.content);
|
|
530
627
|
if (!hasSemanticDensity(text)) {
|
|
531
628
|
log.debug(
|
|
532
629
|
{ messageId },
|
|
533
630
|
"Skipping extraction — message lacks semantic density",
|
|
534
631
|
);
|
|
535
|
-
|
|
632
|
+
triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
|
|
633
|
+
return journalUpserted;
|
|
536
634
|
}
|
|
537
635
|
|
|
538
636
|
const config = getConfig();
|
|
539
637
|
const extractionConfig = config.memory.extraction;
|
|
540
|
-
const effectiveScopeId = scopeId ?? "default";
|
|
541
638
|
|
|
542
639
|
// Resolve the guardian's persona to provide personality-aware extraction
|
|
543
640
|
// context. Currently uses the guardian persona for all conversations —
|
|
@@ -545,17 +642,24 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
545
642
|
// better extraction context than none.
|
|
546
643
|
const userPersona = resolveGuardianPersona();
|
|
547
644
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
effectiveScopeId,
|
|
553
|
-
message.role,
|
|
554
|
-
userPersona,
|
|
555
|
-
)
|
|
556
|
-
: extractItemsPatternBased(text, effectiveScopeId);
|
|
645
|
+
if (!extractionConfig.useLLM) {
|
|
646
|
+
triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
|
|
647
|
+
return journalUpserted;
|
|
648
|
+
}
|
|
557
649
|
|
|
558
|
-
|
|
650
|
+
const extracted = await extractItemsWithLLM(
|
|
651
|
+
text,
|
|
652
|
+
extractionConfig,
|
|
653
|
+
effectiveScopeId,
|
|
654
|
+
message.role,
|
|
655
|
+
precedingMessages,
|
|
656
|
+
userPersona,
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
if (extracted.length === 0) {
|
|
660
|
+
triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
|
|
661
|
+
return journalUpserted;
|
|
662
|
+
}
|
|
559
663
|
|
|
560
664
|
// Guard: re-check after the async LLM call. The event loop yields during
|
|
561
665
|
// extractItemsWithLLM, so another task could have marked the conversation
|
|
@@ -565,13 +669,10 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
565
669
|
{ messageId, conversationId },
|
|
566
670
|
"Skipping upsert — conversation marked failed during extraction",
|
|
567
671
|
);
|
|
568
|
-
|
|
672
|
+
triggerConversationStartersIfNeeded(journalUpserted, effectiveScopeId);
|
|
673
|
+
return journalUpserted;
|
|
569
674
|
}
|
|
570
675
|
|
|
571
|
-
// Determine verification state from message role
|
|
572
|
-
const verificationState =
|
|
573
|
-
message.role === "user" ? "user_reported" : "assistant_inferred";
|
|
574
|
-
|
|
575
676
|
let upserted = 0;
|
|
576
677
|
for (const item of extracted) {
|
|
577
678
|
const now = Date.now();
|
|
@@ -591,13 +692,21 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
591
692
|
let effectiveStatus: string = "active";
|
|
592
693
|
if (existing) {
|
|
593
694
|
memoryItemId = existing.id;
|
|
594
|
-
// Promote verification state if re-seen from a more trusted source
|
|
595
|
-
const promotedState =
|
|
596
|
-
existing.verificationState === "assistant_inferred" &&
|
|
597
|
-
verificationState === "user_reported"
|
|
598
|
-
? "user_reported"
|
|
599
|
-
: existing.verificationState;
|
|
600
695
|
effectiveStatus = "active";
|
|
696
|
+
// Preserve sourceType for tool-sourced items — extraction should not
|
|
697
|
+
// demote items the user explicitly saved.
|
|
698
|
+
const effectiveSourceType =
|
|
699
|
+
existing.sourceType === "tool" ? "tool" : "extraction";
|
|
700
|
+
|
|
701
|
+
// Dual-write verificationState alongside sourceType for client compat.
|
|
702
|
+
// Promote from assistant_inferred → user_reported when re-seen from user.
|
|
703
|
+
const effectiveVerificationState =
|
|
704
|
+
message.role === "user" || existing.verificationState === "user_reported"
|
|
705
|
+
? "user_reported"
|
|
706
|
+
: existing.verificationState === "user_confirmed"
|
|
707
|
+
? "user_confirmed"
|
|
708
|
+
: "assistant_inferred";
|
|
709
|
+
|
|
601
710
|
db.update(memoryItems)
|
|
602
711
|
.set({
|
|
603
712
|
status: effectiveStatus,
|
|
@@ -608,7 +717,9 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
608
717
|
Math.max(existing.importance ?? 0, item.importance),
|
|
609
718
|
),
|
|
610
719
|
lastSeenAt: Math.max(existing.lastSeenAt, seenAt),
|
|
611
|
-
|
|
720
|
+
sourceType: effectiveSourceType,
|
|
721
|
+
sourceMessageRole: message.role,
|
|
722
|
+
verificationState: effectiveVerificationState,
|
|
612
723
|
})
|
|
613
724
|
.where(eq(memoryItems.id, existing.id))
|
|
614
725
|
.run();
|
|
@@ -624,7 +735,11 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
624
735
|
confidence: item.confidence,
|
|
625
736
|
importance: item.importance,
|
|
626
737
|
fingerprint: item.fingerprint,
|
|
627
|
-
|
|
738
|
+
sourceType: "extraction",
|
|
739
|
+
sourceMessageRole: message.role,
|
|
740
|
+
// Dual-write verificationState for client compat
|
|
741
|
+
verificationState:
|
|
742
|
+
message.role === "user" ? "user_reported" : "assistant_inferred",
|
|
628
743
|
scopeId: effectiveScopeId,
|
|
629
744
|
firstSeenAt: message.createdAt,
|
|
630
745
|
lastSeenAt: seenAt,
|
|
@@ -715,11 +830,9 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
715
830
|
}
|
|
716
831
|
|
|
717
832
|
// Fallback subject-match supersession: only when the LLM did not
|
|
718
|
-
// explicitly handle supersession for this item.
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
// rejected (hallucinated) — they should coexist, not trigger
|
|
722
|
-
// subject-based replacement.
|
|
833
|
+
// explicitly handle supersession for this item. Skip items whose
|
|
834
|
+
// supersedes ID was rejected (hallucinated) — they should coexist,
|
|
835
|
+
// not trigger subject-based replacement.
|
|
723
836
|
if (
|
|
724
837
|
!item.supersedes &&
|
|
725
838
|
!item.supersedesRejected &&
|
|
@@ -744,7 +857,7 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
744
857
|
.values({
|
|
745
858
|
memoryItemId,
|
|
746
859
|
messageId,
|
|
747
|
-
evidence:
|
|
860
|
+
evidence: item.statement,
|
|
748
861
|
createdAt: now,
|
|
749
862
|
})
|
|
750
863
|
.onConflictDoNothing()
|
|
@@ -753,139 +866,19 @@ export async function extractAndUpsertMemoryItemsForMessage(
|
|
|
753
866
|
enqueueMemoryJob("embed_item", { itemId: memoryItemId });
|
|
754
867
|
}
|
|
755
868
|
|
|
869
|
+
upserted += journalUpserted;
|
|
870
|
+
|
|
756
871
|
log.debug(
|
|
757
872
|
{ messageId, extracted: extracted.length, upserted },
|
|
758
873
|
"Extracted memory items from message",
|
|
759
874
|
);
|
|
760
875
|
|
|
761
876
|
// Trigger conversation starters generation when new items are upserted
|
|
762
|
-
|
|
763
|
-
try {
|
|
764
|
-
maybeEnqueueConversationStartersJob(effectiveScopeId);
|
|
765
|
-
} catch (err) {
|
|
766
|
-
log.warn(
|
|
767
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
768
|
-
"Failed to check conversation starters cadence",
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
877
|
+
triggerConversationStartersIfNeeded(upserted, effectiveScopeId);
|
|
772
878
|
|
|
773
879
|
return upserted;
|
|
774
880
|
}
|
|
775
881
|
|
|
776
|
-
// ── Pattern-based extraction (fallback) ────────────────────────────────
|
|
777
|
-
|
|
778
|
-
function extractItemsPatternBased(
|
|
779
|
-
text: string,
|
|
780
|
-
scopeId: string = "default",
|
|
781
|
-
): ExtractedItem[] {
|
|
782
|
-
const sentences = text
|
|
783
|
-
.split(/[\n\r]+|(?<=[.!?])\s+/)
|
|
784
|
-
.map((s) => s.trim())
|
|
785
|
-
.filter((s) => s.length >= 20 && s.length <= 500);
|
|
786
|
-
|
|
787
|
-
const items: ExtractedItem[] = [];
|
|
788
|
-
for (const sentence of sentences) {
|
|
789
|
-
const lower = sentence.toLowerCase();
|
|
790
|
-
const classification = classifySentence(lower);
|
|
791
|
-
if (!classification) continue;
|
|
792
|
-
const subject = inferSubject(sentence, classification.kind);
|
|
793
|
-
const statement = sentence.replace(/\s+/g, " ").trim();
|
|
794
|
-
const fingerprint = computeMemoryFingerprint(
|
|
795
|
-
scopeId,
|
|
796
|
-
classification.kind,
|
|
797
|
-
subject,
|
|
798
|
-
statement,
|
|
799
|
-
);
|
|
800
|
-
items.push({
|
|
801
|
-
kind: classification.kind,
|
|
802
|
-
subject,
|
|
803
|
-
statement,
|
|
804
|
-
confidence: classification.confidence,
|
|
805
|
-
importance: classification.importance,
|
|
806
|
-
fingerprint,
|
|
807
|
-
supersedes: null,
|
|
808
|
-
overrideConfidence: "inferred" as OverrideConfidence,
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
return deduplicateItems(items);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function classifySentence(
|
|
816
|
-
lower: string,
|
|
817
|
-
): { kind: MemoryItemKind; confidence: number; importance: number } | null {
|
|
818
|
-
if (
|
|
819
|
-
includesAny(lower, [
|
|
820
|
-
"i prefer",
|
|
821
|
-
"prefer to",
|
|
822
|
-
"favorite",
|
|
823
|
-
"i like",
|
|
824
|
-
"i dislike",
|
|
825
|
-
])
|
|
826
|
-
) {
|
|
827
|
-
return { kind: "preference", confidence: 0.78, importance: 0.7 };
|
|
828
|
-
}
|
|
829
|
-
if (
|
|
830
|
-
includesAny(lower, [
|
|
831
|
-
"my name is",
|
|
832
|
-
"i am ",
|
|
833
|
-
"i work as",
|
|
834
|
-
"i live in",
|
|
835
|
-
"timezone",
|
|
836
|
-
])
|
|
837
|
-
) {
|
|
838
|
-
return { kind: "identity", confidence: 0.72, importance: 0.8 };
|
|
839
|
-
}
|
|
840
|
-
if (includesAny(lower, ["project", "repository", "repo", "codebase"])) {
|
|
841
|
-
return { kind: "project", confidence: 0.68, importance: 0.6 };
|
|
842
|
-
}
|
|
843
|
-
if (
|
|
844
|
-
includesAny(lower, ["we decided", "decision", "chosen approach", "we will"])
|
|
845
|
-
) {
|
|
846
|
-
return { kind: "decision", confidence: 0.75, importance: 0.7 };
|
|
847
|
-
}
|
|
848
|
-
if (
|
|
849
|
-
includesAny(lower, ["todo", "to do", "next step", "follow up", "need to"])
|
|
850
|
-
) {
|
|
851
|
-
return { kind: "project", confidence: 0.74, importance: 0.6 };
|
|
852
|
-
}
|
|
853
|
-
if (
|
|
854
|
-
includesAny(lower, [
|
|
855
|
-
"must",
|
|
856
|
-
"cannot",
|
|
857
|
-
"should not",
|
|
858
|
-
"constraint",
|
|
859
|
-
"requirement",
|
|
860
|
-
])
|
|
861
|
-
) {
|
|
862
|
-
return { kind: "constraint", confidence: 0.7, importance: 0.7 };
|
|
863
|
-
}
|
|
864
|
-
if (includesAny(lower, ["remember", "important", "fact", "noted"])) {
|
|
865
|
-
return { kind: "identity", confidence: 0.62, importance: 0.5 };
|
|
866
|
-
}
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function inferSubject(sentence: string, kind: MemoryItemKind): string {
|
|
871
|
-
const trimmed = sentence.trim();
|
|
872
|
-
if (kind === "project") {
|
|
873
|
-
const match = trimmed.match(
|
|
874
|
-
/(?:project|repo(?:sitory)?)\s+([A-Za-z0-9._/-]{2,80})/i,
|
|
875
|
-
);
|
|
876
|
-
if (match) return match[1];
|
|
877
|
-
}
|
|
878
|
-
const words = trimmed.split(/\s+/).slice(0, 6).join(" ");
|
|
879
|
-
return truncate(words, 80, "");
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function includesAny(text: string, needles: string[]): boolean {
|
|
883
|
-
for (const needle of needles) {
|
|
884
|
-
if (text.includes(needle)) return true;
|
|
885
|
-
}
|
|
886
|
-
return false;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
882
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
890
883
|
|
|
891
884
|
function deduplicateItems(items: ExtractedItem[]): ExtractedItem[] {
|