@vellumai/assistant 0.5.13 → 0.5.15
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/.env.example +1 -6
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +0 -1
- package/bunfig.toml +1 -0
- package/docs/architecture/memory.md +3 -3
- package/openapi.yaml +127 -22
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +2 -32
- package/src/__tests__/actor-token-service.test.ts +1 -31
- package/src/__tests__/anthropic-provider.test.ts +53 -40
- package/src/__tests__/app-git-history.test.ts +9 -17
- package/src/__tests__/app-git-service.test.ts +14 -20
- package/src/__tests__/app-store-dir-names.test.ts +10 -20
- package/src/__tests__/approval-cascade.test.ts +2 -19
- package/src/__tests__/approval-primitive.test.ts +2 -27
- package/src/__tests__/approval-routes-http.test.ts +2 -30
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -28
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -45
- package/src/__tests__/attachments-store.test.ts +5 -32
- package/src/__tests__/audit-log-rotation.test.ts +5 -36
- package/src/__tests__/avatar-e2e.test.ts +1 -9
- package/src/__tests__/avatar-generator.test.ts +1 -7
- package/src/__tests__/browser-fill-credential.test.ts +0 -4
- package/src/__tests__/browser-manager.test.ts +0 -6
- package/src/__tests__/call-controller.test.ts +1 -22
- package/src/__tests__/call-conversation-messages.test.ts +0 -21
- package/src/__tests__/call-domain.test.ts +0 -25
- package/src/__tests__/call-pointer-messages.test.ts +0 -21
- package/src/__tests__/call-recovery.test.ts +0 -22
- package/src/__tests__/call-routes-http.test.ts +0 -24
- package/src/__tests__/call-store.test.ts +0 -21
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -24
- package/src/__tests__/canonical-guardian-store.test.ts +48 -21
- package/src/__tests__/channel-approval-routes.test.ts +6 -26
- package/src/__tests__/channel-approvals.test.ts +1 -38
- package/src/__tests__/channel-delivery-store.test.ts +0 -21
- package/src/__tests__/channel-guardian.test.ts +0 -26
- package/src/__tests__/channel-reply-delivery.test.ts +5 -0
- package/src/__tests__/channel-retry-sweep.test.ts +0 -21
- package/src/__tests__/checker.test.ts +26 -61
- package/src/__tests__/clawhub.test.ts +9 -25
- package/src/__tests__/cli-command-risk-guard.test.ts +0 -18
- package/src/__tests__/config-loader-backfill.test.ts +9 -28
- package/src/__tests__/config-schema-cmd.test.ts +5 -25
- package/src/__tests__/config-schema.test.ts +21 -40
- package/src/__tests__/config-watcher.test.ts +4 -91
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -21
- package/src/__tests__/contacts-tools.test.ts +0 -21
- package/src/__tests__/context-memory-e2e.test.ts +0 -21
- package/src/__tests__/context-window-manager.test.ts +130 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -4
- package/src/__tests__/conversation-agent-loop.test.ts +0 -4
- package/src/__tests__/conversation-attachments.test.ts +1 -24
- package/src/__tests__/conversation-attention-store.test.ts +0 -21
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -22
- package/src/__tests__/conversation-clear-safety.test.ts +0 -22
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -21
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -24
- package/src/__tests__/conversation-disk-view-integration.test.ts +1 -23
- package/src/__tests__/conversation-disk-view.test.ts +5 -27
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +1 -33
- package/src/__tests__/conversation-fork-route.test.ts +0 -27
- package/src/__tests__/conversation-history-web-search.test.ts +23 -16
- package/src/__tests__/conversation-init.benchmark.test.ts +22 -43
- package/src/__tests__/conversation-key-store-disk-view.test.ts +8 -34
- package/src/__tests__/conversation-load-history-repair.test.ts +0 -4
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -4
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -4
- package/src/__tests__/conversation-queue.test.ts +8 -8
- package/src/__tests__/conversation-routes-disk-view.test.ts +13 -51
- package/src/__tests__/conversation-runtime-assembly.test.ts +64 -38
- package/src/__tests__/conversation-slash-commands.test.ts +5 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -4
- package/src/__tests__/conversation-speed-override.test.ts +326 -0
- package/src/__tests__/conversation-starter-routes.test.ts +0 -23
- package/src/__tests__/conversation-store.test.ts +0 -21
- package/src/__tests__/conversation-unread-route.test.ts +0 -24
- package/src/__tests__/conversation-usage.test.ts +56 -21
- package/src/__tests__/conversation-wipe.test.ts +0 -21
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -4
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -4
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -4
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +8 -5
- package/src/__tests__/credential-vault-unit.test.ts +9 -428
- package/src/__tests__/credentials-cli.test.ts +10 -10
- package/src/__tests__/daemon-assistant-events.test.ts +0 -19
- package/src/__tests__/date-context.test.ts +77 -97
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +7 -24
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +29 -42
- package/src/__tests__/delete-managed-skill-tool.test.ts +2 -10
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -26
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +61 -15
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -36
- package/src/__tests__/email-cli.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +5 -17
- package/src/__tests__/first-greeting.test.ts +4 -32
- package/src/__tests__/followup-tools.test.ts +0 -21
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -20
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -23
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -23
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -21
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -21
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -21
- package/src/__tests__/guardian-action-store.test.ts +0 -21
- package/src/__tests__/guardian-action-sweep.test.ts +0 -21
- package/src/__tests__/guardian-binding-drift-heal.test.ts +0 -23
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +172 -22
- package/src/__tests__/guardian-dispatch.test.ts +0 -21
- package/src/__tests__/guardian-grant-minting.test.ts +0 -22
- package/src/__tests__/guardian-outbound-http.test.ts +0 -22
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +0 -23
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -22
- package/src/__tests__/guardian-routing-state.test.ts +0 -22
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -24
- package/src/__tests__/headless-browser-interactions.test.ts +0 -4
- package/src/__tests__/headless-browser-navigate.test.ts +0 -4
- package/src/__tests__/headless-browser-read-tools.test.ts +0 -4
- package/src/__tests__/headless-browser-snapshot.test.ts +0 -4
- package/src/__tests__/heartbeat-service.test.ts +99 -26
- package/src/__tests__/hooks-blocking.test.ts +3 -3
- package/src/__tests__/hooks-config.test.ts +7 -7
- package/src/__tests__/hooks-discovery.test.ts +3 -3
- package/src/__tests__/hooks-integration.test.ts +5 -5
- package/src/__tests__/hooks-manager.test.ts +3 -3
- package/src/__tests__/hooks-runner.test.ts +5 -23
- package/src/__tests__/hooks-settings.test.ts +3 -3
- package/src/__tests__/hooks-templates.test.ts +3 -3
- package/src/__tests__/http-conversation-lineage.test.ts +0 -27
- package/src/__tests__/identity-intro-cache.test.ts +0 -4
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -22
- package/src/__tests__/inline-skill-load-permissions.test.ts +5 -16
- package/src/__tests__/intent-routing.test.ts +2 -55
- package/src/__tests__/invite-redemption-service.test.ts +0 -21
- package/src/__tests__/invite-routes-http.test.ts +0 -21
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +0 -17
- package/src/__tests__/journal-context.test.ts +8 -75
- package/src/__tests__/list-messages-attachments.test.ts +0 -22
- package/src/__tests__/llm-context-route-provider.test.ts +0 -21
- package/src/__tests__/llm-request-log-turn-query.test.ts +46 -28
- package/src/__tests__/llm-usage-store.test.ts +0 -21
- package/src/__tests__/log-export-workspace.test.ts +1 -1
- package/src/__tests__/managed-skill-lifecycle.test.ts +1 -1
- package/src/__tests__/managed-store.test.ts +1 -1
- package/src/__tests__/mcp-cli.test.ts +7 -10
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -21
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +0 -11
- package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -21
- package/src/__tests__/memory-recall-log-store.test.ts +0 -27
- package/src/__tests__/memory-recall-quality.test.ts +0 -21
- package/src/__tests__/memory-regressions.experimental.test.ts +31 -30
- package/src/__tests__/memory-regressions.test.ts +282 -70
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -21
- package/src/__tests__/memory-upsert-concurrency.test.ts +0 -21
- package/src/__tests__/messaging-send-tool.test.ts +201 -0
- package/src/__tests__/migration-cross-version-compatibility.test.ts +18 -13
- package/src/__tests__/migration-export-http.test.ts +7 -1
- package/src/__tests__/migration-import-commit-http.test.ts +16 -14
- package/src/__tests__/migration-import-preflight-http.test.ts +27 -44
- package/src/__tests__/migration-validate-http.test.ts +1 -28
- package/src/__tests__/native-web-search.test.ts +25 -22
- package/src/__tests__/non-member-access-request.test.ts +0 -22
- package/src/__tests__/notification-guardian-path.test.ts +0 -21
- package/src/__tests__/notification-schedule-dedup.test.ts +1 -25
- package/src/__tests__/oauth-apps-routes.test.ts +103 -2
- package/src/__tests__/oauth-cli.test.ts +52 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +0 -16
- package/src/__tests__/oauth-provider-serializer.test.ts +232 -0
- package/src/__tests__/oauth-providers-routes.test.ts +257 -0
- package/src/__tests__/oauth-store.test.ts +0 -21
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/openai-provider.test.ts +261 -0
- package/src/__tests__/pairing-concurrent.test.ts +6 -6
- package/src/__tests__/pairing-routes.test.ts +7 -1
- package/src/__tests__/path-policy.test.ts +1 -1
- package/src/__tests__/platform.test.ts +64 -88
- package/src/__tests__/playbook-execution.test.ts +0 -21
- package/src/__tests__/playbook-tools.test.ts +0 -21
- package/src/__tests__/pricing.test.ts +100 -0
- package/src/__tests__/relay-server.test.ts +1 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -24
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -24
- package/src/__tests__/runtime-events-sse.test.ts +0 -24
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -1
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +1 -1
- package/src/__tests__/schedule-store.test.ts +0 -21
- package/src/__tests__/schedule-tools.test.ts +0 -21
- package/src/__tests__/scheduler-recurrence.test.ts +0 -21
- package/src/__tests__/scoped-approval-grants.test.ts +0 -21
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -21
- package/src/__tests__/secret-allowlist.test.ts +1 -1
- package/src/__tests__/secret-ingress-channel.test.ts +0 -5
- package/src/__tests__/secret-ingress-cli.test.ts +0 -6
- package/src/__tests__/secret-ingress-http.test.ts +0 -5
- package/src/__tests__/secret-ingress.test.ts +0 -5
- package/src/__tests__/send-endpoint-busy.test.ts +0 -24
- package/src/__tests__/sequence-store.test.ts +0 -21
- package/src/__tests__/server-history-render.test.ts +0 -24
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -4
- package/src/__tests__/skill-load-inline-command.test.ts +9 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +9 -0
- package/src/__tests__/skill-load-tool.test.ts +11 -0
- package/src/__tests__/skills-uninstall.test.ts +10 -8
- package/src/__tests__/skills.test.ts +1 -1
- package/src/__tests__/slack-channel-config.test.ts +1 -1
- package/src/__tests__/slack-inbound-verification.test.ts +0 -22
- package/src/__tests__/starter-bundle.test.ts +4 -1
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/test-preload.ts +31 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +0 -20
- package/src/__tests__/tool-input-summary.test.ts +124 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -1
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +1 -1
- package/src/__tests__/trusted-contact-verification.test.ts +1 -1
- package/src/__tests__/turn-boundary-resolution.test.ts +1 -1
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/update-bulletin.test.ts +1 -1
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +1 -1
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -1
- package/src/__tests__/voice-session-bridge.test.ts +1 -1
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +4 -4
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -1
- package/src/__tests__/workspace-migration-down-functions.test.ts +15 -3
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +40 -4
- package/src/agent/loop.ts +6 -9
- package/src/approvals/guardian-decision-primitive.ts +46 -18
- package/src/approvals/guardian-request-resolvers.ts +19 -2
- package/src/calls/active-call-lease.ts +2 -2
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/commands/doctor.ts +9 -9
- package/src/cli/commands/memory.ts +142 -0
- package/src/cli/commands/oauth/__tests__/connect.test.ts +13 -11
- package/src/cli/commands/oauth/__tests__/ping.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +13 -12
- package/src/cli/commands/oauth/index.ts +1 -1
- package/src/cli/commands/oauth/providers.ts +47 -62
- package/src/cli/commands/platform/__tests__/connect.test.ts +72 -46
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +54 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +36 -0
- package/src/cli/commands/platform/connect.ts +17 -7
- package/src/cli/commands/platform/disconnect.ts +28 -3
- package/src/cli/commands/platform/index.ts +3 -3
- package/src/cli.ts +1 -299
- package/src/config/assistant-feature-flags.ts +23 -15
- package/src/config/bundled-skills/app-builder/TOOLS.json +16 -0
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +4 -0
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +5 -1
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +9 -1
- package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +5 -1
- package/src/config/bundled-skills/contacts/TOOLS.json +8 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +10 -1
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +16 -2
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +37 -0
- package/src/config/bundled-skills/slack/SKILL.md +18 -0
- package/src/config/env-registry.ts +15 -11
- package/src/config/env.ts +1 -11
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/schema.ts +4 -0
- package/src/config/schemas/heartbeat.ts +6 -1
- package/src/config/schemas/inference.ts +14 -3
- package/src/config/schemas/memory-processing.ts +16 -8
- package/src/config/schemas/memory-retrieval.ts +3 -3
- package/src/config/skills.ts +1 -1
- package/src/context/window-manager.ts +174 -51
- package/src/credential-execution/executable-discovery.ts +2 -2
- package/src/daemon/approved-devices-store.ts +2 -2
- package/src/daemon/assistant-attachments.ts +2 -0
- package/src/daemon/config-watcher.ts +4 -50
- package/src/daemon/conversation-agent-loop-handlers.ts +9 -1
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-error.ts +3 -5
- package/src/daemon/conversation-history.ts +7 -3
- package/src/daemon/conversation-lifecycle.ts +16 -0
- package/src/daemon/conversation-messaging.ts +1 -0
- package/src/daemon/conversation-notifiers.ts +67 -30
- package/src/daemon/conversation-process.ts +161 -2
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -11
- package/src/daemon/conversation-slash.ts +14 -3
- package/src/daemon/conversation-tool-setup.ts +2 -0
- package/src/daemon/conversation-usage.ts +32 -4
- package/src/daemon/conversation.ts +33 -1
- package/src/daemon/daemon-control.ts +32 -16
- package/src/daemon/date-context.ts +47 -45
- package/src/daemon/dictation-profile-store.ts +2 -2
- package/src/daemon/handlers/conversations.ts +19 -0
- package/src/daemon/handlers/shared.ts +14 -21
- package/src/daemon/lifecycle.ts +5 -7
- package/src/daemon/message-types/conversations.ts +2 -0
- package/src/daemon/message-types/guardian-actions.ts +3 -17
- package/src/daemon/message-types/integrations.ts +11 -1
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/pairing-store.ts +2 -79
- package/src/daemon/server.ts +154 -8
- package/src/daemon/watch-handler.ts +65 -21
- package/src/email/guardrails.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +14 -7
- package/src/hooks/cli.ts +2 -2
- package/src/hooks/config.ts +2 -2
- package/src/hooks/discovery.ts +2 -2
- package/src/hooks/manager.ts +2 -2
- package/src/hooks/runner.ts +5 -2
- package/src/hooks/templates.ts +2 -2
- package/src/memory/admin.ts +181 -2
- package/src/memory/app-git-service.ts +61 -4
- package/src/memory/attachments-store.ts +2 -0
- package/src/memory/canonical-guardian-store.ts +16 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-local.ts +5 -2
- package/src/memory/indexer.ts +44 -26
- package/src/memory/items-extractor.ts +34 -82
- package/src/memory/job-handlers/batch-extraction.ts +741 -0
- package/src/memory/job-handlers/journal-carry-forward.test.ts +383 -0
- package/src/memory/job-handlers/journal-carry-forward.ts +255 -0
- package/src/memory/jobs-store.ts +28 -0
- package/src/memory/jobs-worker.ts +56 -9
- package/src/memory/lifecycle-events-store.ts +4 -2
- package/src/memory/llm-request-log-store.ts +40 -2
- package/src/memory/llm-usage-store.ts +4 -3
- package/src/memory/migrations/199-guardian-request-enrichment-columns.ts +71 -0
- package/src/memory/migrations/200-usage-llm-call-count.ts +20 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/query-expansion.ts +83 -0
- package/src/memory/retriever.test.ts +119 -0
- package/src/memory/retriever.ts +513 -105
- package/src/memory/schema/guardian.ts +4 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/search/formatting.test.ts +140 -0
- package/src/memory/search/formatting.ts +143 -198
- package/src/memory/search/mmr.ts +136 -0
- package/src/memory/search/staleness.ts +0 -15
- package/src/memory/search/tier-classifier.ts +10 -21
- package/src/memory/search/types.ts +17 -0
- package/src/messaging/providers/slack/adapter.ts +51 -5
- package/src/notifications/broadcaster.ts +13 -0
- package/src/notifications/copy-composer.ts +8 -0
- package/src/oauth/connect-orchestrator.ts +1 -1
- package/src/oauth/connection-resolver.ts +2 -2
- package/src/oauth/provider-serializer.ts +116 -0
- package/src/permissions/trust-store.ts +24 -7
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +5 -0
- package/src/prompts/journal-context.ts +50 -35
- package/src/prompts/persona-resolver.ts +1 -1
- package/src/prompts/system-prompt.ts +27 -28
- package/src/prompts/templates/BOOTSTRAP.md +14 -1
- package/src/prompts/templates/HEARTBEAT.md +10 -0
- package/src/prompts/templates/NOW.md +19 -25
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/update-bulletin.ts +1 -1
- package/src/providers/anthropic/client.ts +89 -18
- package/src/providers/model-catalog.ts +22 -2
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/client.ts +40 -1
- package/src/providers/retry.ts +23 -4
- package/src/providers/types.ts +2 -0
- package/src/runtime/assistant-scope.ts +1 -1
- package/src/runtime/auth/__tests__/credential-service.test.ts +1 -0
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/auth/token-service.ts +51 -29
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -1
- package/src/runtime/guardian-decision-types.ts +16 -10
- package/src/runtime/http-server.ts +3 -14
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +7 -4
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -4
- package/src/runtime/migrations/vbundle-importer.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -8
- package/src/runtime/routes/conversation-routes.ts +125 -3
- package/src/runtime/routes/guardian-action-routes.ts +9 -3
- package/src/runtime/routes/identity-routes.ts +25 -4
- package/src/runtime/routes/llm-context-normalization.ts +1 -0
- package/src/runtime/routes/log-export-routes.ts +34 -12
- package/src/runtime/routes/migration-routes.ts +6 -10
- package/src/runtime/routes/oauth-apps.ts +2 -9
- package/src/runtime/routes/oauth-providers.ts +60 -0
- package/src/runtime/routes/pairing-routes.ts +0 -8
- package/src/runtime/routes/settings-routes.ts +0 -1
- package/src/runtime/routes/telemetry-routes.ts +16 -4
- package/src/security/encrypted-store.ts +2 -2
- package/src/security/secret-allowlist.ts +3 -3
- package/src/signals/emit-event.ts +42 -0
- package/src/signals/user-message.ts +37 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +83 -19
- package/src/telemetry/usage-telemetry-reporter.ts +23 -17
- package/src/tools/browser/runtime-check.ts +2 -2
- package/src/tools/credentials/vault.ts +2 -249
- package/src/tools/memory/definitions.ts +1 -1
- package/src/tools/memory/handlers.test.ts +50 -8
- package/src/tools/memory/handlers.ts +3 -1
- package/src/tools/side-effects.ts +1 -6
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +11 -14
- package/src/tools/tool-approval-handler.ts +20 -1
- package/src/tools/tool-input-summary.ts +66 -0
- package/src/tools/types.ts +4 -0
- package/src/usage/types.ts +4 -0
- package/src/util/device-id.ts +10 -10
- package/src/util/platform.ts +71 -33
- package/src/util/pricing.ts +19 -6
- package/src/util/strip-comment-lines.ts +28 -0
- package/src/workspace/git-service.ts +8 -18
- package/src/workspace/migrations/003-seed-device-id.ts +6 -4
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +7 -1
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -4
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +84 -0
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +94 -0
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +86 -0
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +126 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +3 -6
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/signals/confirm.ts +0 -82
- package/src/signals/trust-rule.ts +0 -174
package/src/memory/retriever.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asc, eq, inArray, sql } from "drizzle-orm";
|
|
1
|
+
import { and, asc, eq, inArray, notInArray, sql } from "drizzle-orm";
|
|
2
2
|
|
|
3
3
|
import type { AssistantConfig } from "../config/types.js";
|
|
4
4
|
import { estimateTextTokens } from "../context/token-estimator.js";
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
computeRetryDelay,
|
|
10
10
|
isRetryableNetworkError,
|
|
11
11
|
} from "../util/retry.js";
|
|
12
|
+
import { getConversationDirName } from "./conversation-directories.js";
|
|
12
13
|
import { getDb } from "./db.js";
|
|
13
14
|
import {
|
|
14
15
|
embedWithBackend,
|
|
@@ -17,22 +18,19 @@ import {
|
|
|
17
18
|
logMemoryEmbeddingWarning,
|
|
18
19
|
} from "./embedding-backend.js";
|
|
19
20
|
import { isQdrantBreakerOpen } from "./qdrant-circuit-breaker.js";
|
|
21
|
+
import { expandQueryWithHyDE } from "./query-expansion.js";
|
|
20
22
|
import {
|
|
21
23
|
conversations,
|
|
22
24
|
memoryItems,
|
|
23
25
|
memoryItemSources,
|
|
24
26
|
messages,
|
|
25
27
|
} from "./schema.js";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
CAPABILITY_KINDS,
|
|
29
|
-
IDENTITY_KINDS,
|
|
30
|
-
PREFERENCE_KINDS,
|
|
31
|
-
} from "./search/formatting.js";
|
|
28
|
+
import { buildMemoryInjection } from "./search/formatting.js";
|
|
29
|
+
import { applyMMR } from "./search/mmr.js";
|
|
32
30
|
import { isQdrantConnectionError, semanticSearch } from "./search/semantic.js";
|
|
33
|
-
import {
|
|
31
|
+
import { computeStaleness } from "./search/staleness.js";
|
|
34
32
|
import {
|
|
35
|
-
|
|
33
|
+
filterByMinScore,
|
|
36
34
|
type TieredCandidate,
|
|
37
35
|
} from "./search/tier-classifier.js";
|
|
38
36
|
import type {
|
|
@@ -50,6 +48,7 @@ export {
|
|
|
50
48
|
escapeXmlTags,
|
|
51
49
|
formatAbsoluteTime,
|
|
52
50
|
formatRelativeTime,
|
|
51
|
+
lookupSupersessionChain,
|
|
53
52
|
} from "./search/formatting.js";
|
|
54
53
|
export type {
|
|
55
54
|
DegradationReason,
|
|
@@ -64,6 +63,10 @@ const log = getLogger("memory-retriever");
|
|
|
64
63
|
const EMBED_MAX_RETRIES = 3;
|
|
65
64
|
const EMBED_BASE_DELAY_MS = 500;
|
|
66
65
|
|
|
66
|
+
/** MMR diversity penalty applied to near-duplicate items after score filtering.
|
|
67
|
+
* 0 = no penalty, 1 = maximum penalty. */
|
|
68
|
+
const MMR_PENALTY = 0.6;
|
|
69
|
+
|
|
67
70
|
/**
|
|
68
71
|
* Wrap embedWithBackend with retry + exponential backoff for transient failures
|
|
69
72
|
* (network errors, 429s, 5xx). Aborts immediately if the caller's signal fires.
|
|
@@ -234,20 +237,141 @@ async function generateQueryEmbedding(
|
|
|
234
237
|
return { queryVector, provider, model, degraded, degradation, reason };
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
/** Result from HyDE-expanded search. */
|
|
241
|
+
interface HyDESearchResult {
|
|
242
|
+
candidates: Candidate[];
|
|
243
|
+
hydeExpanded: boolean;
|
|
244
|
+
hydeDocCount: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
237
247
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
248
|
+
* Run HyDE-expanded search: generate hypothetical documents, embed them
|
|
249
|
+
* alongside the raw query in parallel, run parallel semantic searches,
|
|
250
|
+
* and merge all candidate arrays.
|
|
251
|
+
*
|
|
252
|
+
* Falls back to raw-query-only search on any HyDE failure (expansion
|
|
253
|
+
* error, embedding error for hypothetical docs). The raw query search
|
|
254
|
+
* always runs regardless of HyDE success.
|
|
255
|
+
*/
|
|
256
|
+
async function runHyDESearch(
|
|
257
|
+
query: string,
|
|
258
|
+
rawQueryVector: number[],
|
|
259
|
+
config: AssistantConfig,
|
|
260
|
+
signal: AbortSignal | undefined,
|
|
261
|
+
provider: string,
|
|
262
|
+
model: string,
|
|
263
|
+
limit: number,
|
|
264
|
+
excludeMessageIds: string[],
|
|
265
|
+
scopeIds: string[] | undefined,
|
|
266
|
+
sparseVector: { indices: number[]; values: number[] } | undefined,
|
|
267
|
+
): Promise<HyDESearchResult> {
|
|
268
|
+
// Always search with the raw query — this is our baseline
|
|
269
|
+
const rawSearchPromise = semanticSearch(
|
|
270
|
+
rawQueryVector,
|
|
271
|
+
provider,
|
|
272
|
+
model,
|
|
273
|
+
limit,
|
|
274
|
+
excludeMessageIds,
|
|
275
|
+
scopeIds,
|
|
276
|
+
sparseVector,
|
|
277
|
+
);
|
|
278
|
+
// Suppress unhandled rejection if Qdrant rejects before we await
|
|
279
|
+
rawSearchPromise.catch(() => {});
|
|
280
|
+
|
|
281
|
+
// Attempt HyDE expansion — returns [] on any failure
|
|
282
|
+
let hypotheticalDocs: string[];
|
|
283
|
+
try {
|
|
284
|
+
hypotheticalDocs = await expandQueryWithHyDE(query, config, signal);
|
|
285
|
+
} catch {
|
|
286
|
+
// expandQueryWithHyDE already catches internally, but be defensive
|
|
287
|
+
hypotheticalDocs = [];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (hypotheticalDocs.length === 0) {
|
|
291
|
+
// No hypothetical docs — fall back to raw query only
|
|
292
|
+
const rawResults = await rawSearchPromise;
|
|
293
|
+
return {
|
|
294
|
+
candidates: rawResults,
|
|
295
|
+
hydeExpanded: false,
|
|
296
|
+
hydeDocCount: 0,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
log.debug(
|
|
301
|
+
{ hydeDocCount: hypotheticalDocs.length },
|
|
302
|
+
"HyDE expansion produced hypothetical documents",
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Embed all hypothetical docs in parallel with the raw search
|
|
306
|
+
let hydeVectors: number[][] = [];
|
|
307
|
+
try {
|
|
308
|
+
const hydeEmbedResult = await embedWithRetry(config, hypotheticalDocs, {
|
|
309
|
+
signal,
|
|
310
|
+
});
|
|
311
|
+
hydeVectors = hydeEmbedResult.vectors;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
log.warn(
|
|
314
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
315
|
+
"Failed to embed HyDE hypothetical docs; falling back to raw query",
|
|
316
|
+
);
|
|
317
|
+
const rawResults = await rawSearchPromise;
|
|
318
|
+
return {
|
|
319
|
+
candidates: rawResults,
|
|
320
|
+
hydeExpanded: false,
|
|
321
|
+
hydeDocCount: 0,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Run parallel semantic searches for each hypothetical doc embedding,
|
|
326
|
+
// generating per-doc sparse embeddings so sparse and dense components match.
|
|
327
|
+
const hydeSearchPromises = hydeVectors.map((vector, i) => {
|
|
328
|
+
const docSparseVector = generateSparseEmbedding(hypotheticalDocs[i]!);
|
|
329
|
+
return semanticSearch(
|
|
330
|
+
vector,
|
|
331
|
+
provider,
|
|
332
|
+
model,
|
|
333
|
+
limit,
|
|
334
|
+
excludeMessageIds,
|
|
335
|
+
scopeIds,
|
|
336
|
+
docSparseVector,
|
|
337
|
+
).catch((err) => {
|
|
338
|
+
log.warn(
|
|
339
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
340
|
+
"HyDE hypothetical doc search failed; skipping",
|
|
341
|
+
);
|
|
342
|
+
return [] as Candidate[];
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Await all searches in parallel (raw + hypothetical)
|
|
347
|
+
const [rawResults, ...hydeResults] = await Promise.all([
|
|
348
|
+
rawSearchPromise,
|
|
349
|
+
...hydeSearchPromises,
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
// Merge all candidate arrays into a single flat array
|
|
353
|
+
const allCandidates = [rawResults, ...hydeResults].flat();
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
candidates: allCandidates,
|
|
357
|
+
hydeExpanded: true,
|
|
358
|
+
hydeDocCount: hypotheticalDocs.length,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Memory recall pipeline: hybrid search → score filtering →
|
|
364
|
+
* staleness annotation → unified XML injection.
|
|
240
365
|
*
|
|
241
366
|
* Pipeline steps:
|
|
242
367
|
* 1. Build query text (caller provides via buildMemoryQuery)
|
|
243
368
|
* 2. Generate dense + sparse embeddings
|
|
244
369
|
* 3. Hybrid search on Qdrant (dense + sparse RRF fusion)
|
|
245
370
|
* 4. Deduplicate results
|
|
246
|
-
* 5.
|
|
247
|
-
* 6. Enrich
|
|
248
|
-
* 7. Compute staleness per item
|
|
249
|
-
* 8.
|
|
250
|
-
* 9. Build two-layer XML injection with budget allocation
|
|
371
|
+
* 5. Filter by minimum score threshold
|
|
372
|
+
* 6. Enrich candidates with source labels and item metadata
|
|
373
|
+
* 7. Compute staleness per item (for debugging/logging)
|
|
374
|
+
* 8. Build unified XML injection with budget allocation
|
|
251
375
|
*/
|
|
252
376
|
export async function buildMemoryRecall(
|
|
253
377
|
query: string,
|
|
@@ -300,26 +424,49 @@ export async function buildMemoryRecall(
|
|
|
300
424
|
options?.scopePolicyOverride,
|
|
301
425
|
);
|
|
302
426
|
|
|
303
|
-
const HYBRID_LIMIT =
|
|
427
|
+
const HYBRID_LIMIT = 40;
|
|
304
428
|
|
|
305
429
|
let hybridCandidates: Candidate[] = [];
|
|
306
430
|
let semanticSearchFailed = false;
|
|
307
431
|
let sparseVectorUsed = false;
|
|
432
|
+
let hydeExpanded = false;
|
|
433
|
+
let hydeDocCount = 0;
|
|
308
434
|
const hybridSearchStart = Date.now();
|
|
309
435
|
|
|
310
436
|
const qdrantBreakerOpen = isQdrantBreakerOpen();
|
|
311
437
|
if (queryVector && !qdrantBreakerOpen) {
|
|
312
438
|
try {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
439
|
+
if (options?.hydeEnabled) {
|
|
440
|
+
// ── HyDE path: expand query into hypothetical docs and search in parallel ──
|
|
441
|
+
const hydeCandidates = await runHyDESearch(
|
|
442
|
+
query,
|
|
443
|
+
queryVector,
|
|
444
|
+
config,
|
|
445
|
+
signal,
|
|
446
|
+
provider ?? "unknown",
|
|
447
|
+
model ?? "unknown",
|
|
448
|
+
HYBRID_LIMIT,
|
|
449
|
+
excludeMessageIds,
|
|
450
|
+
scopeIds,
|
|
451
|
+
sparseVectorAvailable ? sparseVector : undefined,
|
|
452
|
+
);
|
|
453
|
+
hybridCandidates = hydeCandidates.candidates;
|
|
454
|
+
hydeExpanded = hydeCandidates.hydeExpanded;
|
|
455
|
+
hydeDocCount = hydeCandidates.hydeDocCount;
|
|
456
|
+
sparseVectorUsed = sparseVectorAvailable || hydeExpanded;
|
|
457
|
+
} else {
|
|
458
|
+
// ── Standard path: single raw query search ──
|
|
459
|
+
hybridCandidates = await semanticSearch(
|
|
460
|
+
queryVector,
|
|
461
|
+
provider ?? "unknown",
|
|
462
|
+
model ?? "unknown",
|
|
463
|
+
HYBRID_LIMIT,
|
|
464
|
+
excludeMessageIds,
|
|
465
|
+
scopeIds,
|
|
466
|
+
sparseVectorAvailable ? sparseVector : undefined,
|
|
467
|
+
);
|
|
468
|
+
sparseVectorUsed = sparseVectorAvailable;
|
|
469
|
+
}
|
|
323
470
|
} catch (err) {
|
|
324
471
|
semanticSearchFailed = true;
|
|
325
472
|
if (isQdrantConnectionError(err)) {
|
|
@@ -363,8 +510,9 @@ export async function buildMemoryRecall(
|
|
|
363
510
|
// from messages that were removed by context compaction should be kept —
|
|
364
511
|
// those messages are no longer in the conversation history and memory is
|
|
365
512
|
// the only way they can influence the response.
|
|
513
|
+
let inContextMessageIds: Set<string> | null = null;
|
|
366
514
|
if (conversationId) {
|
|
367
|
-
|
|
515
|
+
inContextMessageIds = getEffectiveInContextMessageIds(conversationId);
|
|
368
516
|
if (inContextMessageIds) {
|
|
369
517
|
for (const [key, c] of candidateMap) {
|
|
370
518
|
if (c.type === "segment") {
|
|
@@ -413,11 +561,12 @@ export async function buildMemoryRecall(
|
|
|
413
561
|
}
|
|
414
562
|
|
|
415
563
|
// Filter items whose ALL sources are in-context
|
|
564
|
+
const contextIds = inContextMessageIds;
|
|
416
565
|
for (const [key, c] of candidateMap) {
|
|
417
566
|
if (c.type !== "item") continue;
|
|
418
567
|
const sourceMessageIds = itemSourceMap.get(c.id);
|
|
419
568
|
if (!sourceMessageIds || sourceMessageIds.length === 0) continue;
|
|
420
|
-
if (sourceMessageIds.every((mid) =>
|
|
569
|
+
if (sourceMessageIds.every((mid) => contextIds.has(mid))) {
|
|
421
570
|
candidateMap.delete(key);
|
|
422
571
|
}
|
|
423
572
|
}
|
|
@@ -436,26 +585,78 @@ export async function buildMemoryRecall(
|
|
|
436
585
|
for (const c of allCandidates) {
|
|
437
586
|
// Multiplicative scoring: importance, confidence, and recency amplify semantic
|
|
438
587
|
// relevance but can't substitute for it. An irrelevant item (semantic ≈ 0)
|
|
439
|
-
// stays low regardless of metadata. Multiplier range: 0.
|
|
588
|
+
// stays low regardless of metadata. Multiplier range: 0.35 (all zero) to 1.0.
|
|
440
589
|
const metadataMultiplier =
|
|
441
|
-
0.
|
|
590
|
+
0.35 + c.importance * 0.3 + c.confidence * 0.1 + c.recency * 0.25;
|
|
442
591
|
c.finalScore = c.semantic * metadataMultiplier;
|
|
443
592
|
}
|
|
444
593
|
allCandidates.sort((a, b) => b.finalScore - a.finalScore);
|
|
445
594
|
|
|
446
|
-
// ── Step 5:
|
|
447
|
-
const
|
|
595
|
+
// ── Step 5: Filter by minimum score threshold ───────────────────
|
|
596
|
+
const filtered = filterByMinScore(allCandidates);
|
|
597
|
+
|
|
598
|
+
// ── Step 5b: MMR diversity ranking ─────────────────────────────
|
|
599
|
+
const mmrRanked = applyMMR(filtered, MMR_PENALTY);
|
|
600
|
+
|
|
601
|
+
// MMR rewrites finalScore, so re-enforce the min-score threshold to
|
|
602
|
+
// drop candidates whose adjusted score fell below the cutoff.
|
|
603
|
+
const diversified = filterByMinScore(mmrRanked);
|
|
604
|
+
|
|
605
|
+
// ── Step 5c: Enrich candidates with source labels ──────────────
|
|
606
|
+
enrichSourceLabels(diversified);
|
|
607
|
+
|
|
608
|
+
// ── Serendipity: sample random memories for unexpected connections ──
|
|
609
|
+
const SERENDIPITY_COUNT = 3;
|
|
610
|
+
const serendipityCandidates = sampleSerendipityItems(
|
|
611
|
+
diversified,
|
|
612
|
+
SERENDIPITY_COUNT,
|
|
613
|
+
scopeIds,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Filter serendipity items whose ALL sources are in-context (same logic
|
|
617
|
+
// as Step 4b) to prevent current-turn content leaking via random sampling.
|
|
618
|
+
if (inContextMessageIds && serendipityCandidates.length > 0) {
|
|
619
|
+
filterInContextItems(serendipityCandidates, inContextMessageIds);
|
|
620
|
+
}
|
|
448
621
|
|
|
449
|
-
|
|
450
|
-
enrichSourceLabels(tiered);
|
|
622
|
+
enrichSourceLabels(serendipityCandidates);
|
|
451
623
|
|
|
452
624
|
// ── Step 6: Enrich with item metadata for staleness ─────────────
|
|
453
|
-
const itemIds =
|
|
625
|
+
const itemIds = diversified.filter((c) => c.type === "item").map((c) => c.id);
|
|
454
626
|
const itemMetadataMap = enrichItemMetadata(itemIds);
|
|
455
627
|
|
|
456
|
-
// ── Step
|
|
628
|
+
// ── Step 6b: Enrich item candidates with supersedes data ────────
|
|
629
|
+
const itemCandidatesForSupersedes = diversified.filter(
|
|
630
|
+
(c) => c.type === "item",
|
|
631
|
+
);
|
|
632
|
+
if (itemCandidatesForSupersedes.length > 0) {
|
|
633
|
+
try {
|
|
634
|
+
const db = getDb();
|
|
635
|
+
const supersedesRows = db
|
|
636
|
+
.select({ id: memoryItems.id, supersedes: memoryItems.supersedes })
|
|
637
|
+
.from(memoryItems)
|
|
638
|
+
.where(
|
|
639
|
+
inArray(
|
|
640
|
+
memoryItems.id,
|
|
641
|
+
itemCandidatesForSupersedes.map((c) => c.id),
|
|
642
|
+
),
|
|
643
|
+
)
|
|
644
|
+
.all();
|
|
645
|
+
const supersedesMap = new Map(
|
|
646
|
+
supersedesRows.map((r) => [r.id, r.supersedes]),
|
|
647
|
+
);
|
|
648
|
+
for (const c of itemCandidatesForSupersedes) {
|
|
649
|
+
const sup = supersedesMap.get(c.id);
|
|
650
|
+
if (sup) c.supersedes = sup;
|
|
651
|
+
}
|
|
652
|
+
} catch (err) {
|
|
653
|
+
log.warn({ err }, "Failed to enrich candidates with supersedes data");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ── Step 7: Compute staleness per item (for debugging/logging) ─
|
|
457
658
|
const now = Date.now();
|
|
458
|
-
for (const c of
|
|
659
|
+
for (const c of diversified) {
|
|
459
660
|
if (c.type !== "item") continue;
|
|
460
661
|
const meta = itemMetadataMap.get(c.id);
|
|
461
662
|
if (!meta) continue;
|
|
@@ -470,10 +671,7 @@ export async function buildMemoryRecall(
|
|
|
470
671
|
c.staleness = level;
|
|
471
672
|
}
|
|
472
673
|
|
|
473
|
-
// ── Step 8:
|
|
474
|
-
const afterDemotion = applyStaleDemotion(tiered);
|
|
475
|
-
|
|
476
|
-
// ── Step 9: Budget allocation and two-layer injection ──────────
|
|
674
|
+
// ── Step 8: Budget allocation and unified injection ────────────
|
|
477
675
|
const maxInjectTokens = Math.max(
|
|
478
676
|
1,
|
|
479
677
|
Math.floor(
|
|
@@ -482,53 +680,24 @@ export async function buildMemoryRecall(
|
|
|
482
680
|
),
|
|
483
681
|
);
|
|
484
682
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
);
|
|
489
|
-
const preferences = afterDemotion.filter(
|
|
490
|
-
(c) => c.tier === 1 && PREFERENCE_KINDS.has(c.kind),
|
|
491
|
-
);
|
|
492
|
-
const capabilities = afterDemotion.filter(
|
|
493
|
-
(c) => c.tier === 1 && CAPABILITY_KINDS.has(c.kind),
|
|
494
|
-
);
|
|
495
|
-
const tier1Candidates = afterDemotion.filter(
|
|
496
|
-
(c) =>
|
|
497
|
-
c.tier === 1 &&
|
|
498
|
-
!IDENTITY_KINDS.has(c.kind) &&
|
|
499
|
-
!PREFERENCE_KINDS.has(c.kind) &&
|
|
500
|
-
!CAPABILITY_KINDS.has(c.kind),
|
|
501
|
-
);
|
|
502
|
-
const tier2Candidates = afterDemotion.filter((c) => c.tier === 2);
|
|
503
|
-
|
|
504
|
-
const injectedText = buildTwoLayerInjection({
|
|
505
|
-
identityItems,
|
|
506
|
-
tier1Candidates,
|
|
507
|
-
tier2Candidates,
|
|
508
|
-
preferences,
|
|
509
|
-
capabilities,
|
|
683
|
+
const injectedText = buildMemoryInjection({
|
|
684
|
+
candidates: diversified,
|
|
685
|
+
serendipityItems: serendipityCandidates,
|
|
510
686
|
totalBudgetTokens: maxInjectTokens,
|
|
511
687
|
});
|
|
512
688
|
|
|
513
689
|
// ── Assemble result ─────────────────────────────────────────────
|
|
514
|
-
const selectedCount =
|
|
515
|
-
|
|
516
|
-
tier1Candidates.length +
|
|
517
|
-
tier2Candidates.length +
|
|
518
|
-
preferences.length +
|
|
519
|
-
capabilities.length;
|
|
520
|
-
|
|
521
|
-
const tier1Count = afterDemotion.filter((c) => c.tier === 1).length;
|
|
522
|
-
const tier2Count = afterDemotion.filter((c) => c.tier === 2).length;
|
|
690
|
+
const selectedCount = diversified.length + serendipityCandidates.length;
|
|
691
|
+
|
|
523
692
|
const stalenessStats = {
|
|
524
|
-
fresh:
|
|
525
|
-
aging:
|
|
526
|
-
stale:
|
|
527
|
-
very_stale:
|
|
528
|
-
.length,
|
|
693
|
+
fresh: diversified.filter((c) => c.staleness === "fresh").length,
|
|
694
|
+
aging: diversified.filter((c) => c.staleness === "aging").length,
|
|
695
|
+
stale: diversified.filter((c) => c.staleness === "stale").length,
|
|
696
|
+
very_stale: diversified.filter((c) => c.staleness === "very_stale").length,
|
|
529
697
|
};
|
|
530
698
|
|
|
531
|
-
const topCandidates: MemoryRecallCandiateDebug[] =
|
|
699
|
+
const topCandidates: MemoryRecallCandiateDebug[] = [...diversified]
|
|
700
|
+
.sort((a, b) => b.finalScore - a.finalScore)
|
|
532
701
|
.slice(0, 10)
|
|
533
702
|
.map((c) => ({
|
|
534
703
|
key: c.key,
|
|
@@ -537,6 +706,7 @@ export async function buildMemoryRecall(
|
|
|
537
706
|
finalScore: c.finalScore,
|
|
538
707
|
semantic: c.semantic,
|
|
539
708
|
recency: c.recency,
|
|
709
|
+
...(c.sourceLabel ? { sourceLabel: c.sourceLabel } : {}),
|
|
540
710
|
}));
|
|
541
711
|
|
|
542
712
|
const latencyMs = Date.now() - start;
|
|
@@ -560,13 +730,12 @@ export async function buildMemoryRecall(
|
|
|
560
730
|
query: truncate(query, 120),
|
|
561
731
|
hybridHits: hybridCandidates.length,
|
|
562
732
|
mergedCount: allCandidates.length,
|
|
563
|
-
tier1Count,
|
|
564
|
-
tier2Count,
|
|
565
733
|
stalenessStats,
|
|
566
734
|
selectedCount,
|
|
567
735
|
maxInjectTokens,
|
|
568
736
|
injectedTokens: estimateTextTokens(injectedText),
|
|
569
737
|
latencyMs,
|
|
738
|
+
...(hydeExpanded ? { hydeExpanded, hydeDocCount } : {}),
|
|
570
739
|
},
|
|
571
740
|
"Memory recall completed",
|
|
572
741
|
);
|
|
@@ -585,10 +754,13 @@ export async function buildMemoryRecall(
|
|
|
585
754
|
injectedText,
|
|
586
755
|
latencyMs,
|
|
587
756
|
topCandidates,
|
|
588
|
-
tier1Count,
|
|
589
|
-
tier2Count,
|
|
757
|
+
tier1Count: 0,
|
|
758
|
+
tier2Count: 0,
|
|
590
759
|
hybridSearchMs,
|
|
591
760
|
sparseVectorUsed,
|
|
761
|
+
hydeExpanded,
|
|
762
|
+
hydeDocCount,
|
|
763
|
+
mmrApplied: true,
|
|
592
764
|
};
|
|
593
765
|
|
|
594
766
|
return result;
|
|
@@ -758,17 +930,17 @@ function enrichSourceLabels(candidates: TieredCandidate[]): void {
|
|
|
758
930
|
try {
|
|
759
931
|
const db = getDb();
|
|
760
932
|
|
|
761
|
-
//
|
|
933
|
+
// ── Items: find conversation via memoryItemSources → messages → conversations ──
|
|
762
934
|
const itemCandidates = candidates.filter((c) => c.type === "item");
|
|
763
935
|
const itemIds = itemCandidates.map((c) => c.id);
|
|
764
936
|
|
|
765
937
|
if (itemIds.length > 0) {
|
|
766
|
-
// For items: find conversation titles via memoryItemSources → messages → conversations.
|
|
767
|
-
// Pick the most recent conversation title per item.
|
|
768
938
|
const rows = db
|
|
769
939
|
.select({
|
|
770
940
|
memoryItemId: memoryItemSources.memoryItemId,
|
|
941
|
+
conversationId: conversations.id,
|
|
771
942
|
title: conversations.title,
|
|
943
|
+
conversationCreatedAt: conversations.createdAt,
|
|
772
944
|
conversationUpdatedAt: conversations.updatedAt,
|
|
773
945
|
})
|
|
774
946
|
.from(memoryItemSources)
|
|
@@ -783,36 +955,272 @@ function enrichSourceLabels(candidates: TieredCandidate[]): void {
|
|
|
783
955
|
.where(inArray(memoryItemSources.memoryItemId, itemIds))
|
|
784
956
|
.all();
|
|
785
957
|
|
|
786
|
-
// Group by item ID and pick the most recently updated conversation
|
|
787
|
-
const
|
|
788
|
-
|
|
958
|
+
// Group by item ID and pick the most recently updated conversation
|
|
959
|
+
const bestConvMap = new Map<
|
|
960
|
+
string,
|
|
961
|
+
{
|
|
962
|
+
title: string | null;
|
|
963
|
+
conversationId: string;
|
|
964
|
+
createdAt: number;
|
|
965
|
+
updatedAt: number;
|
|
966
|
+
}
|
|
967
|
+
>();
|
|
789
968
|
for (const row of rows) {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
969
|
+
const existing = bestConvMap.get(row.memoryItemId);
|
|
970
|
+
if (
|
|
971
|
+
existing === undefined ||
|
|
972
|
+
row.conversationUpdatedAt > existing.updatedAt
|
|
973
|
+
) {
|
|
974
|
+
bestConvMap.set(row.memoryItemId, {
|
|
975
|
+
title: row.title,
|
|
976
|
+
conversationId: row.conversationId,
|
|
977
|
+
createdAt: row.conversationCreatedAt,
|
|
978
|
+
updatedAt: row.conversationUpdatedAt,
|
|
979
|
+
});
|
|
795
980
|
}
|
|
796
981
|
}
|
|
797
982
|
|
|
798
983
|
for (const c of itemCandidates) {
|
|
799
|
-
const
|
|
800
|
-
if (
|
|
801
|
-
c.sourceLabel = title;
|
|
984
|
+
const conv = bestConvMap.get(c.id);
|
|
985
|
+
if (conv) {
|
|
986
|
+
if (conv.title) c.sourceLabel = conv.title;
|
|
987
|
+
const dirName = getConversationDirName(
|
|
988
|
+
conv.conversationId,
|
|
989
|
+
conv.createdAt,
|
|
990
|
+
);
|
|
991
|
+
c.sourcePath = `conversations/${dirName}/messages.jsonl`;
|
|
802
992
|
}
|
|
803
993
|
}
|
|
804
994
|
}
|
|
805
995
|
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
996
|
+
// ── Segments: look up conversation via conversationId on the candidate ──
|
|
997
|
+
const segmentCandidates = candidates.filter(
|
|
998
|
+
(c) => (c.type === "segment" || c.type === "summary") && c.conversationId,
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
if (segmentCandidates.length > 0) {
|
|
1002
|
+
const convIds = [
|
|
1003
|
+
...new Set(segmentCandidates.map((c) => c.conversationId!)),
|
|
1004
|
+
];
|
|
1005
|
+
const convRows = db
|
|
1006
|
+
.select({
|
|
1007
|
+
id: conversations.id,
|
|
1008
|
+
title: conversations.title,
|
|
1009
|
+
createdAt: conversations.createdAt,
|
|
1010
|
+
})
|
|
1011
|
+
.from(conversations)
|
|
1012
|
+
.where(inArray(conversations.id, convIds))
|
|
1013
|
+
.all();
|
|
1014
|
+
|
|
1015
|
+
const convMap = new Map(convRows.map((r) => [r.id, r]));
|
|
1016
|
+
|
|
1017
|
+
for (const c of segmentCandidates) {
|
|
1018
|
+
const conv = convMap.get(c.conversationId!);
|
|
1019
|
+
if (conv) {
|
|
1020
|
+
if (conv.title) c.sourceLabel = conv.title;
|
|
1021
|
+
const dirName = getConversationDirName(conv.id, conv.createdAt);
|
|
1022
|
+
c.sourcePath = `conversations/${dirName}/messages.jsonl`;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
811
1026
|
} catch (err) {
|
|
812
1027
|
log.warn({ err }, "Failed to enrich candidates with source labels");
|
|
813
1028
|
}
|
|
814
1029
|
}
|
|
815
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* Remove items from the array (in-place) whose ALL source messages are
|
|
1033
|
+
* in the given in-context set. This prevents current-turn content from
|
|
1034
|
+
* leaking into the injection via serendipity or other DB-sourced paths.
|
|
1035
|
+
*/
|
|
1036
|
+
function filterInContextItems(
|
|
1037
|
+
candidates: TieredCandidate[],
|
|
1038
|
+
inContextMessageIds: Set<string>,
|
|
1039
|
+
): void {
|
|
1040
|
+
const itemIds = candidates.filter((c) => c.type === "item").map((c) => c.id);
|
|
1041
|
+
if (itemIds.length === 0) return;
|
|
1042
|
+
|
|
1043
|
+
try {
|
|
1044
|
+
const db = getDb();
|
|
1045
|
+
const allSources = db
|
|
1046
|
+
.select({
|
|
1047
|
+
memoryItemId: memoryItemSources.memoryItemId,
|
|
1048
|
+
messageId: memoryItemSources.messageId,
|
|
1049
|
+
})
|
|
1050
|
+
.from(memoryItemSources)
|
|
1051
|
+
.where(inArray(memoryItemSources.memoryItemId, itemIds))
|
|
1052
|
+
.all();
|
|
1053
|
+
|
|
1054
|
+
const itemSourceMap = new Map<string, string[]>();
|
|
1055
|
+
for (const s of allSources) {
|
|
1056
|
+
const existing = itemSourceMap.get(s.memoryItemId);
|
|
1057
|
+
if (existing) existing.push(s.messageId);
|
|
1058
|
+
else itemSourceMap.set(s.memoryItemId, [s.messageId]);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
1062
|
+
const c = candidates[i];
|
|
1063
|
+
if (c.type !== "item") continue;
|
|
1064
|
+
const sourceMessageIds = itemSourceMap.get(c.id);
|
|
1065
|
+
if (!sourceMessageIds || sourceMessageIds.length === 0) continue;
|
|
1066
|
+
if (sourceMessageIds.every((mid) => inContextMessageIds.has(mid))) {
|
|
1067
|
+
candidates.splice(i, 1);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
log.warn(
|
|
1072
|
+
{ err },
|
|
1073
|
+
"Failed to filter in-context serendipity items; skipping",
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Sample random active memory items for serendipitous recall — items
|
|
1080
|
+
* the user didn't ask about but might spark unexpected connections.
|
|
1081
|
+
*
|
|
1082
|
+
* Queries SQLite for random active items not already in the candidate pool,
|
|
1083
|
+
* then selects up to `count` items with probability proportional to their
|
|
1084
|
+
* importance value (importance-weighted sampling).
|
|
1085
|
+
*
|
|
1086
|
+
* Items with importance >= MIN_SERENDIPITY_IMPORTANCE are eligible, as are
|
|
1087
|
+
* legacy items with NULL importance (not yet backfilled). This ensures
|
|
1088
|
+
* genuinely significant memories and pre-importance-era items can both
|
|
1089
|
+
* surface as echoes.
|
|
1090
|
+
*/
|
|
1091
|
+
const MIN_SERENDIPITY_IMPORTANCE = 0.7;
|
|
1092
|
+
|
|
1093
|
+
function sampleSerendipityItems(
|
|
1094
|
+
existingCandidates: TieredCandidate[],
|
|
1095
|
+
count: number,
|
|
1096
|
+
scopeIds?: string[],
|
|
1097
|
+
): TieredCandidate[] {
|
|
1098
|
+
if (count <= 0) return [];
|
|
1099
|
+
|
|
1100
|
+
try {
|
|
1101
|
+
const db = getDb();
|
|
1102
|
+
|
|
1103
|
+
// Collect IDs of item candidates already in the filtered set to exclude them
|
|
1104
|
+
const existingItemIds = existingCandidates
|
|
1105
|
+
.filter((c) => c.type === "item")
|
|
1106
|
+
.map((c) => c.id);
|
|
1107
|
+
|
|
1108
|
+
const RANDOM_POOL_SIZE = 10;
|
|
1109
|
+
|
|
1110
|
+
// Build scope condition: match allowed scopes, or default to 'default'
|
|
1111
|
+
// when no scope filter is set (prevents leaking private-scope items)
|
|
1112
|
+
const scopeCondition = scopeIds
|
|
1113
|
+
? inArray(memoryItems.scopeId, scopeIds)
|
|
1114
|
+
: eq(memoryItems.scopeId, "default");
|
|
1115
|
+
|
|
1116
|
+
const importanceFloor = sql`(${memoryItems.importance} >= ${MIN_SERENDIPITY_IMPORTANCE} OR ${memoryItems.importance} IS NULL)`;
|
|
1117
|
+
|
|
1118
|
+
const baseConditions =
|
|
1119
|
+
existingItemIds.length > 0
|
|
1120
|
+
? and(
|
|
1121
|
+
eq(memoryItems.status, "active"),
|
|
1122
|
+
scopeCondition,
|
|
1123
|
+
importanceFloor,
|
|
1124
|
+
notInArray(memoryItems.id, existingItemIds),
|
|
1125
|
+
)
|
|
1126
|
+
: and(
|
|
1127
|
+
eq(memoryItems.status, "active"),
|
|
1128
|
+
scopeCondition,
|
|
1129
|
+
importanceFloor,
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
// Use rowid-probe sampling instead of ORDER BY RANDOM() to avoid a
|
|
1133
|
+
// full-table sort whose cost grows linearly with memory_items size.
|
|
1134
|
+
// Strategy: get the rowid range, generate random rowids, and probe for
|
|
1135
|
+
// the nearest eligible row with `rowid >= ?`. Each probe is O(log n)
|
|
1136
|
+
// via B-tree lookup, so total cost is O(k·log n) instead of O(n·log n).
|
|
1137
|
+
const range = db
|
|
1138
|
+
.select({
|
|
1139
|
+
minRowid: sql<number>`MIN(rowid)`,
|
|
1140
|
+
maxRowid: sql<number>`MAX(rowid)`,
|
|
1141
|
+
total: sql<number>`COUNT(*)`,
|
|
1142
|
+
})
|
|
1143
|
+
.from(memoryItems)
|
|
1144
|
+
.where(baseConditions)
|
|
1145
|
+
.get();
|
|
1146
|
+
|
|
1147
|
+
if (!range || range.total === 0) return [];
|
|
1148
|
+
|
|
1149
|
+
const columns = {
|
|
1150
|
+
id: memoryItems.id,
|
|
1151
|
+
kind: memoryItems.kind,
|
|
1152
|
+
subject: memoryItems.subject,
|
|
1153
|
+
statement: memoryItems.statement,
|
|
1154
|
+
importance: memoryItems.importance,
|
|
1155
|
+
firstSeenAt: memoryItems.firstSeenAt,
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
let rows;
|
|
1159
|
+
if (range.total <= RANDOM_POOL_SIZE) {
|
|
1160
|
+
// Few enough eligible rows — fetch all, no randomness needed at DB level
|
|
1161
|
+
rows = db
|
|
1162
|
+
.select(columns)
|
|
1163
|
+
.from(memoryItems)
|
|
1164
|
+
.where(baseConditions)
|
|
1165
|
+
.all();
|
|
1166
|
+
} else {
|
|
1167
|
+
// Probe random rowids in the eligible range
|
|
1168
|
+
const seen = new Set<string>();
|
|
1169
|
+
rows = [];
|
|
1170
|
+
const rowidSpan = range.maxRowid - range.minRowid + 1;
|
|
1171
|
+
const maxAttempts = RANDOM_POOL_SIZE * 5;
|
|
1172
|
+
for (let i = 0; i < maxAttempts && rows.length < RANDOM_POOL_SIZE; i++) {
|
|
1173
|
+
const randomRowid =
|
|
1174
|
+
range.minRowid + Math.floor(Math.random() * rowidSpan);
|
|
1175
|
+
const row = db
|
|
1176
|
+
.select(columns)
|
|
1177
|
+
.from(memoryItems)
|
|
1178
|
+
.where(and(baseConditions, sql`rowid >= ${randomRowid}`))
|
|
1179
|
+
.orderBy(sql`rowid`)
|
|
1180
|
+
.limit(1)
|
|
1181
|
+
.get();
|
|
1182
|
+
if (row && !seen.has(row.id)) {
|
|
1183
|
+
seen.add(row.id);
|
|
1184
|
+
rows.push(row);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (rows.length === 0) return [];
|
|
1190
|
+
|
|
1191
|
+
// Importance-weighted sampling: sort by importance * random() descending
|
|
1192
|
+
// and take the top `count` items
|
|
1193
|
+
const weighted = rows
|
|
1194
|
+
.map((row) => ({
|
|
1195
|
+
row,
|
|
1196
|
+
score: (row.importance ?? 0.5) * Math.random(),
|
|
1197
|
+
}))
|
|
1198
|
+
.sort((a, b) => b.score - a.score)
|
|
1199
|
+
.slice(0, count);
|
|
1200
|
+
|
|
1201
|
+
// Convert to Candidate-compatible objects
|
|
1202
|
+
return weighted.map(
|
|
1203
|
+
({ row }): TieredCandidate => ({
|
|
1204
|
+
type: "item",
|
|
1205
|
+
id: row.id,
|
|
1206
|
+
key: `item:${row.id}`,
|
|
1207
|
+
kind: row.kind,
|
|
1208
|
+
text: row.statement,
|
|
1209
|
+
source: "semantic",
|
|
1210
|
+
importance: row.importance ?? 0.5,
|
|
1211
|
+
confidence: 1,
|
|
1212
|
+
semantic: 0,
|
|
1213
|
+
recency: 0,
|
|
1214
|
+
finalScore: 0,
|
|
1215
|
+
createdAt: row.firstSeenAt,
|
|
1216
|
+
}),
|
|
1217
|
+
);
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
log.warn({ err }, "Failed to sample serendipity items");
|
|
1220
|
+
return [];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
816
1224
|
/**
|
|
817
1225
|
* Inject memory recall as a text content block prepended to the last user
|
|
818
1226
|
* message. This follows the same pattern as workspace, temporal, and other
|