@vellumai/assistant 0.8.1 → 0.8.3
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 +13 -19
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +17 -0
- package/docker-init-apt-root.sh +167 -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 +642 -5
- 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-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- 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 +147 -0
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- 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 +31 -65
- 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 +59 -1
- 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-media-retry.test.ts +19 -8
- 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 +102 -13
- 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__/date-context.test.ts +45 -0
- 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 +151 -55
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- 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 +507 -10
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- 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-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +15 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -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-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +58 -13
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +41 -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__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -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 +242 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- 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} +7 -2
- 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 +158 -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} +33 -31
- 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__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
- 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 +670 -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-087-memory-router-balanced-profile.test.ts +228 -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/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/agent/loop.ts +167 -18
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +122 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +960 -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 +388 -346
- package/src/cli/commands/plugins.ts +252 -0
- package/src/cli/commands/schedules.ts +683 -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__/search-plugins.test.ts +261 -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 +303 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +52 -2
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
- 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/phone-calls/SKILL.md +1 -1
- 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/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +41 -9
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +9 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +17 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +23 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1107 -0
- package/src/context/token-estimator.ts +34 -36
- 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 +33 -18
- 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-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +198 -11
- 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 +25 -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 -8
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/conversations.ts +1 -0
- 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 +153 -27
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +89 -91
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +85 -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/notifications.ts +21 -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 +11 -54
- package/src/daemon/pkb-reminder-builder.ts +5 -20
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +24 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +33 -0
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +73 -189
- package/src/home/__tests__/feed-types.test.ts +80 -0
- package/src/home/feed-types.ts +36 -2
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/ipc/cli-client.ts +147 -45
- 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 +483 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- 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 +197 -11
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +12 -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 +150 -12
- package/src/memory/graph/conversation-graph-memory.ts +49 -21
- package/src/memory/graph/tools.ts +9 -40
- package/src/memory/indexer.ts +34 -29
- package/src/memory/invite-store.ts +53 -0
- 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 +24 -12
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- 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/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +9 -0
- package/src/memory/migrations/registry.ts +16 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +3 -3
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +288 -11
- 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/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +81 -26
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +63 -8
- 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/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- 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/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +113 -45
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +21 -1
- package/src/notifications/home-feed-side-effect.ts +138 -5
- package/src/notifications/signal.ts +3 -5
- package/src/notifications/types.ts +8 -0
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +19 -6
- 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 +74 -22
- 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 +187 -42
- 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 +40 -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 +10 -43
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -174
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +164 -0
- package/src/providers/__tests__/inference.test.ts +24 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +68 -11
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +32 -6
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +159 -34
- package/src/providers/inference/resolve-auth.ts +14 -4
- package/src/providers/model-catalog.ts +249 -12
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +169 -8
- package/src/providers/openrouter/client.ts +49 -4
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
- 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 +38 -0
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +52 -15
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +103 -15
- package/src/runtime/auth/route-policy.ts +21 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +19 -47
- 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__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- 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 +126 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -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 +99 -35
- package/src/runtime/routes/conversation-routes.ts +97 -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 +8 -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 +199 -22
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- 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/llm-call-sites-routes.ts +11 -1
- 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 +98 -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 +17 -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/memory/register.ts +1 -9
- 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 +107 -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/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +10 -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/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -121,6 +121,9 @@ const state = {
|
|
|
121
121
|
// Throw queue for upsert: first call shifts and throws if non-null;
|
|
122
122
|
// subsequent calls succeed once the queue is exhausted.
|
|
123
123
|
upsertThrowQueue: [] as Array<Error | null>,
|
|
124
|
+
// Throw queue for createPayloadIndex: each entry maps to the next call,
|
|
125
|
+
// so tests can simulate index-creation failures (strict-mode, network).
|
|
126
|
+
createIndexThrowQueue: [] as Array<Error | null>,
|
|
124
127
|
};
|
|
125
128
|
|
|
126
129
|
class MockQdrantClient {
|
|
@@ -157,6 +160,10 @@ class MockQdrantClient {
|
|
|
157
160
|
params: { field_name: string; field_schema: string },
|
|
158
161
|
) {
|
|
159
162
|
state.createIndexCalls.push(params);
|
|
163
|
+
if (state.createIndexThrowQueue.length > 0) {
|
|
164
|
+
const next = state.createIndexThrowQueue.shift();
|
|
165
|
+
if (next) throw next;
|
|
166
|
+
}
|
|
160
167
|
return {};
|
|
161
168
|
}
|
|
162
169
|
async upsert(_name: string, params: { wait: boolean; points: MockPoint[] }) {
|
|
@@ -228,6 +235,7 @@ function resetState(): void {
|
|
|
228
235
|
state.countThrows = null;
|
|
229
236
|
state.countCalls = 0;
|
|
230
237
|
state.upsertThrowQueue.length = 0;
|
|
238
|
+
state.createIndexThrowQueue.length = 0;
|
|
231
239
|
_resetMemoryV2QdrantForTests();
|
|
232
240
|
// Drop any sentinel a prior test left behind so the no-drift default path
|
|
233
241
|
// doesn't accidentally report `migrated: true`.
|
|
@@ -278,9 +286,10 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
278
286
|
});
|
|
279
287
|
expect(params.on_disk_payload).toBe(true);
|
|
280
288
|
|
|
281
|
-
// Slug payload
|
|
289
|
+
// Slug + kind payload indexes are created up front.
|
|
282
290
|
expect(state.createIndexCalls).toEqual([
|
|
283
291
|
{ field_name: "slug", field_schema: "keyword" },
|
|
292
|
+
{ field_name: "kind", field_schema: "keyword" },
|
|
284
293
|
]);
|
|
285
294
|
});
|
|
286
295
|
|
|
@@ -295,9 +304,14 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
295
304
|
await ensureConceptPageCollection();
|
|
296
305
|
|
|
297
306
|
// Existence check fired exactly once thanks to the in-memory readiness
|
|
298
|
-
// cache; createCollection
|
|
307
|
+
// cache; createCollection never ran. Payload indexes are (idempotently)
|
|
308
|
+
// ensured on the existing-collection path to backfill long-lived installs
|
|
309
|
+
// that predate the `kind` index.
|
|
299
310
|
expect(state.createCollectionCalls).toBe(0);
|
|
300
|
-
expect(state.createIndexCalls).toEqual([
|
|
311
|
+
expect(state.createIndexCalls).toEqual([
|
|
312
|
+
{ field_name: "slug", field_schema: "keyword" },
|
|
313
|
+
{ field_name: "kind", field_schema: "keyword" },
|
|
314
|
+
]);
|
|
301
315
|
expect(state.collectionExistsCalls).toBe(1);
|
|
302
316
|
});
|
|
303
317
|
|
|
@@ -314,6 +328,7 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
314
328
|
expect(state.createCollectionCalls).toBe(1);
|
|
315
329
|
expect(state.createIndexCalls).toEqual([
|
|
316
330
|
{ field_name: "slug", field_schema: "keyword" },
|
|
331
|
+
{ field_name: "kind", field_schema: "keyword" },
|
|
317
332
|
]);
|
|
318
333
|
});
|
|
319
334
|
|
|
@@ -433,6 +448,54 @@ describe("memory v2 qdrant — collection lifecycle", () => {
|
|
|
433
448
|
expect(existsSync(REEMBED_SENTINEL_PATH)).toBe(false);
|
|
434
449
|
});
|
|
435
450
|
|
|
451
|
+
test("swallows 'already exists' on createPayloadIndex but propagates other failures without latching readiness", async () => {
|
|
452
|
+
// Existing collection that already has the full schema — the ensure
|
|
453
|
+
// path goes through `ensurePayloadIndexes` to backfill long-lived
|
|
454
|
+
// installs. The first index call hits an "already exists" race
|
|
455
|
+
// (benign; swallow); the second hits a strict-mode rejection (must
|
|
456
|
+
// propagate so readiness is not latched).
|
|
457
|
+
state.collectionExistsBeforeCreate = true;
|
|
458
|
+
state.createIndexThrowQueue.push(
|
|
459
|
+
Object.assign(
|
|
460
|
+
new Error("Wrong input: Payload field 'slug' already exists"),
|
|
461
|
+
{ status: 400 },
|
|
462
|
+
),
|
|
463
|
+
Object.assign(
|
|
464
|
+
new Error(
|
|
465
|
+
"Strict mode prohibits creating payload indexes on this deployment",
|
|
466
|
+
),
|
|
467
|
+
{ status: 400 },
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
let caught: unknown = null;
|
|
472
|
+
try {
|
|
473
|
+
await ensureConceptPageCollection();
|
|
474
|
+
} catch (err) {
|
|
475
|
+
caught = err;
|
|
476
|
+
}
|
|
477
|
+
expect((caught as Error | null)?.message).toMatch(/strict mode/i);
|
|
478
|
+
|
|
479
|
+
// Both attempts ran; the strict-mode failure was not swallowed.
|
|
480
|
+
expect(state.createIndexCalls).toEqual([
|
|
481
|
+
{ field_name: "slug", field_schema: "keyword" },
|
|
482
|
+
{ field_name: "kind", field_schema: "keyword" },
|
|
483
|
+
]);
|
|
484
|
+
|
|
485
|
+
// Readiness must NOT be latched after a non-benign failure — otherwise
|
|
486
|
+
// later slug/kind-filtered queries (e.g. skill backfill) would keep
|
|
487
|
+
// failing until a daemon restart. A follow-up ensure must retry.
|
|
488
|
+
const result = await ensureConceptPageCollection();
|
|
489
|
+
expect(result).toEqual({ migrated: false });
|
|
490
|
+
// Indexes attempted again on the retry (no throws queued this time).
|
|
491
|
+
expect(state.createIndexCalls).toEqual([
|
|
492
|
+
{ field_name: "slug", field_schema: "keyword" },
|
|
493
|
+
{ field_name: "kind", field_schema: "keyword" },
|
|
494
|
+
{ field_name: "slug", field_schema: "keyword" },
|
|
495
|
+
{ field_name: "kind", field_schema: "keyword" },
|
|
496
|
+
]);
|
|
497
|
+
});
|
|
498
|
+
|
|
436
499
|
test("concurrent ensure during a schema rebuild only deletes/creates once", async () => {
|
|
437
500
|
state.collectionExistsBeforeCreate = true;
|
|
438
501
|
state.getCollectionInfo = {
|
|
@@ -460,6 +460,21 @@ describe("runRouter — failure modes", () => {
|
|
|
460
460
|
expect(warnSeen).toBe(true);
|
|
461
461
|
});
|
|
462
462
|
|
|
463
|
+
test("duplicate-heavy IDs are deduped before the cap is applied", async () => {
|
|
464
|
+
// [1, 1, 2] with max=2 must yield two distinct slugs, not collapse to one
|
|
465
|
+
// after a pre-dedupe slice trims away the only other unique ID.
|
|
466
|
+
providerStub = makeProvider(toolUseResponse([1, 1, 2]));
|
|
467
|
+
|
|
468
|
+
const result = await runRouter({
|
|
469
|
+
workspaceDir,
|
|
470
|
+
...COMMON_PARAMS,
|
|
471
|
+
config: makeConfig({ maxPageIds: 2 }),
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(result.failureReason).toBeNull();
|
|
475
|
+
expect(result.selectedSlugs).toEqual(["alpha", "bravo"]);
|
|
476
|
+
});
|
|
477
|
+
|
|
463
478
|
test("more than max_page_ids → truncated with warn", async () => {
|
|
464
479
|
providerStub = makeProvider(toolUseResponse([1, 2, 3]));
|
|
465
480
|
|
|
@@ -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
|
});
|
|
@@ -32,11 +32,15 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
let configMemoryV2Enabled = true;
|
|
35
|
+
let configMemoryEnabled = true;
|
|
35
36
|
|
|
36
37
|
mock.module("../../../config/loader.js", () => ({
|
|
37
38
|
getConfig: () => ({}),
|
|
38
39
|
loadConfig: () => ({
|
|
39
|
-
memory: {
|
|
40
|
+
memory: {
|
|
41
|
+
enabled: configMemoryEnabled,
|
|
42
|
+
v2: { enabled: configMemoryV2Enabled },
|
|
43
|
+
},
|
|
40
44
|
}),
|
|
41
45
|
loadRawConfig: () => ({}),
|
|
42
46
|
saveRawConfig: () => {},
|
|
@@ -71,6 +75,7 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
71
75
|
beforeEach(() => {
|
|
72
76
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
73
77
|
configMemoryV2Enabled = true;
|
|
78
|
+
configMemoryEnabled = true;
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
afterEach(() => {
|
|
@@ -83,6 +88,12 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
83
88
|
expect(readMemoryV2StaticContent()).toBeNull();
|
|
84
89
|
});
|
|
85
90
|
|
|
91
|
+
test("returns null when config.memory.enabled is off even with v2 on", () => {
|
|
92
|
+
configMemoryEnabled = false;
|
|
93
|
+
for (const file of MEMORY_FILES) writeMemoryFile(file, `Content ${file}`);
|
|
94
|
+
expect(readMemoryV2StaticContent()).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
86
97
|
test("returns headed sections in canonical order when all files have content", () => {
|
|
87
98
|
writeMemoryFile("essentials.md", "Alice prefers dark mode.");
|
|
88
99
|
writeMemoryFile("threads.md", "Open thread: ship PR-123 review.");
|