@vellumai/assistant 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -7
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +5 -0
- package/docker-init-apt-root.sh +94 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +325 -3
- package/package.json +3 -1
- package/scripts/generate-openapi.ts +83 -10
- package/scripts/sync-llm-catalog.ts +2 -2
- package/scripts/sync-web-search-catalog.ts +47 -25
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +45 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +21 -0
- package/src/__tests__/config-set-route.test.ts +80 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/context-search-conversations-source.test.ts +117 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +1 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-error.test.ts +42 -3
- package/src/__tests__/conversation-fork-crud.test.ts +82 -0
- package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
- package/src/__tests__/conversation-lifecycle.test.ts +173 -0
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +4 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +3 -2
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +60 -36
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +73 -0
- package/src/__tests__/host-app-control-proxy.test.ts +266 -10
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
- package/src/__tests__/inference-profile-reaper.test.ts +4 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
- package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
- package/src/__tests__/injector-chain.test.ts +10 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-catalog-parity.test.ts +55 -13
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +31 -29
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/oauth-commands-routes.test.ts +168 -16
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/openai-provider.test.ts +24 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +10 -36
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +0 -1
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
- package/src/__tests__/plugin-types.test.ts +3 -13
- package/src/__tests__/process-message-background-slack.test.ts +8 -1
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-routes.test.ts +50 -3
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
- package/src/__tests__/skill-load-tool.test.ts +27 -89
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/system-prompt.test.ts +737 -63
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/user-plugin-loader.test.ts +0 -7
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/types.ts +108 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/commands/__tests__/schedules.test.ts +491 -0
- package/src/cli/commands/changelog.ts +106 -42
- package/src/cli/commands/conversations.ts +102 -17
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/notifications.ts +329 -317
- package/src/cli/commands/plugins.ts +185 -0
- package/src/cli/commands/schedules.ts +391 -0
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/install-from-github.ts +304 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +38 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -0
- package/src/config/feature-flag-registry.json +41 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +7 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/channels.ts +8 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1047 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/context/window-manager.ts +197 -1520
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
- package/src/daemon/approval-generators.ts +8 -6
- package/src/daemon/config-watcher.ts +94 -31
- package/src/daemon/conversation-agent-loop.ts +169 -9
- package/src/daemon/conversation-error.ts +171 -37
- package/src/daemon/conversation-lifecycle.ts +53 -40
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +49 -12
- package/src/daemon/conversation-runtime-assembly.ts +16 -1
- package/src/daemon/conversation-slash.ts +12 -5
- package/src/daemon/conversation-store.ts +11 -4
- package/src/daemon/conversation-tool-setup.ts +39 -7
- package/src/daemon/conversation.ts +33 -1
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +111 -108
- package/src/daemon/history-repair.ts +28 -1
- package/src/daemon/host-app-control-proxy.ts +98 -23
- package/src/daemon/lifecycle.ts +45 -35
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +49 -0
- package/src/daemon/message-protocol.ts +1 -0
- package/src/daemon/message-types/conversations.ts +25 -0
- package/src/daemon/message-types/messages.ts +61 -0
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pkb-reminder-builder.test.ts +1 -1
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +21 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +29 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +41 -0
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +22 -0
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
- package/src/memory/__tests__/conversation-queries.test.ts +263 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +42 -10
- package/src/memory/context-search/sources/conversations.ts +62 -2
- package/src/memory/context-search/sources/workspace.ts +4 -0
- package/src/memory/conversation-crud.ts +63 -19
- package/src/memory/conversation-queries.ts +110 -10
- package/src/memory/db-init.ts +6 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +4 -4
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
- package/src/memory/graph/conversation-graph-memory.ts +31 -15
- package/src/memory/graph/tools.ts +3 -3
- package/src/memory/indexer.ts +34 -29
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
- package/src/memory/jobs/embed-concept-page.ts +20 -11
- package/src/memory/jobs-worker.ts +6 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
- package/src/memory/message-content.ts +1 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/inference.ts +1 -3
- package/src/memory/schema/infrastructure.ts +12 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/injection.test.ts +98 -8
- package/src/memory/v2/__tests__/migration.test.ts +87 -0
- package/src/memory/v2/__tests__/page-index.test.ts +83 -0
- package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
- package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
- package/src/memory/v2/__tests__/router.test.ts +15 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
- package/src/memory/v2/injection.ts +32 -6
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +35 -5
- package/src/memory/v2/prompts/router.ts +11 -8
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +135 -7
- package/src/memory/v2/router.ts +9 -8
- package/src/memory/v2/skill-store.ts +120 -35
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/decision-engine.ts +2 -1
- package/src/notifications/emit-signal.ts +20 -1
- package/src/notifications/home-feed-side-effect.ts +54 -0
- package/src/notifications/signal.ts +3 -1
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.ts +6 -2
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +34 -1
- package/src/plugin-api/types.ts +104 -22
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +36 -3
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +105 -32
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +31 -26
- package/src/plugins/user-loader.ts +21 -2
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +37 -5
- package/src/prompts/__tests__/system-prompt.test.ts +12 -0
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -166
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +173 -0
- package/src/providers/__tests__/inference.test.ts +22 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/connection-resolution.ts +7 -0
- package/src/providers/inference/adapter-factory.ts +41 -4
- package/src/providers/inference/connections.ts +74 -29
- package/src/providers/inference/resolve-auth.ts +12 -4
- package/src/providers/model-catalog.ts +294 -12
- package/src/providers/openai/chat-completions-provider.ts +10 -2
- package/src/providers/openrouter/client.ts +7 -0
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +36 -0
- package/src/providers/registry.ts +22 -14
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +42 -14
- package/src/runtime/auth/route-policy.ts +8 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-types.ts +19 -0
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +5 -3
- package/src/runtime/routes/auth-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +5 -3
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +121 -0
- package/src/runtime/routes/conversation-cli-routes.ts +44 -3
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +17 -42
- package/src/runtime/routes/conversation-query-routes.ts +40 -35
- package/src/runtime/routes/conversation-routes.ts +90 -11
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
- package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
- package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +6 -13
- package/src/runtime/routes/notification-routes.ts +1 -1
- package/src/runtime/routes/oauth-commands-routes.ts +105 -15
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +4 -7
- package/src/runtime/routes/subagents-routes.ts +57 -18
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +27 -2
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +93 -1
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +9 -1
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +15 -7
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/browser-execution.ts +15 -11
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +72 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/watcher/__tests__/engine.test.ts +22 -0
- package/src/watcher/engine.ts +6 -2
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +39 -9
- package/src/workspace/migrations/types.ts +4 -0
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-window-manager.test.ts +0 -2481
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* - It swallows errors from the embedding backend — the function resolves
|
|
16
16
|
* and the cache is unchanged from prior state.
|
|
17
17
|
*
|
|
18
|
-
* Hermetic by design: the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Hermetic by design: the embedding backend, Qdrant module, and feature-flag
|
|
19
|
+
* resolver are module-mocked so the suite never reaches a real backend. One
|
|
20
|
+
* regression case uses a temp workspace to exercise disk-discovered skills.
|
|
21
21
|
*/
|
|
22
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
22
25
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
23
26
|
|
|
24
27
|
import { makeMockLogger } from "../../../__tests__/helpers/mock-logger.js";
|
|
@@ -48,9 +51,15 @@ interface PruneCall {
|
|
|
48
51
|
options?: { kind?: string };
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
interface BackfillCall {
|
|
55
|
+
prefix: string;
|
|
56
|
+
kind: string;
|
|
57
|
+
allowedSuffixes: ReadonlySet<string>;
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
interface TestState {
|
|
52
|
-
catalog: SkillSummary[];
|
|
53
|
-
resolved: ResolvedSkill[];
|
|
61
|
+
catalog: SkillSummary[] | null;
|
|
62
|
+
resolved: ResolvedSkill[] | null;
|
|
54
63
|
fullCatalog: CatalogSkill[];
|
|
55
64
|
fullCatalogThrows: Error | null;
|
|
56
65
|
flagsEnabled: Record<string, boolean>;
|
|
@@ -60,6 +69,10 @@ interface TestState {
|
|
|
60
69
|
upsertCalls: UpsertCall[];
|
|
61
70
|
pruneCalls: PruneCall[];
|
|
62
71
|
upsertThrows: Error | null;
|
|
72
|
+
backfillCalls: BackfillCall[];
|
|
73
|
+
backfillReturn: number;
|
|
74
|
+
backfillThrows: Error | null;
|
|
75
|
+
callSequence: Array<"upsert" | "prune" | "backfill">;
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
const state: TestState = {
|
|
@@ -74,6 +87,10 @@ const state: TestState = {
|
|
|
74
87
|
upsertCalls: [],
|
|
75
88
|
pruneCalls: [],
|
|
76
89
|
upsertThrows: null,
|
|
90
|
+
backfillCalls: [],
|
|
91
|
+
backfillReturn: 0,
|
|
92
|
+
backfillThrows: null,
|
|
93
|
+
callSequence: [],
|
|
77
94
|
};
|
|
78
95
|
|
|
79
96
|
// Stub config so resolveSkillStates / mcp augmentation have something to read.
|
|
@@ -83,16 +100,39 @@ mock.module("../../../config/loader.js", () => ({
|
|
|
83
100
|
qdrant: { url: "http://127.0.0.1:6333", vectorSize: 3, onDisk: false },
|
|
84
101
|
},
|
|
85
102
|
mcp: { servers: {} },
|
|
86
|
-
skills: { entries: {}, allowBundled:
|
|
103
|
+
skills: { entries: {}, allowBundled: [] },
|
|
87
104
|
}),
|
|
88
105
|
}));
|
|
89
106
|
|
|
90
107
|
mock.module("../../../config/skills.js", () => ({
|
|
91
|
-
loadSkillCatalog: () => state.catalog,
|
|
108
|
+
loadSkillCatalog: () => state.catalog ?? [],
|
|
92
109
|
}));
|
|
93
110
|
|
|
94
111
|
mock.module("../../../config/skill-state.js", () => ({
|
|
95
|
-
resolveSkillStates: (
|
|
112
|
+
resolveSkillStates: (
|
|
113
|
+
catalog: SkillSummary[],
|
|
114
|
+
config: { skills?: { allowBundled?: string[] | null } },
|
|
115
|
+
) => {
|
|
116
|
+
if (state.resolved) return state.resolved;
|
|
117
|
+
return catalog
|
|
118
|
+
.filter((summary) => {
|
|
119
|
+
const allowBundled = config.skills?.allowBundled;
|
|
120
|
+
return !(
|
|
121
|
+
summary.source === "bundled" &&
|
|
122
|
+
allowBundled != null &&
|
|
123
|
+
!allowBundled.includes(summary.id)
|
|
124
|
+
);
|
|
125
|
+
})
|
|
126
|
+
.map((summary) => ({
|
|
127
|
+
summary,
|
|
128
|
+
state:
|
|
129
|
+
summary.source === "managed" ||
|
|
130
|
+
summary.source === "bundled" ||
|
|
131
|
+
summary.source === "plugin"
|
|
132
|
+
? "enabled"
|
|
133
|
+
: "disabled",
|
|
134
|
+
}));
|
|
135
|
+
},
|
|
96
136
|
}));
|
|
97
137
|
|
|
98
138
|
mock.module("../../../config/assistant-feature-flags.js", () => ({
|
|
@@ -115,6 +155,7 @@ mock.module("../../embedding-backend.js", () => ({
|
|
|
115
155
|
mock.module("../qdrant.js", () => ({
|
|
116
156
|
upsertConceptPageEmbedding: async (params: UpsertCall) => {
|
|
117
157
|
if (state.upsertThrows) throw state.upsertThrows;
|
|
158
|
+
state.callSequence.push("upsert");
|
|
118
159
|
state.upsertCalls.push(params);
|
|
119
160
|
},
|
|
120
161
|
pruneSlugsWithPrefixExcept: async (
|
|
@@ -122,8 +163,19 @@ mock.module("../qdrant.js", () => ({
|
|
|
122
163
|
activeSuffixes: readonly string[],
|
|
123
164
|
options?: { kind?: string },
|
|
124
165
|
) => {
|
|
166
|
+
state.callSequence.push("prune");
|
|
125
167
|
state.pruneCalls.push({ prefix, activeSuffixes, options });
|
|
126
168
|
},
|
|
169
|
+
backfillKindOnPointsWithPrefix: async (
|
|
170
|
+
prefix: string,
|
|
171
|
+
kind: string,
|
|
172
|
+
allowedSuffixes: ReadonlySet<string>,
|
|
173
|
+
) => {
|
|
174
|
+
if (state.backfillThrows) throw state.backfillThrows;
|
|
175
|
+
state.callSequence.push("backfill");
|
|
176
|
+
state.backfillCalls.push({ prefix, kind, allowedSuffixes });
|
|
177
|
+
return state.backfillReturn;
|
|
178
|
+
},
|
|
127
179
|
}));
|
|
128
180
|
|
|
129
181
|
mock.module("../../../skills/catalog-cache.js", () => ({
|
|
@@ -170,6 +222,10 @@ function resetState(): void {
|
|
|
170
222
|
state.upsertCalls.length = 0;
|
|
171
223
|
state.pruneCalls.length = 0;
|
|
172
224
|
state.upsertThrows = null;
|
|
225
|
+
state.backfillCalls.length = 0;
|
|
226
|
+
state.backfillReturn = 0;
|
|
227
|
+
state.backfillThrows = null;
|
|
228
|
+
state.callSequence.length = 0;
|
|
173
229
|
_resetSkillStoreForTests();
|
|
174
230
|
}
|
|
175
231
|
|
|
@@ -390,6 +446,154 @@ describe("seedV2SkillEntries", () => {
|
|
|
390
446
|
expect(getSkillCapability("skills/unknown-skill")).toBeNull();
|
|
391
447
|
});
|
|
392
448
|
|
|
449
|
+
test("skips stale in-flight seed results when a newer refresh is requested", async () => {
|
|
450
|
+
const skillA = makeSummary({
|
|
451
|
+
id: "example-skill-a",
|
|
452
|
+
displayName: "Skill A",
|
|
453
|
+
});
|
|
454
|
+
const skillB = makeSummary({
|
|
455
|
+
id: "example-skill-b",
|
|
456
|
+
displayName: "Skill B",
|
|
457
|
+
});
|
|
458
|
+
state.catalog = [skillA];
|
|
459
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
460
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
461
|
+
|
|
462
|
+
const firstSeed = seedV2SkillEntries();
|
|
463
|
+
state.catalog = [skillB];
|
|
464
|
+
state.resolved = [{ summary: skillB, state: "enabled" }];
|
|
465
|
+
const secondSeed = seedV2SkillEntries();
|
|
466
|
+
|
|
467
|
+
await Promise.all([firstSeed, secondSeed]);
|
|
468
|
+
|
|
469
|
+
expect(state.upsertCalls.map((call) => call.slug)).toEqual([
|
|
470
|
+
"skills/example-skill-b",
|
|
471
|
+
]);
|
|
472
|
+
expect(getSkillCapability("example-skill-a")).toBeNull();
|
|
473
|
+
expect(getSkillCapability("example-skill-b")?.content).toContain("Skill B");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("continues draining when waiter continuations enqueue additional generations", async () => {
|
|
477
|
+
const skillA = makeSummary({
|
|
478
|
+
id: "example-skill-a",
|
|
479
|
+
displayName: "Skill A",
|
|
480
|
+
});
|
|
481
|
+
const skillB = makeSummary({
|
|
482
|
+
id: "example-skill-b",
|
|
483
|
+
displayName: "Skill B",
|
|
484
|
+
});
|
|
485
|
+
const skillC = makeSummary({
|
|
486
|
+
id: "example-skill-c",
|
|
487
|
+
displayName: "Skill C",
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
function useSkill(skill: SkillSummary, dense: number[]): void {
|
|
491
|
+
state.catalog = [skill];
|
|
492
|
+
state.resolved = [{ summary: skill, state: "enabled" }];
|
|
493
|
+
state.embedReturn = [dense];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
useSkill(skillA, [0.1, 0.2, 0.3]);
|
|
497
|
+
const firstSeed = seedV2SkillEntries();
|
|
498
|
+
const secondSeed = firstSeed.then(() => {
|
|
499
|
+
useSkill(skillB, [0.4, 0.5, 0.6]);
|
|
500
|
+
return seedV2SkillEntries();
|
|
501
|
+
});
|
|
502
|
+
const thirdSeed = secondSeed.then(() => {
|
|
503
|
+
useSkill(skillC, [0.7, 0.8, 0.9]);
|
|
504
|
+
return seedV2SkillEntries();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
508
|
+
try {
|
|
509
|
+
await expect(
|
|
510
|
+
Promise.race([
|
|
511
|
+
Promise.all([firstSeed, secondSeed, thirdSeed]),
|
|
512
|
+
new Promise<never>((_, reject) => {
|
|
513
|
+
timeout = setTimeout(
|
|
514
|
+
() => reject(new Error("seed queue stalled")),
|
|
515
|
+
500,
|
|
516
|
+
);
|
|
517
|
+
}),
|
|
518
|
+
]),
|
|
519
|
+
).resolves.toBeDefined();
|
|
520
|
+
} finally {
|
|
521
|
+
if (timeout) clearTimeout(timeout);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
expect(state.upsertCalls.map((call) => call.slug)).toEqual([
|
|
525
|
+
"skills/example-skill-a",
|
|
526
|
+
"skills/example-skill-b",
|
|
527
|
+
"skills/example-skill-c",
|
|
528
|
+
]);
|
|
529
|
+
expect(getSkillCapability("example-skill-a")).toBeNull();
|
|
530
|
+
expect(getSkillCapability("example-skill-b")).toBeNull();
|
|
531
|
+
expect(getSkillCapability("example-skill-c")?.content).toContain("Skill C");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("seeds disk-discovered managed skills omitted from a stale SKILLS.md index", async () => {
|
|
535
|
+
const workspaceDir = mkdtempSync(join(tmpdir(), "skill-store-index-"));
|
|
536
|
+
state.resolved = null;
|
|
537
|
+
state.embedReturn = [[0.7, 0.8, 0.9]];
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const skillsDir = join(workspaceDir, "skills");
|
|
541
|
+
const skillDir = join(skillsDir, "geo-article-writer");
|
|
542
|
+
mkdirSync(skillDir, { recursive: true });
|
|
543
|
+
writeFileSync(join(skillsDir, "SKILLS.md"), "- stale-only\n", "utf-8");
|
|
544
|
+
writeFileSync(
|
|
545
|
+
join(skillDir, "SKILL.md"),
|
|
546
|
+
`---
|
|
547
|
+
name: "Geo Article Writer"
|
|
548
|
+
description: "Writes local geo articles"
|
|
549
|
+
metadata:
|
|
550
|
+
vellum:
|
|
551
|
+
activation-hints:
|
|
552
|
+
- user asks for local article drafts
|
|
553
|
+
avoid-when:
|
|
554
|
+
- user only wants citation extraction
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
Write a local article draft.
|
|
558
|
+
`,
|
|
559
|
+
"utf-8",
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
state.catalog = [
|
|
563
|
+
{
|
|
564
|
+
id: "geo-article-writer",
|
|
565
|
+
name: "Geo Article Writer",
|
|
566
|
+
displayName: "Geo Article Writer",
|
|
567
|
+
description: "Writes local geo articles",
|
|
568
|
+
directoryPath: skillDir,
|
|
569
|
+
skillFilePath: join(skillDir, "SKILL.md"),
|
|
570
|
+
source: "managed",
|
|
571
|
+
activationHints: ["user asks for local article drafts"],
|
|
572
|
+
avoidWhen: ["user only wants citation extraction"],
|
|
573
|
+
},
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
await seedV2SkillEntries();
|
|
577
|
+
} finally {
|
|
578
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
expect(state.upsertCalls).toHaveLength(1);
|
|
582
|
+
expect(state.upsertCalls[0].slug).toBe("skills/geo-article-writer");
|
|
583
|
+
|
|
584
|
+
const entry = getSkillCapability("geo-article-writer");
|
|
585
|
+
expect(entry).not.toBeNull();
|
|
586
|
+
expect(entry?.id).toBe("geo-article-writer");
|
|
587
|
+
expect(entry?.content).toContain('The "Geo Article Writer" skill');
|
|
588
|
+
expect(entry?.content).toContain("Writes local geo articles");
|
|
589
|
+
expect(entry?.content).toContain(
|
|
590
|
+
"Use when: user asks for local article drafts.",
|
|
591
|
+
);
|
|
592
|
+
expect(entry?.content).toContain(
|
|
593
|
+
"Avoid when: user only wants citation extraction.",
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
|
|
393
597
|
test("swallows errors from embedWithBackend and leaves prior cache intact", async () => {
|
|
394
598
|
const skillA = makeSummary({ id: "example-skill-a" });
|
|
395
599
|
state.catalog = [skillA];
|
|
@@ -444,6 +648,118 @@ describe("seedV2SkillEntries", () => {
|
|
|
444
648
|
expect([...state.pruneCalls[0].activeSuffixes]).toEqual(["remote-only"]);
|
|
445
649
|
});
|
|
446
650
|
|
|
651
|
+
test("passes kind: 'skill' to upsert and prune so legacy skill rows stay scoped to the skill kind", async () => {
|
|
652
|
+
const skillA = makeSummary({ id: "example-skill-a" });
|
|
653
|
+
state.catalog = [skillA];
|
|
654
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
655
|
+
state.fullCatalog = [
|
|
656
|
+
{ id: "example-skill-a", name: "example-skill-a", description: "A" },
|
|
657
|
+
];
|
|
658
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
659
|
+
|
|
660
|
+
await seedV2SkillEntries();
|
|
661
|
+
|
|
662
|
+
expect(state.upsertCalls).toHaveLength(1);
|
|
663
|
+
expect(state.upsertCalls[0].kind).toBe("skill");
|
|
664
|
+
expect(state.pruneCalls).toHaveLength(1);
|
|
665
|
+
expect(state.pruneCalls[0].options).toEqual({ kind: "skill" });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("runs the legacy kind backfill before pruning so kindless skill points become prunable", async () => {
|
|
669
|
+
// Simulates an install carrying legacy skill points written before the
|
|
670
|
+
// kind discriminator existed: the backfill must run before prune so the
|
|
671
|
+
// kind-scoped prune can see and delete the orphans.
|
|
672
|
+
const skillA = makeSummary({ id: "example-skill-a" });
|
|
673
|
+
state.catalog = [skillA];
|
|
674
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
675
|
+
state.fullCatalog = [
|
|
676
|
+
{ id: "example-skill-a", name: "example-skill-a", description: "A" },
|
|
677
|
+
];
|
|
678
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
679
|
+
state.backfillReturn = 3;
|
|
680
|
+
|
|
681
|
+
await seedV2SkillEntries();
|
|
682
|
+
|
|
683
|
+
expect(state.backfillCalls).toHaveLength(1);
|
|
684
|
+
expect(state.backfillCalls[0].prefix).toBe("skills/");
|
|
685
|
+
expect(state.backfillCalls[0].kind).toBe("skill");
|
|
686
|
+
expect([...state.backfillCalls[0].allowedSuffixes].sort()).toEqual([
|
|
687
|
+
"example-skill-a",
|
|
688
|
+
]);
|
|
689
|
+
expect(state.pruneCalls).toHaveLength(1);
|
|
690
|
+
expect(state.pruneCalls[0].options).toEqual({ kind: "skill" });
|
|
691
|
+
expect(state.callSequence.filter((s) => s !== "upsert")).toEqual([
|
|
692
|
+
"backfill",
|
|
693
|
+
"prune",
|
|
694
|
+
]);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("backfill only runs once per process across repeated seed runs", async () => {
|
|
698
|
+
const skillA = makeSummary({ id: "example-skill-a" });
|
|
699
|
+
state.catalog = [skillA];
|
|
700
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
701
|
+
state.fullCatalog = [
|
|
702
|
+
{ id: "example-skill-a", name: "example-skill-a", description: "A" },
|
|
703
|
+
];
|
|
704
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
705
|
+
|
|
706
|
+
await seedV2SkillEntries();
|
|
707
|
+
expect(state.backfillCalls).toHaveLength(1);
|
|
708
|
+
|
|
709
|
+
// A second seed should not re-scan: new upserts already carry kind, so
|
|
710
|
+
// there's nothing for the backfill to do.
|
|
711
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
712
|
+
await seedV2SkillEntries();
|
|
713
|
+
expect(state.backfillCalls).toHaveLength(1);
|
|
714
|
+
expect(state.pruneCalls).toHaveLength(2);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("backfill failure is non-fatal — prune still runs and lastSeedError stays clean", async () => {
|
|
718
|
+
const skillA = makeSummary({ id: "example-skill-a" });
|
|
719
|
+
state.catalog = [skillA];
|
|
720
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
721
|
+
state.fullCatalog = [
|
|
722
|
+
{ id: "example-skill-a", name: "example-skill-a", description: "A" },
|
|
723
|
+
];
|
|
724
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
725
|
+
state.backfillThrows = new Error("qdrant scroll exploded");
|
|
726
|
+
|
|
727
|
+
await expect(
|
|
728
|
+
seedV2SkillEntries({ throwOnError: true }),
|
|
729
|
+
).resolves.toBeUndefined();
|
|
730
|
+
|
|
731
|
+
// Prune still ran despite the backfill failure — we don't want to block
|
|
732
|
+
// the steady-state prune when the legacy scan trips.
|
|
733
|
+
expect(state.pruneCalls).toHaveLength(1);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test("backfill allowlist spans installed + remote catalog ids so user-authored skills/* pages stay untagged", async () => {
|
|
737
|
+
// Regression: backfilling kind on every `skills/*` point would also tag
|
|
738
|
+
// user-authored concept pages slugged like `skills/my-notes` — those
|
|
739
|
+
// would then be pruned as stale skills. The allowlist must contain
|
|
740
|
+
// every legitimate skill id we know about (installed + remote catalog)
|
|
741
|
+
// and nothing else.
|
|
742
|
+
const installed = makeSummary({ id: "installed-skill" });
|
|
743
|
+
state.catalog = [installed];
|
|
744
|
+
state.resolved = [{ summary: installed, state: "enabled" }];
|
|
745
|
+
state.fullCatalog = [
|
|
746
|
+
{ id: "installed-skill", name: "installed-skill", description: "X" },
|
|
747
|
+
{ id: "remote-only-skill", name: "remote-only-skill", description: "Y" },
|
|
748
|
+
];
|
|
749
|
+
state.embedReturn = [
|
|
750
|
+
[0.1, 0.2, 0.3],
|
|
751
|
+
[0.4, 0.5, 0.6],
|
|
752
|
+
];
|
|
753
|
+
|
|
754
|
+
await seedV2SkillEntries();
|
|
755
|
+
|
|
756
|
+
expect(state.backfillCalls).toHaveLength(1);
|
|
757
|
+
expect([...state.backfillCalls[0].allowedSuffixes].sort()).toEqual([
|
|
758
|
+
"installed-skill",
|
|
759
|
+
"remote-only-skill",
|
|
760
|
+
]);
|
|
761
|
+
});
|
|
762
|
+
|
|
447
763
|
test("skips pruning when catalog fetch returns empty (network failure guard)", async () => {
|
|
448
764
|
const skillA = makeSummary({ id: "example-skill-a" });
|
|
449
765
|
state.catalog = [skillA];
|
|
@@ -473,6 +789,38 @@ describe("getSkillCapability", () => {
|
|
|
473
789
|
|
|
474
790
|
expect(getSkillCapability("does-not-exist")).toBeNull();
|
|
475
791
|
});
|
|
792
|
+
|
|
793
|
+
test("mutating the returned entry does not corrupt the cache", async () => {
|
|
794
|
+
const skillA = makeSummary({ id: "example-skill-a" });
|
|
795
|
+
state.catalog = [skillA];
|
|
796
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
797
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
798
|
+
|
|
799
|
+
await seedV2SkillEntries();
|
|
800
|
+
|
|
801
|
+
const first = getSkillCapability("example-skill-a");
|
|
802
|
+
expect(first).not.toBeNull();
|
|
803
|
+
const originalContent = first!.content;
|
|
804
|
+
|
|
805
|
+
// Frozen entries throw in strict mode when mutated; suppress so we can
|
|
806
|
+
// prove cache invariance even if a future refactor swaps freeze for a
|
|
807
|
+
// plain clone.
|
|
808
|
+
try {
|
|
809
|
+
(first as unknown as { id: string }).id = "tampered";
|
|
810
|
+
(first as unknown as { content: string }).content = "tampered";
|
|
811
|
+
} catch {
|
|
812
|
+
// expected under Object.freeze
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const second = getSkillCapability("example-skill-a");
|
|
816
|
+
expect(second?.id).toBe("example-skill-a");
|
|
817
|
+
expect(second?.content).toBe(originalContent);
|
|
818
|
+
|
|
819
|
+
// listSkillEntries path also unaffected.
|
|
820
|
+
const viaList = listSkillEntries();
|
|
821
|
+
expect(viaList[0].id).toBe("example-skill-a");
|
|
822
|
+
expect(viaList[0].content).toBe(originalContent);
|
|
823
|
+
});
|
|
476
824
|
});
|
|
477
825
|
|
|
478
826
|
describe("listSkillEntries", () => {
|
|
@@ -521,4 +869,35 @@ describe("listSkillEntries", () => {
|
|
|
521
869
|
expect(second).toHaveLength(1);
|
|
522
870
|
expect(second[0].id).toBe("example-skill-a");
|
|
523
871
|
});
|
|
872
|
+
|
|
873
|
+
test("mutating a returned entry does not corrupt the cache", async () => {
|
|
874
|
+
const skillA = makeSummary({ id: "example-skill-a" });
|
|
875
|
+
state.catalog = [skillA];
|
|
876
|
+
state.resolved = [{ summary: skillA, state: "enabled" }];
|
|
877
|
+
state.embedReturn = [[0.1, 0.2, 0.3]];
|
|
878
|
+
|
|
879
|
+
await seedV2SkillEntries();
|
|
880
|
+
|
|
881
|
+
const first = listSkillEntries();
|
|
882
|
+
expect(first).toHaveLength(1);
|
|
883
|
+
const originalContent = first[0].content;
|
|
884
|
+
|
|
885
|
+
// Frozen entries throw in strict mode (ESM tests are strict) when
|
|
886
|
+
// mutated; suppress so we can prove cache invariance even if a future
|
|
887
|
+
// refactor swaps freeze for a plain clone.
|
|
888
|
+
try {
|
|
889
|
+
(first[0] as { id: string }).id = "tampered";
|
|
890
|
+
(first[0] as { content: string }).content = "tampered";
|
|
891
|
+
} catch {
|
|
892
|
+
// expected under Object.freeze
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const second = listSkillEntries();
|
|
896
|
+
expect(second[0].id).toBe("example-skill-a");
|
|
897
|
+
expect(second[0].content).toBe(originalContent);
|
|
898
|
+
|
|
899
|
+
// Lookup-by-id path also unaffected.
|
|
900
|
+
const viaLookup = getSkillCapability("example-skill-a");
|
|
901
|
+
expect(viaLookup?.content).toBe(originalContent);
|
|
902
|
+
});
|
|
524
903
|
});
|
|
@@ -144,6 +144,7 @@ export async function injectMemoryV2Block(
|
|
|
144
144
|
} = params;
|
|
145
145
|
|
|
146
146
|
const workspaceDir = getWorkspaceDir();
|
|
147
|
+
const mode: InjectMemoryV2Mode = params.mode ?? "per-turn";
|
|
147
148
|
|
|
148
149
|
// (1) Hydrate. Missing rows are normal at conversation start — proceed
|
|
149
150
|
// with an effective empty prior state so the first turn can still inject.
|
|
@@ -151,10 +152,17 @@ export async function injectMemoryV2Block(
|
|
|
151
152
|
const priorState = await hydrate(database, conversationId);
|
|
152
153
|
|
|
153
154
|
// Flag-gated router dispatch: when the LLM router is enabled, route the
|
|
154
|
-
//
|
|
155
|
-
//
|
|
155
|
+
// page selection through `runRouter` and reuse `finalizeInjection` for
|
|
156
|
+
// persistence, render, and telemetry. The activation pipeline below
|
|
156
157
|
// remains the default (flag-off) behavior — every code path past this
|
|
157
158
|
// branch only runs when the router is disabled.
|
|
159
|
+
//
|
|
160
|
+
// Runs on both `per-turn` and `context-load`. The `everInjected` dedupe
|
|
161
|
+
// concern from earlier doesn't apply post-compaction because
|
|
162
|
+
// `evictCompactedTurnsV2` in `ConversationGraphMemory.onCompacted`
|
|
163
|
+
// empties the list before this code runs. Router abstention on
|
|
164
|
+
// context-load means no v2 pages restored that turn, which is preferable
|
|
165
|
+
// to letting the activation graph pick something arbitrary.
|
|
158
166
|
if (config.memory.v2.router.enabled) {
|
|
159
167
|
return injectViaRouter({
|
|
160
168
|
workspaceDir,
|
|
@@ -214,7 +222,6 @@ export async function injectMemoryV2Block(
|
|
|
214
222
|
// prior cached attachments don't exist or have been thrown away. The user
|
|
215
223
|
// message gets a complete top-K dump alongside the static
|
|
216
224
|
// essentials/threads/recent block, then per-turn turns just add deltas.
|
|
217
|
-
const mode: InjectMemoryV2Mode = params.mode ?? "per-turn";
|
|
218
225
|
const priorEverInjected: readonly EverInjectedEntry[] =
|
|
219
226
|
priorState?.everInjected ?? [];
|
|
220
227
|
const { topNow, toInject } = selectInjections({
|
|
@@ -310,6 +317,14 @@ async function finalizeInjection(args: {
|
|
|
310
317
|
telemetryRows: MemoryV2ConceptRowRecord[];
|
|
311
318
|
config: AssistantConfig;
|
|
312
319
|
nextStateMap: Record<string, number>;
|
|
320
|
+
/**
|
|
321
|
+
* When true, errors thrown inside the helper (save / render / status
|
|
322
|
+
* finalization) are logged and swallowed instead of re-thrown. Used by
|
|
323
|
+
* the router-failure path, which is already a best-effort cleanup: a
|
|
324
|
+
* transient SQLite write here must not abort the turn on top of the
|
|
325
|
+
* router failure that already happened. Defaults to throwing.
|
|
326
|
+
*/
|
|
327
|
+
bestEffort?: boolean;
|
|
313
328
|
}): Promise<InjectMemoryV2BlockResult> {
|
|
314
329
|
const {
|
|
315
330
|
workspaceDir,
|
|
@@ -449,9 +464,16 @@ async function finalizeInjection(args: {
|
|
|
449
464
|
} catch (err) {
|
|
450
465
|
// Stash the error and let `finally` flush a best-effort telemetry row
|
|
451
466
|
// before we re-throw to the caller. `mode = "errored"` flags the row
|
|
452
|
-
// for observability dashboards / inspector queries.
|
|
467
|
+
// for observability dashboards / inspector queries. On the best-effort
|
|
468
|
+
// path the error is logged and swallowed so the trailing return stands.
|
|
453
469
|
caughtErr = err;
|
|
454
470
|
mode = "errored";
|
|
471
|
+
if (args.bestEffort) {
|
|
472
|
+
log.warn(
|
|
473
|
+
{ err, conversationId, turn: currentTurn },
|
|
474
|
+
"Memory v2 finalizeInjection error on best-effort path — swallowing",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
455
477
|
} finally {
|
|
456
478
|
try {
|
|
457
479
|
recordMemoryV2ActivationLog({
|
|
@@ -469,7 +491,7 @@ async function finalizeInjection(args: {
|
|
|
469
491
|
}
|
|
470
492
|
}
|
|
471
493
|
|
|
472
|
-
if (caughtErr !== undefined) throw caughtErr;
|
|
494
|
+
if (caughtErr !== undefined && !args.bestEffort) throw caughtErr;
|
|
473
495
|
return { block, toInject: newlyInjected };
|
|
474
496
|
}
|
|
475
497
|
|
|
@@ -537,7 +559,10 @@ async function injectViaRouter(args: {
|
|
|
537
559
|
// (preserving `priorEverInjected` so future turns still subtract
|
|
538
560
|
// previously-attached slugs) and writes the telemetry row through the
|
|
539
561
|
// same code path as the success branch — no inline duplication of
|
|
540
|
-
// `save` + `recordMemoryV2ActivationLog`.
|
|
562
|
+
// `save` + `recordMemoryV2ActivationLog`. `bestEffort: true` matches
|
|
563
|
+
// the pre-refactor inline behavior of logging and continuing if the
|
|
564
|
+
// stub-state `save()` throws — we don't want a transient SQLite write
|
|
565
|
+
// to abort the turn on top of the router failure that already happened.
|
|
541
566
|
return finalizeInjection({
|
|
542
567
|
workspaceDir,
|
|
543
568
|
database,
|
|
@@ -550,6 +575,7 @@ async function injectViaRouter(args: {
|
|
|
550
575
|
telemetryRows: [],
|
|
551
576
|
config,
|
|
552
577
|
nextStateMap: {},
|
|
578
|
+
bestEffort: true,
|
|
553
579
|
});
|
|
554
580
|
}
|
|
555
581
|
|
|
@@ -643,12 +643,15 @@ export async function runMemoryV2Migration(
|
|
|
643
643
|
}
|
|
644
644
|
|
|
645
645
|
/**
|
|
646
|
-
* HTML
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
646
|
+
* Paired HTML markers wrapped around each appended block. The opening marker
|
|
647
|
+
* also serves as the idempotency guard: `appendLines` is a read-modify-write,
|
|
648
|
+
* and without it a crash between `appendPromotions` and `writeSentinel` would
|
|
649
|
+
* let the next boot duplicate every promotion line. The closing marker
|
|
650
|
+
* delimits the migration-inserted region so a force-rerun strip can remove
|
|
651
|
+
* exactly that block without touching user/assistant edits appended below.
|
|
650
652
|
*/
|
|
651
|
-
const
|
|
653
|
+
const PROMOTION_MARKER_OPEN = "<!-- migration:v1-to-v2 -->";
|
|
654
|
+
const PROMOTION_MARKER_CLOSE = "<!-- /migration:v1-to-v2 -->";
|
|
652
655
|
|
|
653
656
|
/**
|
|
654
657
|
* Append each promotion bucket to its target file. Files are created if
|
|
@@ -680,8 +683,8 @@ async function appendPromotions(
|
|
|
680
683
|
|
|
681
684
|
/**
|
|
682
685
|
* Append `lines` to `path`, creating it (with a trailing newline) if absent.
|
|
683
|
-
* If the file already contains `
|
|
684
|
-
* prior partially-completed migration already wrote this block.
|
|
686
|
+
* If the file already contains `PROMOTION_MARKER_OPEN`, the append is skipped
|
|
687
|
+
* — a prior partially-completed migration already wrote this block.
|
|
685
688
|
*/
|
|
686
689
|
async function appendLines(path: string, lines: string[]): Promise<void> {
|
|
687
690
|
let existing = "";
|
|
@@ -690,16 +693,17 @@ async function appendLines(path: string, lines: string[]): Promise<void> {
|
|
|
690
693
|
} catch (err) {
|
|
691
694
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
692
695
|
}
|
|
693
|
-
if (existing.includes(
|
|
696
|
+
if (existing.includes(PROMOTION_MARKER_OPEN)) return;
|
|
694
697
|
const trailing = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
695
|
-
const
|
|
698
|
+
const block = `${PROMOTION_MARKER_OPEN}\n${lines.join("\n")}\n${PROMOTION_MARKER_CLOSE}\n`;
|
|
699
|
+
const next = `${existing}${trailing}${block}`;
|
|
696
700
|
await writeFile(path, next, "utf-8");
|
|
697
701
|
}
|
|
698
702
|
|
|
699
703
|
/**
|
|
700
|
-
* Strip any prior migration-block
|
|
701
|
-
*
|
|
702
|
-
*
|
|
704
|
+
* Strip any prior migration-block from each promotion target. Called on
|
|
705
|
+
* force-reruns so the marker guard in `appendLines` doesn't skip the new
|
|
706
|
+
* promotions.
|
|
703
707
|
*/
|
|
704
708
|
async function stripPromotionMarkerBlocks(workspaceDir: string): Promise<void> {
|
|
705
709
|
const memoryDir = join(workspaceDir, "memory");
|
|
@@ -718,6 +722,16 @@ async function stripPromotionMarkerBlocks(workspaceDir: string): Promise<void> {
|
|
|
718
722
|
await Promise.all(candidates.map(stripMarkerBlock));
|
|
719
723
|
}
|
|
720
724
|
|
|
725
|
+
/**
|
|
726
|
+
* Remove the migration-inserted block from `path` while preserving content
|
|
727
|
+
* outside it. The block is identified by the
|
|
728
|
+
* `PROMOTION_MARKER_OPEN ... PROMOTION_MARKER_CLOSE` envelope.
|
|
729
|
+
*
|
|
730
|
+
* If an opening marker is present without a matching close, strip from the
|
|
731
|
+
* opening marker to the next blank line, or to EOF if none is found. Content
|
|
732
|
+
* appended after such an unclosed block without a blank-line separator can
|
|
733
|
+
* be dropped on that fallback path.
|
|
734
|
+
*/
|
|
721
735
|
async function stripMarkerBlock(path: string): Promise<void> {
|
|
722
736
|
let existing: string;
|
|
723
737
|
try {
|
|
@@ -726,13 +740,29 @@ async function stripMarkerBlock(path: string): Promise<void> {
|
|
|
726
740
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
727
741
|
throw err;
|
|
728
742
|
}
|
|
729
|
-
const
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
743
|
+
const openIdx = existing.indexOf(PROMOTION_MARKER_OPEN);
|
|
744
|
+
if (openIdx === -1) return;
|
|
745
|
+
|
|
746
|
+
let endIdx: number;
|
|
747
|
+
const closeIdx = existing.indexOf(PROMOTION_MARKER_CLOSE, openIdx);
|
|
748
|
+
if (closeIdx !== -1) {
|
|
749
|
+
endIdx = closeIdx + PROMOTION_MARKER_CLOSE.length;
|
|
750
|
+
if (existing[endIdx] === "\n") endIdx += 1;
|
|
751
|
+
} else {
|
|
752
|
+
const blankIdx = existing.indexOf("\n\n", openIdx);
|
|
753
|
+
endIdx = blankIdx === -1 ? existing.length : blankIdx + 2;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const head = existing.slice(0, openIdx).replace(/\n+$/, "");
|
|
757
|
+
const tail = existing.slice(endIdx);
|
|
758
|
+
let next: string;
|
|
759
|
+
if (head.length === 0) {
|
|
760
|
+
next = tail;
|
|
761
|
+
} else if (tail.length === 0) {
|
|
762
|
+
next = `${head}\n`;
|
|
763
|
+
} else {
|
|
764
|
+
next = `${head}\n${tail}`;
|
|
765
|
+
}
|
|
736
766
|
await writeFile(path, next, "utf-8");
|
|
737
767
|
}
|
|
738
768
|
|