@vellumai/assistant 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +20 -4
- package/docker-entrypoint.sh +4 -2
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/docs/plugins.md +75 -79
- package/examples/plugins/echo/README.md +6 -12
- package/examples/plugins/echo/register.ts +0 -41
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
- package/openapi.yaml +3381 -348
- package/package.json +1 -1
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +37 -87
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -3
- package/src/__tests__/anthropic-provider.test.ts +95 -2
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
- package/src/__tests__/btw-routes.test.ts +62 -3
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- package/src/__tests__/catalog-files.test.ts +1 -1
- package/src/__tests__/clawhub-files.test.ts +1 -1
- package/src/__tests__/compaction-pipeline.test.ts +1 -1
- package/src/__tests__/compaction.benchmark.test.ts +0 -30
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
- package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -1
- package/src/__tests__/conversation-pairing.test.ts +4 -31
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
- package/src/__tests__/conversation-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
- package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
- package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
- package/src/__tests__/conversation-sync-tags.test.ts +27 -15
- package/src/__tests__/conversation-title-service.test.ts +135 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -0
- package/src/__tests__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +12 -100
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
- package/src/__tests__/injector-background-turn.test.ts +2 -8
- package/src/__tests__/injector-chain.test.ts +106 -270
- package/src/__tests__/injector-disk-pressure.test.ts +3 -12
- package/src/__tests__/injector-document-comments.test.ts +2 -2
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +31 -37
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-page-latest.test.ts +60 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
- package/src/__tests__/llm-usage-store.test.ts +223 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- package/src/__tests__/native-web-search.test.ts +191 -0
- package/src/__tests__/onboarding-template-contract.test.ts +2 -0
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +31 -1
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pipeline-runner.test.ts +29 -39
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-bootstrap.test.ts +13 -28
- package/src/__tests__/plugin-registry.test.ts +0 -27
- package/src/__tests__/plugin-types.test.ts +2 -125
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
- package/src/__tests__/schedule-routes.test.ts +603 -2
- package/src/__tests__/schedule-store.test.ts +41 -0
- package/src/__tests__/schedule-tools.test.ts +35 -0
- package/src/__tests__/server-history-render.test.ts +314 -1
- package/src/__tests__/skillssh-files.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +20 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +6 -1
- package/src/__tests__/title-generate-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -2
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- package/src/agent/loop.ts +346 -442
- package/src/api/events/assistant-thinking-delta.ts +33 -0
- package/src/api/events/tool-output-chunk.ts +45 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/trace-event.ts +69 -0
- package/src/api/index.ts +48 -13
- package/src/api/responses/conversation-message.ts +368 -0
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- package/src/config/assistant-feature-flags.ts +22 -11
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
- package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -7
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +35 -3
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-v2.ts +8 -0
- package/src/config/schemas/memory-v3.ts +8 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +1 -1
- package/src/context/strip-injections.ts +122 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +3 -6
- package/src/credential-execution/executable-discovery.ts +16 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
- package/src/daemon/conversation-agent-loop.ts +281 -760
- package/src/daemon/conversation-history.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +3 -4
- package/src/daemon/conversation-messaging.ts +7 -6
- package/src/daemon/conversation-process.ts +11 -16
- package/src/daemon/conversation-runtime-assembly.ts +130 -347
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation.ts +32 -14
- package/src/daemon/external-plugins-bootstrap.ts +9 -10
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +39 -10
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/message-types/apps.ts +1 -29
- package/src/daemon/message-types/messages.ts +9 -57
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +136 -3
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- package/src/daemon/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +19 -0
- package/src/daemon/wake-target-adapter.ts +3 -1
- package/src/home/home-greeting-cache.ts +24 -1
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.ts +3 -3
- package/src/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
- package/src/memory/auth-fallback-events-store.ts +94 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-init.ts +4 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/memory/graph/conversation-graph-memory.ts +65 -0
- package/src/memory/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +31 -4
- package/src/memory/llm-usage-store.ts +224 -50
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
- package/src/memory/v2/consolidation-job.ts +1 -1
- package/src/memory/v3/__tests__/health.test.ts +16 -0
- package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
- package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
- package/src/memory/v3/__tests__/router.test.ts +101 -29
- package/src/memory/v3/__tests__/selector.test.ts +93 -27
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/llm-retry.ts +32 -0
- package/src/memory/v3/orchestrate.ts +26 -14
- package/src/memory/v3/provider-blocks.ts +15 -5
- package/src/memory/v3/router.ts +48 -42
- package/src/memory/v3/selector.ts +57 -42
- package/src/memory/v3/shadow-plugin.ts +47 -15
- package/src/memory/v3/types.ts +8 -0
- package/src/notifications/conversation-pairing.ts +8 -15
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/home-feed-side-effect.ts +12 -1
- package/src/permissions/prompter.ts +4 -0
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +8 -1
- package/src/plugin-api/types.ts +151 -1
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/register.ts +8 -13
- package/src/plugins/defaults/index.ts +1 -15
- package/src/plugins/defaults/injectors/register.ts +243 -74
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/title-generate/package.json +1 -1
- package/src/plugins/defaults/title-generate/register.ts +18 -18
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +1 -1
- package/src/plugins/defaults/tool-error/register.ts +9 -21
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
- package/src/plugins/pipeline.ts +6 -18
- package/src/plugins/registry.ts +8 -25
- package/src/plugins/types.ts +43 -474
- package/src/proactive-artifact/aux-message-injector.ts +3 -3
- package/src/proactive-artifact/job.test.ts +7 -12
- package/src/prompts/__tests__/system-prompt.test.ts +36 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +15 -0
- package/src/providers/anthropic/client.ts +37 -29
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -0
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/placeholder-sentinels.ts +35 -0
- package/src/runtime/__tests__/agent-wake.test.ts +5 -1
- package/src/runtime/agent-wake.ts +2 -2
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
- package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- package/src/runtime/routes/conversation-query-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +358 -184
- package/src/runtime/routes/documents-routes.ts +4 -0
- package/src/runtime/routes/domain-routes.ts +51 -37
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +28 -34
- package/src/runtime/routes/gateway-log-routes.ts +26 -4
- package/src/runtime/routes/heartbeat-routes.ts +32 -12
- package/src/runtime/routes/identity-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +208 -17
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/a2a.ts +12 -10
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +4 -0
- package/src/runtime/routes/integrations/slack/share.ts +27 -6
- package/src/runtime/routes/integrations/telegram.ts +6 -0
- package/src/runtime/routes/integrations/twilio.ts +42 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
- package/src/runtime/routes/log-export-routes.ts +8 -0
- package/src/runtime/routes/memory-v2-routes.ts +15 -8
- package/src/runtime/routes/memory-v3-routes.ts +50 -28
- package/src/runtime/routes/oauth-apps.ts +66 -12
- package/src/runtime/routes/oauth-providers.ts +44 -5
- package/src/runtime/routes/platform-routes.ts +81 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/schedule-routes.ts +152 -42
- package/src/runtime/routes/secret-routes.ts +14 -2
- package/src/runtime/routes/skills-routes.ts +43 -14
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trust-rules-routes.ts +26 -2
- package/src/runtime/routes/tts-routes.ts +35 -0
- package/src/runtime/routes/types.ts +66 -8
- package/src/runtime/routes/usage-routes.ts +47 -39
- package/src/runtime/routes/webhook-routes.ts +41 -2
- package/src/runtime/routes/workspace-routes.ts +4 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
- package/src/runtime/services/analyze-conversation.ts +2 -2
- package/src/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- package/src/skills/catalog-files.ts +2 -2
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -2
- package/src/skills/skillssh-files.ts +1 -2
- package/src/telemetry/types.ts +29 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
- package/src/telemetry/usage-telemetry-reporter.ts +57 -2
- package/src/tools/executor.ts +1 -53
- package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +11 -3
- package/src/tools/network/web-search-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +207 -48
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +9 -1
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- package/src/util/mutex.ts +47 -0
- package/src/workspace/git-service.ts +1 -42
- package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- package/src/__tests__/empty-response-pipeline.test.ts +0 -423
- package/src/__tests__/llm-call-pipeline.test.ts +0 -287
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -503
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
- package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- package/src/home/feature-gate.ts +0 -22
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
- package/src/plugins/defaults/empty-response/terminal.ts +0 -106
- package/src/plugins/defaults/injectors/package.json +0 -15
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
- package/src/plugins/defaults/llm-call/package.json +0 -15
- package/src/plugins/defaults/llm-call/register.ts +0 -45
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
- package/src/plugins/defaults/memory-retrieval/package.json +0 -15
- package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
- package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
- package/src/plugins/defaults/persistence/package.json +0 -15
- package/src/plugins/defaults/persistence/register.ts +0 -38
- package/src/plugins/defaults/persistence/terminal.ts +0 -83
- package/src/plugins/defaults/title-generate/terminal.ts +0 -31
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
- package/src/plugins/defaults/token-estimate/package.json +0 -15
- package/src/plugins/defaults/token-estimate/register.ts +0 -34
- package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
- package/src/plugins/defaults/tool-error/terminal.ts +0 -47
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
- package/src/plugins/defaults/tool-execute/package.json +0 -15
- package/src/plugins/defaults/tool-execute/register.ts +0 -49
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
- package/src/skills/category-inference.ts +0 -111
|
@@ -72,6 +72,7 @@ const CONSOLIDATE_CHECKPOINT_KEY = "memory_v2_consolidate_last_run";
|
|
|
72
72
|
|
|
73
73
|
function buildConfig(overrides: {
|
|
74
74
|
v2Enabled?: boolean;
|
|
75
|
+
consolidationEnabled?: boolean;
|
|
75
76
|
intervalHours?: number;
|
|
76
77
|
maxBufferLines?: number | null;
|
|
77
78
|
}) {
|
|
@@ -79,6 +80,13 @@ function buildConfig(overrides: {
|
|
|
79
80
|
if (overrides.v2Enabled !== undefined) {
|
|
80
81
|
partial.memory.v2.enabled = overrides.v2Enabled;
|
|
81
82
|
}
|
|
83
|
+
if (overrides.consolidationEnabled !== undefined) {
|
|
84
|
+
(
|
|
85
|
+
partial.memory.v2 as typeof partial.memory.v2 & {
|
|
86
|
+
consolidation_enabled?: boolean;
|
|
87
|
+
}
|
|
88
|
+
).consolidation_enabled = overrides.consolidationEnabled;
|
|
89
|
+
}
|
|
82
90
|
if (overrides.intervalHours !== undefined) {
|
|
83
91
|
partial.memory.v2.consolidation_interval_hours = overrides.intervalHours;
|
|
84
92
|
}
|
|
@@ -110,6 +118,15 @@ function countPendingJobs(type: string): number {
|
|
|
110
118
|
.all().length;
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
function consolidationJobPayloads(): Record<string, unknown>[] {
|
|
122
|
+
return getDb()
|
|
123
|
+
.select({ payload: memoryJobs.payload })
|
|
124
|
+
.from(memoryJobs)
|
|
125
|
+
.where(eq(memoryJobs.type, "memory_v2_consolidate"))
|
|
126
|
+
.all()
|
|
127
|
+
.map((row) => JSON.parse(row.payload) as Record<string, unknown>);
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
// Initialize the DB once for the file; clear per-test tables in beforeEach
|
|
114
131
|
// rather than tearing down the singleton, which is slow because it re-runs
|
|
115
132
|
// every migration on the next access.
|
|
@@ -138,6 +155,7 @@ describe("maybeEnqueueGraphMaintenanceJobs — memory v2 consolidation", () => {
|
|
|
138
155
|
maybeEnqueueGraphMaintenanceJobs(config, Date.now());
|
|
139
156
|
|
|
140
157
|
expect(countPendingJobs("memory_v2_consolidate")).toBe(1);
|
|
158
|
+
expect(consolidationJobPayloads()).toEqual([{ trigger: "automatic" }]);
|
|
141
159
|
// v1 entries are suppressed when v2 is active.
|
|
142
160
|
expect(countPendingJobs("graph_decay")).toBe(0);
|
|
143
161
|
expect(countPendingJobs("graph_consolidate")).toBe(0);
|
|
@@ -170,6 +188,7 @@ describe("maybeEnqueueGraphMaintenanceJobs — memory v2 consolidation", () => {
|
|
|
170
188
|
maybeEnqueueGraphMaintenanceJobs(config, now);
|
|
171
189
|
|
|
172
190
|
expect(countPendingJobs("memory_v2_consolidate")).toBe(1);
|
|
191
|
+
expect(consolidationJobPayloads()).toEqual([{ trigger: "automatic" }]);
|
|
173
192
|
});
|
|
174
193
|
|
|
175
194
|
test("respects a custom consolidation_interval_hours value", () => {
|
|
@@ -231,6 +250,28 @@ describe("maybeEnqueueGraphMaintenanceJobs — memory v2 consolidation", () => {
|
|
|
231
250
|
expect(countPendingJobs("graph_narrative_refine")).toBe(1);
|
|
232
251
|
expect(countPendingJobs("memory_v2_consolidate")).toBe(0);
|
|
233
252
|
});
|
|
253
|
+
|
|
254
|
+
test("automatic consolidation off suppresses the v2 schedule without re-enabling v1 maintenance", () => {
|
|
255
|
+
const config = buildConfig({
|
|
256
|
+
v2Enabled: true,
|
|
257
|
+
consolidationEnabled: false,
|
|
258
|
+
intervalHours: 1,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
deleteMemoryCheckpoint("graph_maintenance:decay:last_run");
|
|
262
|
+
deleteMemoryCheckpoint("graph_maintenance:consolidate:last_run");
|
|
263
|
+
deleteMemoryCheckpoint("graph_maintenance:pattern_scan:last_run");
|
|
264
|
+
deleteMemoryCheckpoint("graph_maintenance:narrative:last_run");
|
|
265
|
+
deleteMemoryCheckpoint(CONSOLIDATE_CHECKPOINT_KEY);
|
|
266
|
+
|
|
267
|
+
maybeEnqueueGraphMaintenanceJobs(config, Date.now());
|
|
268
|
+
|
|
269
|
+
expect(countPendingJobs("memory_v2_consolidate")).toBe(0);
|
|
270
|
+
expect(countPendingJobs("graph_decay")).toBe(0);
|
|
271
|
+
expect(countPendingJobs("graph_consolidate")).toBe(0);
|
|
272
|
+
expect(countPendingJobs("graph_pattern_scan")).toBe(0);
|
|
273
|
+
expect(countPendingJobs("graph_narrative_refine")).toBe(0);
|
|
274
|
+
});
|
|
234
275
|
});
|
|
235
276
|
|
|
236
277
|
describe("maybeEnqueueGraphMaintenanceJobs — buffer-size trigger", () => {
|
|
@@ -369,4 +410,19 @@ describe("maybeEnqueueGraphMaintenanceJobs — buffer-size trigger", () => {
|
|
|
369
410
|
|
|
370
411
|
expect(countPendingJobs("memory_v2_consolidate")).toBe(0);
|
|
371
412
|
});
|
|
413
|
+
|
|
414
|
+
test("size trigger inert when automatic consolidation is disabled", () => {
|
|
415
|
+
const config = buildConfig({
|
|
416
|
+
v2Enabled: true,
|
|
417
|
+
consolidationEnabled: false,
|
|
418
|
+
intervalHours: 1,
|
|
419
|
+
maxBufferLines: 1,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
writeBuffer(100);
|
|
423
|
+
|
|
424
|
+
maybeEnqueueGraphMaintenanceJobs(config, Date.now());
|
|
425
|
+
|
|
426
|
+
expect(countPendingJobs("memory_v2_consolidate")).toBe(0);
|
|
427
|
+
});
|
|
372
428
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { and, asc, eq, gt, or } from "drizzle-orm";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
|
|
4
|
+
import { getConfig } from "../config/loader.js";
|
|
5
|
+
import { getDb } from "./db-connection.js";
|
|
6
|
+
import { authFallbackEvents } from "./schema.js";
|
|
7
|
+
|
|
8
|
+
/** A single aggregated auth-fallback count for one (guard, path, failure_kind). */
|
|
9
|
+
export interface AuthFallbackCount {
|
|
10
|
+
guard: string;
|
|
11
|
+
path: string;
|
|
12
|
+
failureKind: string;
|
|
13
|
+
count: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A persisted auth-fallback event row. */
|
|
17
|
+
export interface AuthFallbackEvent {
|
|
18
|
+
id: string;
|
|
19
|
+
createdAt: number;
|
|
20
|
+
guard: string;
|
|
21
|
+
path: string;
|
|
22
|
+
failureKind: string;
|
|
23
|
+
count: number;
|
|
24
|
+
windowStart: number;
|
|
25
|
+
windowEnd: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a batch of aggregated auth-fallback counts forwarded by the gateway —
|
|
30
|
+
* one row per count entry, all sharing the same flush window. Returns the
|
|
31
|
+
* number of rows recorded, or 0 when usage data collection is disabled (the
|
|
32
|
+
* counts are dropped to honor the opt-out, matching the rest of telemetry).
|
|
33
|
+
*/
|
|
34
|
+
export function recordAuthFallbackCounts(
|
|
35
|
+
windowStart: number,
|
|
36
|
+
windowEnd: number,
|
|
37
|
+
counts: AuthFallbackCount[],
|
|
38
|
+
): number {
|
|
39
|
+
if (!getConfig().collectUsageData) return 0;
|
|
40
|
+
if (counts.length === 0) return 0;
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const createdAt = Date.now();
|
|
43
|
+
const rows = counts.map((c) => ({
|
|
44
|
+
id: uuid(),
|
|
45
|
+
createdAt,
|
|
46
|
+
guard: c.guard,
|
|
47
|
+
path: c.path,
|
|
48
|
+
failureKind: c.failureKind,
|
|
49
|
+
count: c.count,
|
|
50
|
+
windowStart,
|
|
51
|
+
windowEnd,
|
|
52
|
+
}));
|
|
53
|
+
db.insert(authFallbackEvents).values(rows).run();
|
|
54
|
+
return rows.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Query auth-fallback events that haven't been reported to telemetry yet.
|
|
59
|
+
* Uses a compound cursor (createdAt + id) for reliable watermarking.
|
|
60
|
+
*/
|
|
61
|
+
export function queryUnreportedAuthFallbackEvents(
|
|
62
|
+
afterCreatedAt: number,
|
|
63
|
+
afterId: string | undefined,
|
|
64
|
+
limit: number,
|
|
65
|
+
): AuthFallbackEvent[] {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
const rows = db
|
|
68
|
+
.select({
|
|
69
|
+
id: authFallbackEvents.id,
|
|
70
|
+
createdAt: authFallbackEvents.createdAt,
|
|
71
|
+
guard: authFallbackEvents.guard,
|
|
72
|
+
path: authFallbackEvents.path,
|
|
73
|
+
failureKind: authFallbackEvents.failureKind,
|
|
74
|
+
count: authFallbackEvents.count,
|
|
75
|
+
windowStart: authFallbackEvents.windowStart,
|
|
76
|
+
windowEnd: authFallbackEvents.windowEnd,
|
|
77
|
+
})
|
|
78
|
+
.from(authFallbackEvents)
|
|
79
|
+
.where(
|
|
80
|
+
afterId
|
|
81
|
+
? or(
|
|
82
|
+
gt(authFallbackEvents.createdAt, afterCreatedAt),
|
|
83
|
+
and(
|
|
84
|
+
eq(authFallbackEvents.createdAt, afterCreatedAt),
|
|
85
|
+
gt(authFallbackEvents.id, afterId),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
: gt(authFallbackEvents.createdAt, afterCreatedAt),
|
|
89
|
+
)
|
|
90
|
+
.orderBy(asc(authFallbackEvents.createdAt), asc(authFallbackEvents.id))
|
|
91
|
+
.limit(limit)
|
|
92
|
+
.all();
|
|
93
|
+
return rows;
|
|
94
|
+
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
import { getConfiguredProvider } from "../providers/provider-send-message.js";
|
|
11
11
|
import type { Provider } from "../providers/types.js";
|
|
12
12
|
import { runBtwSidechain } from "../runtime/btw-sidechain.js";
|
|
13
|
+
import { publishConversationTitleChanged } from "../runtime/sync/resource-sync-events.js";
|
|
13
14
|
import { getLogger } from "../util/logger.js";
|
|
15
|
+
import { Mutex } from "../util/mutex.js";
|
|
14
16
|
import {
|
|
15
17
|
getConversation,
|
|
16
18
|
getMessages,
|
|
@@ -93,8 +95,6 @@ export interface GenerateTitleParams {
|
|
|
93
95
|
userMessage?: string;
|
|
94
96
|
/** Assistant response text (first turn). */
|
|
95
97
|
assistantResponse?: string;
|
|
96
|
-
/** Callback to emit title update events. */
|
|
97
|
-
onTitleUpdated?: (title: string) => void;
|
|
98
98
|
/** Abort signal. */
|
|
99
99
|
signal?: AbortSignal;
|
|
100
100
|
}
|
|
@@ -106,14 +106,8 @@ export interface GenerateTitleParams {
|
|
|
106
106
|
export async function generateAndPersistConversationTitle(
|
|
107
107
|
params: GenerateTitleParams,
|
|
108
108
|
): Promise<{ title: string; updated: boolean }> {
|
|
109
|
-
const {
|
|
110
|
-
|
|
111
|
-
context,
|
|
112
|
-
userMessage,
|
|
113
|
-
assistantResponse,
|
|
114
|
-
onTitleUpdated,
|
|
115
|
-
signal,
|
|
116
|
-
} = params;
|
|
109
|
+
const { conversationId, context, userMessage, assistantResponse, signal } =
|
|
110
|
+
params;
|
|
117
111
|
|
|
118
112
|
// Check current title is replaceable
|
|
119
113
|
const conversation = getConversation(conversationId);
|
|
@@ -127,7 +121,7 @@ export async function generateAndPersistConversationTitle(
|
|
|
127
121
|
// No provider available — fall back to context-derived title or untitled
|
|
128
122
|
const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
|
|
129
123
|
updateConversationTitle(conversationId, fallback, 1);
|
|
130
|
-
|
|
124
|
+
publishConversationTitleChanged(conversationId, fallback);
|
|
131
125
|
return { title: fallback, updated: true };
|
|
132
126
|
}
|
|
133
127
|
|
|
@@ -139,7 +133,7 @@ export async function generateAndPersistConversationTitle(
|
|
|
139
133
|
tools: [],
|
|
140
134
|
callSite: "conversationTitle",
|
|
141
135
|
signal,
|
|
142
|
-
timeoutMs:
|
|
136
|
+
timeoutMs: 15_000,
|
|
143
137
|
});
|
|
144
138
|
const title = normalizeTitle(result.text);
|
|
145
139
|
if (title) {
|
|
@@ -150,7 +144,7 @@ export async function generateAndPersistConversationTitle(
|
|
|
150
144
|
}
|
|
151
145
|
|
|
152
146
|
updateConversationTitle(conversationId, title, 1);
|
|
153
|
-
|
|
147
|
+
publishConversationTitleChanged(conversationId, title);
|
|
154
148
|
log.info({ conversationId, title }, "Auto-generated conversation title");
|
|
155
149
|
return { title, updated: true };
|
|
156
150
|
}
|
|
@@ -167,36 +161,61 @@ export async function generateAndPersistConversationTitle(
|
|
|
167
161
|
|
|
168
162
|
const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
|
|
169
163
|
updateConversationTitle(conversationId, fallback, 1);
|
|
170
|
-
|
|
164
|
+
publishConversationTitleChanged(conversationId, fallback);
|
|
171
165
|
return { title: fallback, updated: true };
|
|
172
166
|
}
|
|
173
167
|
|
|
168
|
+
// ── Serial title-generation queue ────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Each title generation makes an LLM call. Without serialization, burst
|
|
172
|
+
* conversation creation (e.g. 5 new chats in quick succession) fires N
|
|
173
|
+
* concurrent requests that can hit provider rate limits or contend for
|
|
174
|
+
* API capacity, causing later calls to time out and fall back to
|
|
175
|
+
* "Untitled Conversation".
|
|
176
|
+
*
|
|
177
|
+
* A serial queue ensures at most one title-generation LLM call is
|
|
178
|
+
* in-flight at a time. Each call is lightweight (~1–3 s for a ≤5-word
|
|
179
|
+
* title), so the added serial latency is modest and invisible to the
|
|
180
|
+
* user (the UI shows "Generating title…" as a placeholder during the
|
|
181
|
+
* wait). Both initial generation and second-pass regeneration share
|
|
182
|
+
* this queue since they hit the same provider.
|
|
183
|
+
*/
|
|
184
|
+
export const titleMutex = new Mutex();
|
|
185
|
+
|
|
174
186
|
/**
|
|
175
187
|
* Fire-and-forget wrapper for title generation. Failures are logged
|
|
176
188
|
* but do not propagate. On failure, replaces loading placeholder with
|
|
177
189
|
* a stable fallback title so loading state is never permanent.
|
|
190
|
+
*
|
|
191
|
+
* Calls are serialized via {@link titleMutex} so burst conversation
|
|
192
|
+
* creation does not overwhelm the LLM provider.
|
|
178
193
|
*/
|
|
179
194
|
export function queueGenerateConversationTitle(
|
|
180
195
|
params: GenerateTitleParams,
|
|
181
196
|
): void {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
void titleMutex
|
|
198
|
+
.withLock(async () => {
|
|
199
|
+
await generateAndPersistConversationTitle(params);
|
|
200
|
+
})
|
|
201
|
+
.catch((err) => {
|
|
202
|
+
log.warn(
|
|
203
|
+
{ err, conversationId: params.conversationId },
|
|
204
|
+
"Failed to generate conversation title (non-fatal)",
|
|
205
|
+
);
|
|
206
|
+
// Replace loading placeholder with stable fallback
|
|
207
|
+
try {
|
|
208
|
+
const conversation = getConversation(params.conversationId);
|
|
209
|
+
if (conversation && conversation.title === GENERATING_TITLE) {
|
|
210
|
+
const fallback =
|
|
211
|
+
deriveFallbackTitle(params.context) ?? UNTITLED_FALLBACK;
|
|
212
|
+
updateConversationTitle(params.conversationId, fallback);
|
|
213
|
+
publishConversationTitleChanged(params.conversationId, fallback);
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Best-effort
|
|
195
217
|
}
|
|
196
|
-
}
|
|
197
|
-
// Best-effort
|
|
198
|
-
}
|
|
199
|
-
});
|
|
218
|
+
});
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
// ── Title regeneration (second pass) ─────────────────────────────────
|
|
@@ -204,7 +223,6 @@ export function queueGenerateConversationTitle(
|
|
|
204
223
|
export interface RegenerateTitleParams {
|
|
205
224
|
conversationId: string;
|
|
206
225
|
provider?: Provider;
|
|
207
|
-
onTitleUpdated?: (title: string) => void;
|
|
208
226
|
signal?: AbortSignal;
|
|
209
227
|
}
|
|
210
228
|
|
|
@@ -216,7 +234,7 @@ export interface RegenerateTitleParams {
|
|
|
216
234
|
export async function regenerateConversationTitle(
|
|
217
235
|
params: RegenerateTitleParams,
|
|
218
236
|
): Promise<{ title: string; updated: boolean }> {
|
|
219
|
-
const { conversationId,
|
|
237
|
+
const { conversationId, signal } = params;
|
|
220
238
|
|
|
221
239
|
const conversation = getConversation(conversationId);
|
|
222
240
|
if (!conversation || !conversation.isAutoTitle) {
|
|
@@ -249,7 +267,7 @@ export async function regenerateConversationTitle(
|
|
|
249
267
|
tools: [],
|
|
250
268
|
callSite: "conversationTitle",
|
|
251
269
|
signal,
|
|
252
|
-
timeoutMs:
|
|
270
|
+
timeoutMs: 15_000,
|
|
253
271
|
});
|
|
254
272
|
const title = normalizeTitle(result.text);
|
|
255
273
|
if (title) {
|
|
@@ -260,7 +278,7 @@ export async function regenerateConversationTitle(
|
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
updateConversationTitle(conversationId, title, 1);
|
|
263
|
-
|
|
281
|
+
publishConversationTitleChanged(conversationId, title);
|
|
264
282
|
log.info(
|
|
265
283
|
{ conversationId, title },
|
|
266
284
|
"Re-generated conversation title (second pass)",
|
|
@@ -273,16 +291,22 @@ export async function regenerateConversationTitle(
|
|
|
273
291
|
|
|
274
292
|
/**
|
|
275
293
|
* Fire-and-forget wrapper for title regeneration.
|
|
294
|
+
*
|
|
295
|
+
* Serialized via the same {@link titleMutex} as initial generation.
|
|
276
296
|
*/
|
|
277
297
|
export function queueRegenerateConversationTitle(
|
|
278
298
|
params: RegenerateTitleParams,
|
|
279
299
|
): void {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
285
|
-
|
|
300
|
+
void titleMutex
|
|
301
|
+
.withLock(async () => {
|
|
302
|
+
await regenerateConversationTitle(params);
|
|
303
|
+
})
|
|
304
|
+
.catch((err) => {
|
|
305
|
+
log.warn(
|
|
306
|
+
{ err, conversationId: params.conversationId },
|
|
307
|
+
"Failed to regenerate conversation title (non-fatal)",
|
|
308
|
+
);
|
|
309
|
+
});
|
|
286
310
|
}
|
|
287
311
|
|
|
288
312
|
// ── Internal helpers ─────────────────────────────────────────────────
|
package/src/memory/db-init.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
addCoreColumns,
|
|
19
19
|
createApprovalPromptTsTrackerTable,
|
|
20
20
|
createAssistantInboxTables,
|
|
21
|
+
createAuthFallbackEventsTable,
|
|
21
22
|
createCallSessionsTables,
|
|
22
23
|
createCanonicalGuardianTables,
|
|
23
24
|
createChannelGuardianTables,
|
|
@@ -188,6 +189,7 @@ import {
|
|
|
188
189
|
migrateScheduleReuseConversation,
|
|
189
190
|
migrateScheduleScriptColumn,
|
|
190
191
|
migrateScheduleScriptTimeout,
|
|
192
|
+
migrateScheduleSourceConversation,
|
|
191
193
|
migrateScheduleWakeConversationId,
|
|
192
194
|
migrateSchemaIndexesAndColumns,
|
|
193
195
|
migrateScrubCorruptedImageAttachments,
|
|
@@ -473,7 +475,9 @@ export function initializeDb(): void {
|
|
|
473
475
|
migrateLlmUsageEventsAddAssistantVersion,
|
|
474
476
|
migrateAddMemoryV3Selections,
|
|
475
477
|
migrateScheduleScriptTimeout,
|
|
478
|
+
migrateScheduleSourceConversation,
|
|
476
479
|
migrateMessagesRoleCreatedAtIndex,
|
|
480
|
+
createAuthFallbackEventsTable,
|
|
477
481
|
];
|
|
478
482
|
|
|
479
483
|
// Run each migration step, catching and logging individual failures so one
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the live, per-conversation {@link ConversationGraphMemory} registry
|
|
3
|
+
* (`getLiveGraphMemory`). The registry lets memory-domain code that only knows
|
|
4
|
+
* a conversation id — notably the post-compaction re-injection hook — reach the
|
|
5
|
+
* same in-memory handle the turn's retrieval mutated, without the agent loop
|
|
6
|
+
* threading the handle through its generic context.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { createMockLoggerModule } from "../../../__tests__/helpers/mock-logger.js";
|
|
11
|
+
|
|
12
|
+
mock.module("../../../util/logger.js", () => createMockLoggerModule());
|
|
13
|
+
|
|
14
|
+
const { ConversationGraphMemory, getLiveGraphMemory } =
|
|
15
|
+
await import("../conversation-graph-memory.js");
|
|
16
|
+
|
|
17
|
+
describe("ConversationGraphMemory live registry", () => {
|
|
18
|
+
test("a constructed handle is discoverable by its conversation id", () => {
|
|
19
|
+
/**
|
|
20
|
+
* Tests that constructing a handle registers it so it can be looked up.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// GIVEN a conversation id
|
|
24
|
+
const conversationId = `conv-registry-${crypto.randomUUID()}`;
|
|
25
|
+
|
|
26
|
+
// WHEN a graph handle is constructed for it
|
|
27
|
+
const handle = new ConversationGraphMemory(conversationId);
|
|
28
|
+
|
|
29
|
+
// THEN the registry returns that exact instance
|
|
30
|
+
expect(getLiveGraphMemory(conversationId)).toBe(handle);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("dispose removes the handle from the registry", () => {
|
|
34
|
+
/**
|
|
35
|
+
* Tests that disposing a handle unregisters it.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// GIVEN a registered graph handle
|
|
39
|
+
const conversationId = `conv-registry-${crypto.randomUUID()}`;
|
|
40
|
+
const handle = new ConversationGraphMemory(conversationId);
|
|
41
|
+
|
|
42
|
+
// WHEN it is disposed
|
|
43
|
+
handle.dispose();
|
|
44
|
+
|
|
45
|
+
// THEN the registry no longer resolves the conversation id
|
|
46
|
+
expect(getLiveGraphMemory(conversationId)).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("missing id and undefined id both resolve to undefined", () => {
|
|
50
|
+
/**
|
|
51
|
+
* Tests the lookup's absence handling for unknown and undefined keys.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// GIVEN no handle registered for these keys
|
|
55
|
+
// WHEN the registry is queried with an unknown id and with undefined
|
|
56
|
+
// THEN both resolve to undefined (the hook treats absence as "no graph")
|
|
57
|
+
expect(
|
|
58
|
+
getLiveGraphMemory(`conv-never-${crypto.randomUUID()}`),
|
|
59
|
+
).toBeUndefined();
|
|
60
|
+
expect(getLiveGraphMemory(undefined)).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("recreating for the same id replaces the registered handle", () => {
|
|
64
|
+
/**
|
|
65
|
+
* Tests that the latest constructed handle wins for a conversation id.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
// GIVEN a handle already registered for a conversation id
|
|
69
|
+
const conversationId = `conv-registry-${crypto.randomUUID()}`;
|
|
70
|
+
new ConversationGraphMemory(conversationId);
|
|
71
|
+
|
|
72
|
+
// WHEN a second handle is constructed for the same id
|
|
73
|
+
const recreated = new ConversationGraphMemory(conversationId);
|
|
74
|
+
|
|
75
|
+
// THEN the registry resolves to the most recent instance
|
|
76
|
+
expect(getLiveGraphMemory(conversationId)).toBe(recreated);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("disposing a stale handle does not evict the current one", () => {
|
|
80
|
+
/**
|
|
81
|
+
* Tests the dispose guard during an eviction + recreation race: a stale
|
|
82
|
+
* handle must not delete the live entry that now points at a newer handle.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
// GIVEN a superseded (stale) handle and the current handle for one id
|
|
86
|
+
const conversationId = `conv-registry-${crypto.randomUUID()}`;
|
|
87
|
+
const stale = new ConversationGraphMemory(conversationId);
|
|
88
|
+
const current = new ConversationGraphMemory(conversationId);
|
|
89
|
+
|
|
90
|
+
// WHEN the stale handle is disposed
|
|
91
|
+
stale.dispose();
|
|
92
|
+
|
|
93
|
+
// THEN the registry still resolves to the current handle
|
|
94
|
+
expect(getLiveGraphMemory(conversationId)).toBe(current);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("records and exposes the PKB query vector pair from the registry", () => {
|
|
98
|
+
/**
|
|
99
|
+
* Tests that the dense/sparse PKB query pair recorded during retrieval is
|
|
100
|
+
* readable off the same live handle the PKB-reminder injector looks up.
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
// GIVEN a registered graph handle with no recorded vectors yet
|
|
104
|
+
const conversationId = `conv-registry-${crypto.randomUUID()}`;
|
|
105
|
+
const handle = new ConversationGraphMemory(conversationId);
|
|
106
|
+
expect(handle.pkbQueryVector).toBeUndefined();
|
|
107
|
+
expect(handle.pkbSparseVector).toBeUndefined();
|
|
108
|
+
|
|
109
|
+
// WHEN a retrieval records the turn's dense/sparse pair
|
|
110
|
+
const dense = [0.1, 0.2, 0.3];
|
|
111
|
+
const sparse = { indices: [0, 2], values: [0.5, 0.9] };
|
|
112
|
+
handle.recordPkbQueryVectors(dense, sparse);
|
|
113
|
+
|
|
114
|
+
// THEN a registry consumer reads back the same pair by conversation id
|
|
115
|
+
const looked = getLiveGraphMemory(conversationId);
|
|
116
|
+
expect(looked?.pkbQueryVector).toBe(dense);
|
|
117
|
+
expect(looked?.pkbSparseVector).toBe(sparse);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -62,6 +62,31 @@ const ESTIMATED_IMAGE_TOKENS = 1000;
|
|
|
62
62
|
// Per-conversation state
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Registry of the live, per-conversation graph handles keyed by conversation
|
|
67
|
+
* id. A handle registers itself on construction and removes itself on
|
|
68
|
+
* {@link ConversationGraphMemory.dispose}, so memory-domain code that only
|
|
69
|
+
* knows a conversation id (e.g. the post-compaction re-injection hook) can
|
|
70
|
+
* reach the same in-memory handle the turn's retrieval used — its live
|
|
71
|
+
* `tracker` / cached-node state, which a DB-reconstructed handle would not
|
|
72
|
+
* carry. Not a general service locator: it holds only the graph handle, and
|
|
73
|
+
* the daemon's `Conversation` remains the owner of the instance's lifecycle.
|
|
74
|
+
*/
|
|
75
|
+
const liveByConversation = new Map<string, ConversationGraphMemory>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Look up the live {@link ConversationGraphMemory} for a conversation, or
|
|
79
|
+
* `undefined` when none is registered (no active conversation, or a context
|
|
80
|
+
* with no conversation id). Returns the same instance the turn's retrieval
|
|
81
|
+
* mutated, so cached-node re-tracking operates on real state.
|
|
82
|
+
*/
|
|
83
|
+
export function getLiveGraphMemory(
|
|
84
|
+
conversationId: string | undefined,
|
|
85
|
+
): ConversationGraphMemory | undefined {
|
|
86
|
+
if (!conversationId) return undefined;
|
|
87
|
+
return liveByConversation.get(conversationId);
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
/**
|
|
66
91
|
* Manages memory graph state for a single conversation.
|
|
67
92
|
* Create one per Conversation instance. Persists across turns.
|
|
@@ -75,9 +100,24 @@ export class ConversationGraphMemory {
|
|
|
75
100
|
private lastInjectedBlock: string | null = null;
|
|
76
101
|
private lastInjectedNodeIds: string[] = [];
|
|
77
102
|
private lastInjectedImages: Map<string, ResolvedImage> = new Map();
|
|
103
|
+
private lastPkbQueryVector: number[] | undefined;
|
|
104
|
+
private lastPkbSparseVector: QdrantSparseVector | undefined;
|
|
78
105
|
|
|
79
106
|
constructor(conversationId: string) {
|
|
80
107
|
this.conversationId = conversationId;
|
|
108
|
+
liveByConversation.set(conversationId, this);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove this handle from the live registry. Called from
|
|
113
|
+
* `Conversation.dispose`. Guards against clobbering a newer handle for the
|
|
114
|
+
* same conversation (eviction + recreation) by only deleting the entry when
|
|
115
|
+
* it still points at this instance.
|
|
116
|
+
*/
|
|
117
|
+
dispose(): void {
|
|
118
|
+
if (liveByConversation.get(this.conversationId) === this) {
|
|
119
|
+
liveByConversation.delete(this.conversationId);
|
|
120
|
+
}
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
/**
|
|
@@ -305,6 +345,31 @@ export class ConversationGraphMemory {
|
|
|
305
345
|
this.tracker.add(this.lastInjectedNodeIds);
|
|
306
346
|
}
|
|
307
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Record the dense/sparse query-vector pair this turn's retrieval produced
|
|
350
|
+
* for PKB hybrid search. The PKB-reminder injector reuses the same
|
|
351
|
+
* embedding (looked up by conversation id via {@link getLiveGraphMemory})
|
|
352
|
+
* rather than receiving it threaded through the agent loop, so the vectors
|
|
353
|
+
* stay owned by the memory-retrieval domain that computes them.
|
|
354
|
+
*/
|
|
355
|
+
recordPkbQueryVectors(
|
|
356
|
+
dense: number[] | undefined,
|
|
357
|
+
sparse: QdrantSparseVector | undefined,
|
|
358
|
+
): void {
|
|
359
|
+
this.lastPkbQueryVector = dense;
|
|
360
|
+
this.lastPkbSparseVector = sparse;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Dense PKB query vector from this turn's retrieval, or `undefined`. */
|
|
364
|
+
get pkbQueryVector(): number[] | undefined {
|
|
365
|
+
return this.lastPkbQueryVector;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Sparse PKB query vector paired with {@link pkbQueryVector}. */
|
|
369
|
+
get pkbSparseVector(): QdrantSparseVector | undefined {
|
|
370
|
+
return this.lastPkbSparseVector;
|
|
371
|
+
}
|
|
372
|
+
|
|
308
373
|
/**
|
|
309
374
|
* Main entry point — called on every turn before the LLM sees the messages.
|
|
310
375
|
*
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -79,6 +79,11 @@ export const SLOW_LLM_JOB_TYPES: MemoryJobType[] = [
|
|
|
79
79
|
"graph_bootstrap",
|
|
80
80
|
];
|
|
81
81
|
|
|
82
|
+
export const MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS = {
|
|
83
|
+
automatic: "automatic",
|
|
84
|
+
manual: "manual",
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
82
87
|
/** Returns `false` only when `config.memory.enabled` is explicitly `false`; defaults to `true` on missing config or load errors. */
|
|
83
88
|
export function isMemoryEnabled(): boolean {
|
|
84
89
|
try {
|
|
@@ -337,6 +342,34 @@ export function hasActiveJobOfType(type: MemoryJobType): boolean {
|
|
|
337
342
|
);
|
|
338
343
|
}
|
|
339
344
|
|
|
345
|
+
export function isAutomaticConsolidationJob(job: MemoryJob): boolean {
|
|
346
|
+
return job.payload.trigger !== MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS.manual;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function cancelPendingAutomaticConsolidationJobs(): number {
|
|
350
|
+
const db = getDb();
|
|
351
|
+
db.update(memoryJobs)
|
|
352
|
+
.set({
|
|
353
|
+
status: "failed",
|
|
354
|
+
lastError: "automatic_consolidation_disabled",
|
|
355
|
+
updatedAt: Date.now(),
|
|
356
|
+
})
|
|
357
|
+
.where(
|
|
358
|
+
and(
|
|
359
|
+
eq(memoryJobs.type, "memory_v2_consolidate"),
|
|
360
|
+
eq(memoryJobs.status, "pending"),
|
|
361
|
+
or(
|
|
362
|
+
sql`json_extract(${memoryJobs.payload}, '$.trigger') = ${MEMORY_V2_CONSOLIDATION_JOB_TRIGGERS.automatic}`,
|
|
363
|
+
// Legacy rows predate trigger markers and are indistinguishable from
|
|
364
|
+
// automatic enqueues, so disabling the schedule treats them as auto.
|
|
365
|
+
sql`json_extract(${memoryJobs.payload}, '$.trigger') IS NULL`,
|
|
366
|
+
),
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
.run();
|
|
370
|
+
return rawChanges();
|
|
371
|
+
}
|
|
372
|
+
|
|
340
373
|
export function enqueuePruneOldLlmRequestLogsJob(retentionMs?: number): string {
|
|
341
374
|
const db = getDb();
|
|
342
375
|
const existing = db
|