@vellumai/assistant 0.4.49 → 0.4.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/docs/architecture/memory.md +180 -119
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +23 -18
- package/src/__tests__/credential-vault.test.ts +30 -18
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +166 -13
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +824 -31
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +363 -17
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +55 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +78 -18
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +85 -5
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +134 -3
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +44 -6
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +5 -4
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +11 -8
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +68 -10
- package/src/cli/commands/oauth/connections.ts +475 -105
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +18 -4
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +20 -22
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/loader.ts +6 -42
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +55 -2
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +39 -4
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -42
- package/src/daemon/server.ts +6 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-slash.ts +3 -5
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/email/providers/index.ts +2 -2
- package/src/instrument.ts +61 -1
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +25 -83
- package/src/memory/db-init.ts +32 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +40 -25
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +183 -31
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +214 -8
- package/src/oauth/token-persistence.ts +31 -16
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +72 -12
- package/src/runtime/invite-service.ts +43 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +56 -11
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +9 -4
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +44 -19
- package/src/security/token-manager.ts +46 -39
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +22 -7
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +10 -3
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { getLogger } from "../util/logger.js";
|
|
2
|
-
import { rawGet, rawRun } from "./db.js";
|
|
3
|
-
|
|
4
|
-
const log = getLogger("fts-reconciler");
|
|
5
|
-
|
|
6
|
-
export interface FtsReconciliationResult {
|
|
7
|
-
table: string;
|
|
8
|
-
baseCount: number;
|
|
9
|
-
ftsCount: number;
|
|
10
|
-
missingInserted: number;
|
|
11
|
-
orphansRemoved: number;
|
|
12
|
-
staleRefreshed: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Reconcile a single FTS index against its base table. Detects missing entries
|
|
17
|
-
* (rows in the base table with no corresponding FTS row) and orphaned entries
|
|
18
|
-
* (FTS rows whose base table row no longer exists), then repairs both.
|
|
19
|
-
*
|
|
20
|
-
* This is lighter than a full rebuild — it only touches the delta rather than
|
|
21
|
-
* wiping and re-inserting the entire index.
|
|
22
|
-
*/
|
|
23
|
-
function reconcileTable(opts: {
|
|
24
|
-
ftsTable: string;
|
|
25
|
-
ftsIdColumn: string;
|
|
26
|
-
ftsContentColumn: string;
|
|
27
|
-
baseTable: string;
|
|
28
|
-
baseIdColumn: string;
|
|
29
|
-
baseContentColumn: string;
|
|
30
|
-
}): FtsReconciliationResult {
|
|
31
|
-
const {
|
|
32
|
-
ftsTable,
|
|
33
|
-
ftsIdColumn,
|
|
34
|
-
ftsContentColumn,
|
|
35
|
-
baseTable,
|
|
36
|
-
baseIdColumn,
|
|
37
|
-
baseContentColumn,
|
|
38
|
-
} = opts;
|
|
39
|
-
|
|
40
|
-
const baseCount = (
|
|
41
|
-
rawGet<{ c: number }>(`SELECT COUNT(*) AS c FROM ${baseTable}`) ?? { c: 0 }
|
|
42
|
-
).c;
|
|
43
|
-
const ftsCount = (
|
|
44
|
-
rawGet<{ c: number }>(`SELECT COUNT(*) AS c FROM ${ftsTable}`) ?? { c: 0 }
|
|
45
|
-
).c;
|
|
46
|
-
|
|
47
|
-
// Find base table rows missing from the FTS index
|
|
48
|
-
const missingInserted = rawRun(/*sql*/ `
|
|
49
|
-
INSERT INTO ${ftsTable}(${ftsIdColumn}, ${ftsContentColumn})
|
|
50
|
-
SELECT b.${baseIdColumn}, b.${baseContentColumn}
|
|
51
|
-
FROM ${baseTable} b
|
|
52
|
-
LEFT JOIN ${ftsTable} f ON f.${ftsIdColumn} = b.${baseIdColumn}
|
|
53
|
-
WHERE f.${ftsIdColumn} IS NULL
|
|
54
|
-
`);
|
|
55
|
-
|
|
56
|
-
// Find FTS rows whose base table row no longer exists
|
|
57
|
-
const orphansRemoved = rawRun(/*sql*/ `
|
|
58
|
-
DELETE FROM ${ftsTable}
|
|
59
|
-
WHERE ${ftsIdColumn} IN (
|
|
60
|
-
SELECT f.${ftsIdColumn}
|
|
61
|
-
FROM ${ftsTable} f
|
|
62
|
-
LEFT JOIN ${baseTable} b ON b.${baseIdColumn} = f.${ftsIdColumn}
|
|
63
|
-
WHERE b.${baseIdColumn} IS NULL
|
|
64
|
-
)
|
|
65
|
-
`);
|
|
66
|
-
|
|
67
|
-
// Refresh FTS rows whose content is stale (base row was updated but the
|
|
68
|
-
// update trigger didn't fire or was missing). Delete-then-insert is the
|
|
69
|
-
// standard FTS5 update pattern.
|
|
70
|
-
const staleDeleted = rawRun(/*sql*/ `
|
|
71
|
-
DELETE FROM ${ftsTable}
|
|
72
|
-
WHERE ${ftsIdColumn} IN (
|
|
73
|
-
SELECT f.${ftsIdColumn}
|
|
74
|
-
FROM ${ftsTable} f
|
|
75
|
-
JOIN ${baseTable} b ON b.${baseIdColumn} = f.${ftsIdColumn}
|
|
76
|
-
WHERE b.${baseContentColumn} IS NOT f.${ftsContentColumn}
|
|
77
|
-
)
|
|
78
|
-
`);
|
|
79
|
-
if (staleDeleted > 0) {
|
|
80
|
-
rawRun(/*sql*/ `
|
|
81
|
-
INSERT INTO ${ftsTable}(${ftsIdColumn}, ${ftsContentColumn})
|
|
82
|
-
SELECT b.${baseIdColumn}, b.${baseContentColumn}
|
|
83
|
-
FROM ${baseTable} b
|
|
84
|
-
LEFT JOIN ${ftsTable} f ON f.${ftsIdColumn} = b.${baseIdColumn}
|
|
85
|
-
WHERE f.${ftsIdColumn} IS NULL
|
|
86
|
-
`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
table: ftsTable,
|
|
91
|
-
baseCount,
|
|
92
|
-
ftsCount,
|
|
93
|
-
missingInserted,
|
|
94
|
-
orphansRemoved,
|
|
95
|
-
staleRefreshed: staleDeleted,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Reconcile all FTS indexes. Returns results for each table so callers can
|
|
101
|
-
* inspect what was repaired.
|
|
102
|
-
*/
|
|
103
|
-
export function reconcileFtsIndexes(): FtsReconciliationResult[] {
|
|
104
|
-
const results: FtsReconciliationResult[] = [];
|
|
105
|
-
const errors: unknown[] = [];
|
|
106
|
-
|
|
107
|
-
// memory_segment_fts tracks memory_segments
|
|
108
|
-
try {
|
|
109
|
-
const memResult = reconcileTable({
|
|
110
|
-
ftsTable: "memory_segment_fts",
|
|
111
|
-
ftsIdColumn: "segment_id",
|
|
112
|
-
ftsContentColumn: "text",
|
|
113
|
-
baseTable: "memory_segments",
|
|
114
|
-
baseIdColumn: "id",
|
|
115
|
-
baseContentColumn: "text",
|
|
116
|
-
});
|
|
117
|
-
results.push(memResult);
|
|
118
|
-
if (
|
|
119
|
-
memResult.missingInserted > 0 ||
|
|
120
|
-
memResult.orphansRemoved > 0 ||
|
|
121
|
-
memResult.staleRefreshed > 0
|
|
122
|
-
) {
|
|
123
|
-
log.info(memResult, "Reconciled memory_segment_fts");
|
|
124
|
-
} else {
|
|
125
|
-
log.debug(memResult, "memory_segment_fts is in sync");
|
|
126
|
-
}
|
|
127
|
-
} catch (err) {
|
|
128
|
-
log.error({ err }, "Failed to reconcile memory_segment_fts");
|
|
129
|
-
errors.push(err);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// messages_fts tracks messages
|
|
133
|
-
try {
|
|
134
|
-
const msgResult = reconcileTable({
|
|
135
|
-
ftsTable: "messages_fts",
|
|
136
|
-
ftsIdColumn: "message_id",
|
|
137
|
-
ftsContentColumn: "content",
|
|
138
|
-
baseTable: "messages",
|
|
139
|
-
baseIdColumn: "id",
|
|
140
|
-
baseContentColumn: "content",
|
|
141
|
-
});
|
|
142
|
-
results.push(msgResult);
|
|
143
|
-
if (
|
|
144
|
-
msgResult.missingInserted > 0 ||
|
|
145
|
-
msgResult.orphansRemoved > 0 ||
|
|
146
|
-
msgResult.staleRefreshed > 0
|
|
147
|
-
) {
|
|
148
|
-
log.info(msgResult, "Reconciled messages_fts");
|
|
149
|
-
} else {
|
|
150
|
-
log.debug(msgResult, "messages_fts is in sync");
|
|
151
|
-
}
|
|
152
|
-
} catch (err) {
|
|
153
|
-
log.error({ err }, "Failed to reconcile messages_fts");
|
|
154
|
-
errors.push(err);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (errors.length > 0) {
|
|
158
|
-
throw new AggregateError(
|
|
159
|
-
errors,
|
|
160
|
-
`FTS reconciliation failed for ${errors.length} table(s)`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return results;
|
|
165
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { and, asc, eq, inArray, lt, ne } from "drizzle-orm";
|
|
2
|
-
|
|
3
|
-
import type { AssistantConfig } from "../../config/types.js";
|
|
4
|
-
import { getLogger } from "../../util/logger.js";
|
|
5
|
-
import { resolveConflictClarification } from "../clarification-resolver.js";
|
|
6
|
-
import {
|
|
7
|
-
areStatementsCoherent,
|
|
8
|
-
computeConflictRelevance,
|
|
9
|
-
looksLikeClarificationReply,
|
|
10
|
-
shouldAttemptConflictResolution,
|
|
11
|
-
} from "../conflict-intent.js";
|
|
12
|
-
import {
|
|
13
|
-
isConflictKindPairEligible,
|
|
14
|
-
isConflictUserEvidenced,
|
|
15
|
-
isStatementConflictEligible,
|
|
16
|
-
} from "../conflict-policy.js";
|
|
17
|
-
import {
|
|
18
|
-
applyConflictResolution,
|
|
19
|
-
listPendingConflictDetails,
|
|
20
|
-
resolveConflict,
|
|
21
|
-
} from "../conflict-store.js";
|
|
22
|
-
import { getDb } from "../db.js";
|
|
23
|
-
import { asPositiveMs, asString } from "../job-utils.js";
|
|
24
|
-
import { enqueueMemoryJob, type MemoryJob } from "../jobs-store.js";
|
|
25
|
-
import { extractTextFromStoredMessageContent } from "../message-content.js";
|
|
26
|
-
import { memoryItemConflicts, messages } from "../schema.js";
|
|
27
|
-
|
|
28
|
-
const log = getLogger("memory-jobs-worker");
|
|
29
|
-
|
|
30
|
-
const CLEANUP_BATCH_LIMIT = 250;
|
|
31
|
-
|
|
32
|
-
export async function resolvePendingConflictsForMessageJob(
|
|
33
|
-
job: MemoryJob,
|
|
34
|
-
config: AssistantConfig,
|
|
35
|
-
): Promise<void> {
|
|
36
|
-
if (!config.memory.conflicts.enabled) return;
|
|
37
|
-
const messageId = asString(job.payload.messageId);
|
|
38
|
-
if (!messageId) return;
|
|
39
|
-
const scopeId = asString(job.payload.scopeId) ?? "default";
|
|
40
|
-
const db = getDb();
|
|
41
|
-
const message = db
|
|
42
|
-
.select({
|
|
43
|
-
id: messages.id,
|
|
44
|
-
role: messages.role,
|
|
45
|
-
content: messages.content,
|
|
46
|
-
createdAt: messages.createdAt,
|
|
47
|
-
})
|
|
48
|
-
.from(messages)
|
|
49
|
-
.where(eq(messages.id, messageId))
|
|
50
|
-
.get();
|
|
51
|
-
if (!message || message.role !== "user") return;
|
|
52
|
-
|
|
53
|
-
const userMessage = extractTextFromStoredMessageContent(
|
|
54
|
-
message.content,
|
|
55
|
-
).trim();
|
|
56
|
-
if (userMessage.length === 0) return;
|
|
57
|
-
const clarificationReply = looksLikeClarificationReply(userMessage);
|
|
58
|
-
if (!clarificationReply) return;
|
|
59
|
-
|
|
60
|
-
const pending = listPendingConflictDetails(scopeId, 25);
|
|
61
|
-
|
|
62
|
-
// Dismiss non-actionable conflicts (kind or statement policy)
|
|
63
|
-
const conflictableKinds = config.memory.conflicts.conflictableKinds;
|
|
64
|
-
for (const conflict of pending) {
|
|
65
|
-
const kindEligible = isConflictKindPairEligible(
|
|
66
|
-
conflict.existingKind,
|
|
67
|
-
conflict.candidateKind,
|
|
68
|
-
{ conflictableKinds },
|
|
69
|
-
);
|
|
70
|
-
if (
|
|
71
|
-
!kindEligible ||
|
|
72
|
-
!isStatementConflictEligible(
|
|
73
|
-
conflict.existingKind,
|
|
74
|
-
conflict.existingStatement,
|
|
75
|
-
{ conflictableKinds },
|
|
76
|
-
) ||
|
|
77
|
-
!isStatementConflictEligible(
|
|
78
|
-
conflict.candidateKind,
|
|
79
|
-
conflict.candidateStatement,
|
|
80
|
-
{ conflictableKinds },
|
|
81
|
-
)
|
|
82
|
-
) {
|
|
83
|
-
resolveConflict(conflict.id, {
|
|
84
|
-
status: "dismissed",
|
|
85
|
-
resolutionNote: "Dismissed by conflict policy (transient/non-durable).",
|
|
86
|
-
});
|
|
87
|
-
} else if (
|
|
88
|
-
!isConflictUserEvidenced(
|
|
89
|
-
conflict.existingVerificationState,
|
|
90
|
-
conflict.candidateVerificationState,
|
|
91
|
-
)
|
|
92
|
-
) {
|
|
93
|
-
resolveConflict(conflict.id, {
|
|
94
|
-
status: "dismissed",
|
|
95
|
-
resolutionNote:
|
|
96
|
-
"Dismissed by conflict policy (no user-evidenced provenance).",
|
|
97
|
-
});
|
|
98
|
-
} else if (
|
|
99
|
-
!areStatementsCoherent(
|
|
100
|
-
conflict.existingStatement,
|
|
101
|
-
conflict.candidateStatement,
|
|
102
|
-
)
|
|
103
|
-
) {
|
|
104
|
-
resolveConflict(conflict.id, {
|
|
105
|
-
status: "dismissed",
|
|
106
|
-
resolutionNote:
|
|
107
|
-
"Dismissed by conflict policy (incoherent — zero statement overlap).",
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Re-fetch after dismissal
|
|
113
|
-
const actionablePending = listPendingConflictDetails(scopeId, 25);
|
|
114
|
-
const eligible = actionablePending.filter(
|
|
115
|
-
(conflict) => conflict.createdAt <= message.createdAt,
|
|
116
|
-
);
|
|
117
|
-
if (eligible.length === 0) return;
|
|
118
|
-
const candidates = eligible.filter((conflict) => {
|
|
119
|
-
const relevance = computeConflictRelevance(userMessage, conflict);
|
|
120
|
-
return shouldAttemptConflictResolution({
|
|
121
|
-
clarificationReply,
|
|
122
|
-
relevance,
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
if (candidates.length === 0) return;
|
|
126
|
-
|
|
127
|
-
let resolvedCount = 0;
|
|
128
|
-
for (const conflict of candidates) {
|
|
129
|
-
const resolution = await resolveConflictClarification(
|
|
130
|
-
{
|
|
131
|
-
existingStatement: conflict.existingStatement,
|
|
132
|
-
candidateStatement: conflict.candidateStatement,
|
|
133
|
-
userMessage,
|
|
134
|
-
},
|
|
135
|
-
{ timeoutMs: config.memory.conflicts.resolverLlmTimeoutMs },
|
|
136
|
-
);
|
|
137
|
-
if (resolution.resolution === "still_unclear") continue;
|
|
138
|
-
const resolved = applyConflictResolution({
|
|
139
|
-
conflictId: conflict.id,
|
|
140
|
-
resolution: resolution.resolution,
|
|
141
|
-
mergedStatement:
|
|
142
|
-
resolution.resolution === "merge" ? resolution.resolvedStatement : null,
|
|
143
|
-
resolutionNote: `Background message resolver (${resolution.strategy}): ${resolution.explanation}`,
|
|
144
|
-
});
|
|
145
|
-
if (resolved) resolvedCount += 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
log.debug(
|
|
149
|
-
{
|
|
150
|
-
messageId,
|
|
151
|
-
scopeId,
|
|
152
|
-
pendingConflicts: pending.length,
|
|
153
|
-
eligibleConflicts: eligible.length,
|
|
154
|
-
candidateConflicts: candidates.length,
|
|
155
|
-
resolvedConflicts: resolvedCount,
|
|
156
|
-
},
|
|
157
|
-
"Processed pending conflict resolution job",
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function cleanupResolvedConflictsJob(
|
|
162
|
-
job: MemoryJob,
|
|
163
|
-
config: AssistantConfig,
|
|
164
|
-
): void {
|
|
165
|
-
const db = getDb();
|
|
166
|
-
const retentionMs =
|
|
167
|
-
asPositiveMs(job.payload.retentionMs) ??
|
|
168
|
-
config.memory.cleanup.resolvedConflictRetentionMs;
|
|
169
|
-
const cutoff = Date.now() - retentionMs;
|
|
170
|
-
const stale = db
|
|
171
|
-
.select({ id: memoryItemConflicts.id })
|
|
172
|
-
.from(memoryItemConflicts)
|
|
173
|
-
.where(
|
|
174
|
-
and(
|
|
175
|
-
ne(memoryItemConflicts.status, "pending_clarification"),
|
|
176
|
-
lt(memoryItemConflicts.resolvedAt, cutoff),
|
|
177
|
-
),
|
|
178
|
-
)
|
|
179
|
-
.orderBy(asc(memoryItemConflicts.resolvedAt), asc(memoryItemConflicts.id))
|
|
180
|
-
.limit(CLEANUP_BATCH_LIMIT)
|
|
181
|
-
.all();
|
|
182
|
-
if (stale.length === 0) return;
|
|
183
|
-
|
|
184
|
-
const ids = stale.map((row) => row.id);
|
|
185
|
-
db.delete(memoryItemConflicts)
|
|
186
|
-
.where(inArray(memoryItemConflicts.id, ids))
|
|
187
|
-
.run();
|
|
188
|
-
if (stale.length === CLEANUP_BATCH_LIMIT) {
|
|
189
|
-
enqueueMemoryJob("cleanup_resolved_conflicts", { retentionMs });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
log.debug(
|
|
193
|
-
{
|
|
194
|
-
removedConflicts: stale.length,
|
|
195
|
-
retentionMs,
|
|
196
|
-
cutoff,
|
|
197
|
-
},
|
|
198
|
-
"Cleaned up resolved memory conflicts",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
|
|
2
|
-
|
|
3
|
-
import { getConfig } from "../config/loader.js";
|
|
4
|
-
import { estimateTextTokens } from "../context/token-estimator.js";
|
|
5
|
-
import { getDb } from "./db.js";
|
|
6
|
-
import { memoryItems } from "./schema.js";
|
|
7
|
-
|
|
8
|
-
const PROFILE_KIND_ALLOWLIST = [
|
|
9
|
-
"profile",
|
|
10
|
-
"preference",
|
|
11
|
-
"constraint",
|
|
12
|
-
"instruction",
|
|
13
|
-
"style",
|
|
14
|
-
] as const;
|
|
15
|
-
|
|
16
|
-
const TRUST_RANK: Record<string, number> = {
|
|
17
|
-
user_confirmed: 3,
|
|
18
|
-
user_reported: 2,
|
|
19
|
-
assistant_inferred: 1,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export interface CompileProfileOptions {
|
|
23
|
-
scopeId?: string;
|
|
24
|
-
maxInjectTokensOverride?: number;
|
|
25
|
-
/** When true and scopeId is not 'default', query both the given scope and 'default'. */
|
|
26
|
-
includeDefaultFallback?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface CompiledProfile {
|
|
30
|
-
text: string;
|
|
31
|
-
sourceCount: number;
|
|
32
|
-
selectedCount: number;
|
|
33
|
-
budgetTokens: number;
|
|
34
|
-
tokenEstimate: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ProfileCandidate {
|
|
38
|
-
kind: string;
|
|
39
|
-
subject: string;
|
|
40
|
-
statement: string;
|
|
41
|
-
verificationState: string;
|
|
42
|
-
confidence: number;
|
|
43
|
-
importance: number | null;
|
|
44
|
-
lastSeenAt: number;
|
|
45
|
-
firstSeenAt: number;
|
|
46
|
-
scopeId: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function compileDynamicProfile(
|
|
50
|
-
options?: CompileProfileOptions,
|
|
51
|
-
): CompiledProfile {
|
|
52
|
-
const config = getConfig();
|
|
53
|
-
const profileConfig = config.memory.profile;
|
|
54
|
-
const scopeId = options?.scopeId ?? "default";
|
|
55
|
-
const budgetTokens = Math.max(
|
|
56
|
-
0,
|
|
57
|
-
Math.floor(
|
|
58
|
-
options?.maxInjectTokensOverride ?? profileConfig.maxInjectTokens,
|
|
59
|
-
),
|
|
60
|
-
);
|
|
61
|
-
if (!profileConfig.enabled || budgetTokens <= 0) {
|
|
62
|
-
return {
|
|
63
|
-
text: "",
|
|
64
|
-
sourceCount: 0,
|
|
65
|
-
selectedCount: 0,
|
|
66
|
-
budgetTokens,
|
|
67
|
-
tokenEstimate: 0,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const db = getDb();
|
|
72
|
-
const shouldFallback =
|
|
73
|
-
options?.includeDefaultFallback === true && scopeId !== "default";
|
|
74
|
-
const scopeFilter = shouldFallback
|
|
75
|
-
? inArray(memoryItems.scopeId, [scopeId, "default"])
|
|
76
|
-
: eq(memoryItems.scopeId, scopeId);
|
|
77
|
-
const rows = db
|
|
78
|
-
.select({
|
|
79
|
-
kind: memoryItems.kind,
|
|
80
|
-
subject: memoryItems.subject,
|
|
81
|
-
statement: memoryItems.statement,
|
|
82
|
-
verificationState: memoryItems.verificationState,
|
|
83
|
-
confidence: memoryItems.confidence,
|
|
84
|
-
importance: memoryItems.importance,
|
|
85
|
-
lastSeenAt: memoryItems.lastSeenAt,
|
|
86
|
-
firstSeenAt: memoryItems.firstSeenAt,
|
|
87
|
-
scopeId: memoryItems.scopeId,
|
|
88
|
-
})
|
|
89
|
-
.from(memoryItems)
|
|
90
|
-
.where(
|
|
91
|
-
and(
|
|
92
|
-
scopeFilter,
|
|
93
|
-
eq(memoryItems.status, "active"),
|
|
94
|
-
isNull(memoryItems.invalidAt),
|
|
95
|
-
inArray(memoryItems.kind, [...PROFILE_KIND_ALLOWLIST]),
|
|
96
|
-
),
|
|
97
|
-
)
|
|
98
|
-
.orderBy(desc(memoryItems.lastSeenAt))
|
|
99
|
-
.all();
|
|
100
|
-
|
|
101
|
-
const nowMs = Date.now();
|
|
102
|
-
const trusted = rows
|
|
103
|
-
.filter((row) => TRUST_RANK[row.verificationState] !== undefined)
|
|
104
|
-
.sort((a, b) =>
|
|
105
|
-
compareProfileCandidates(
|
|
106
|
-
a,
|
|
107
|
-
b,
|
|
108
|
-
nowMs,
|
|
109
|
-
shouldFallback ? scopeId : undefined,
|
|
110
|
-
),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const selectedLines: string[] = [];
|
|
114
|
-
const seenKeys = new Set<string>();
|
|
115
|
-
for (const candidate of trusted) {
|
|
116
|
-
const subject = normalizeWhitespace(candidate.subject, 80);
|
|
117
|
-
const statement = normalizeWhitespace(candidate.statement, 220);
|
|
118
|
-
if (!subject || !statement) continue;
|
|
119
|
-
const dedupeKey = `${candidate.kind}|${subject.toLowerCase()}`;
|
|
120
|
-
if (seenKeys.has(dedupeKey)) continue;
|
|
121
|
-
|
|
122
|
-
const line = `- ${subject}: ${statement}`;
|
|
123
|
-
const tentative = renderProfileText([...selectedLines, line]);
|
|
124
|
-
if (estimateTextTokens(tentative) > budgetTokens) continue;
|
|
125
|
-
seenKeys.add(dedupeKey);
|
|
126
|
-
selectedLines.push(line);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const text = renderProfileText(selectedLines);
|
|
130
|
-
const tokenEstimate = text ? estimateTextTokens(text) : 0;
|
|
131
|
-
return {
|
|
132
|
-
text,
|
|
133
|
-
sourceCount: trusted.length,
|
|
134
|
-
selectedCount: selectedLines.length,
|
|
135
|
-
budgetTokens,
|
|
136
|
-
tokenEstimate,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Half-life for recency decay — items seen this many ms ago score ~0.5. (30 days) */
|
|
141
|
-
const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
142
|
-
|
|
143
|
-
function recencyScore(lastSeenAt: number, nowMs: number): number {
|
|
144
|
-
const ageMs = Math.max(0, nowMs - lastSeenAt);
|
|
145
|
-
return Math.pow(0.5, ageMs / RECENCY_HALF_LIFE_MS);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function candidateRankScore(c: ProfileCandidate, nowMs: number): number {
|
|
149
|
-
const importance = c.importance ?? 0.5;
|
|
150
|
-
const recency = recencyScore(c.lastSeenAt, nowMs);
|
|
151
|
-
// Use weighted additive formula: importance dominates, recency is a minor boost
|
|
152
|
-
// This preserves high-importance items even when they haven't been seen recently
|
|
153
|
-
return importance * 0.7 + recency * 0.3;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function compareProfileCandidates(
|
|
157
|
-
left: ProfileCandidate,
|
|
158
|
-
right: ProfileCandidate,
|
|
159
|
-
nowMs: number,
|
|
160
|
-
/** When set, entries matching this scope sort before 'default' entries. */
|
|
161
|
-
preferredScopeId?: string,
|
|
162
|
-
): number {
|
|
163
|
-
// Scoped entries beat default fallback entries so local overrides win deduplication
|
|
164
|
-
if (preferredScopeId !== undefined) {
|
|
165
|
-
const leftIsPreferred = left.scopeId === preferredScopeId;
|
|
166
|
-
const rightIsPreferred = right.scopeId === preferredScopeId;
|
|
167
|
-
if (leftIsPreferred && !rightIsPreferred) return -1;
|
|
168
|
-
if (!leftIsPreferred && rightIsPreferred) return 1;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const trustDelta =
|
|
172
|
-
(TRUST_RANK[right.verificationState] ?? 0) -
|
|
173
|
-
(TRUST_RANK[left.verificationState] ?? 0);
|
|
174
|
-
if (trustDelta !== 0) return trustDelta;
|
|
175
|
-
|
|
176
|
-
const scoreDelta =
|
|
177
|
-
candidateRankScore(right, nowMs) - candidateRankScore(left, nowMs);
|
|
178
|
-
if (scoreDelta !== 0) return scoreDelta;
|
|
179
|
-
|
|
180
|
-
const confidenceDelta = right.confidence - left.confidence;
|
|
181
|
-
if (confidenceDelta !== 0) return confidenceDelta;
|
|
182
|
-
|
|
183
|
-
return right.firstSeenAt - left.firstSeenAt;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function normalizeWhitespace(input: string, maxLength: number): string {
|
|
187
|
-
return input.replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function renderProfileText(lines: string[]): string {
|
|
191
|
-
if (lines.length === 0) return "";
|
|
192
|
-
return ["<dynamic-user-profile>", ...lines, "</dynamic-user-profile>"].join(
|
|
193
|
-
"\n",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
MemoryRecallOptions,
|
|
5
|
-
MemoryRecallResult,
|
|
6
|
-
} from "./search/types.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* In-memory cache for memory recall results.
|
|
10
|
-
*
|
|
11
|
-
* The full retrieval pipeline (FTS5 + Qdrant + entity graph + RRF merge) is
|
|
12
|
-
* expensive. When the same query is issued multiple turns in a row (common
|
|
13
|
-
* when the conversation context hasn't changed), we can serve the cached
|
|
14
|
-
* result instantly.
|
|
15
|
-
*
|
|
16
|
-
* Invalidation: a monotonic version counter is bumped whenever new memory
|
|
17
|
-
* is indexed (segments, items, embeddings). Cache entries are only valid
|
|
18
|
-
* when their version matches the current global version.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
interface CacheEntry {
|
|
22
|
-
version: number;
|
|
23
|
-
createdAt: number;
|
|
24
|
-
result: MemoryRecallResult;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const MAX_ENTRIES = 32;
|
|
28
|
-
const TTL_MS = 60_000; // 60 seconds
|
|
29
|
-
|
|
30
|
-
let _version = 0;
|
|
31
|
-
const _cache = new Map<string, CacheEntry>();
|
|
32
|
-
|
|
33
|
-
/** Bump the global memory version, invalidating all cached recall results. */
|
|
34
|
-
export function bumpMemoryVersion(): void {
|
|
35
|
-
_version++;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Return the current memory version (for snapshot-based staleness checks). */
|
|
39
|
-
export function getMemoryVersion(): number {
|
|
40
|
-
return _version;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Build a deterministic cache key from the recall inputs. */
|
|
44
|
-
function buildCacheKey(
|
|
45
|
-
query: string,
|
|
46
|
-
conversationId: string,
|
|
47
|
-
options?: MemoryRecallOptions,
|
|
48
|
-
configFingerprint?: string,
|
|
49
|
-
): string {
|
|
50
|
-
const parts = [
|
|
51
|
-
query,
|
|
52
|
-
conversationId,
|
|
53
|
-
options?.scopeId ?? "",
|
|
54
|
-
options?.scopePolicyOverride
|
|
55
|
-
? `${options.scopePolicyOverride.scopeId}:${options.scopePolicyOverride.fallbackToDefault}`
|
|
56
|
-
: "",
|
|
57
|
-
options?.excludeMessageIds
|
|
58
|
-
? [...options.excludeMessageIds].sort().join(",")
|
|
59
|
-
: "",
|
|
60
|
-
options?.maxInjectTokensOverride != null
|
|
61
|
-
? String(options.maxInjectTokensOverride)
|
|
62
|
-
: "",
|
|
63
|
-
configFingerprint ?? "",
|
|
64
|
-
];
|
|
65
|
-
return createHash("sha256").update(parts.join("\0")).digest("hex");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Look up a cached recall result. Returns undefined on miss or stale entry. */
|
|
69
|
-
export function getCachedRecall(
|
|
70
|
-
query: string,
|
|
71
|
-
conversationId: string,
|
|
72
|
-
options?: MemoryRecallOptions,
|
|
73
|
-
configFingerprint?: string,
|
|
74
|
-
): MemoryRecallResult | undefined {
|
|
75
|
-
const key = buildCacheKey(query, conversationId, options, configFingerprint);
|
|
76
|
-
const entry = _cache.get(key);
|
|
77
|
-
if (!entry) return undefined;
|
|
78
|
-
if (entry.version !== _version || Date.now() - entry.createdAt > TTL_MS) {
|
|
79
|
-
_cache.delete(key);
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
// Move to end of Map iteration order so it's treated as most-recently-used
|
|
83
|
-
_cache.delete(key);
|
|
84
|
-
_cache.set(key, entry);
|
|
85
|
-
return entry.result;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Store a recall result in the cache. Evicts least-recently-used entries when full.
|
|
90
|
-
*
|
|
91
|
-
* When `snapshotVersion` is provided, the entry is only stored if the
|
|
92
|
-
* snapshot still matches the current global version — this prevents a
|
|
93
|
-
* stale result from being cached under a version that was bumped while
|
|
94
|
-
* the retrieval pipeline was in flight.
|
|
95
|
-
*/
|
|
96
|
-
export function setCachedRecall(
|
|
97
|
-
query: string,
|
|
98
|
-
conversationId: string,
|
|
99
|
-
options: MemoryRecallOptions | undefined,
|
|
100
|
-
result: MemoryRecallResult,
|
|
101
|
-
snapshotVersion?: number,
|
|
102
|
-
configFingerprint?: string,
|
|
103
|
-
): void {
|
|
104
|
-
// If a snapshot version was provided, only cache when it still matches
|
|
105
|
-
// the current version — otherwise the result may be stale.
|
|
106
|
-
if (snapshotVersion !== undefined && snapshotVersion !== _version) return;
|
|
107
|
-
|
|
108
|
-
const key = buildCacheKey(query, conversationId, options, configFingerprint);
|
|
109
|
-
|
|
110
|
-
// Evict oldest entries if at capacity
|
|
111
|
-
if (_cache.size >= MAX_ENTRIES && !_cache.has(key)) {
|
|
112
|
-
const oldest = _cache.keys().next().value;
|
|
113
|
-
if (oldest !== undefined) _cache.delete(oldest);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
_cache.set(key, { version: _version, createdAt: Date.now(), result });
|
|
117
|
-
}
|