@vellumai/assistant 0.11.3 → 0.11.4-staging.1
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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -14,6 +14,7 @@ mock.module("../daemon/date-context.js", () => ({
|
|
|
14
14
|
formatTurnTimestamp: () => FIXED_TURN_TIMESTAMP,
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
17
18
|
import {
|
|
18
19
|
clearConversations,
|
|
19
20
|
setConversation,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
} from "../plugins/defaults/workspace/injectors.js";
|
|
40
41
|
import type { Injector, TurnContext } from "../plugins/types.js";
|
|
41
42
|
import type { Message } from "../providers/types.js";
|
|
43
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
42
44
|
|
|
43
45
|
// `applyRuntimeInjections` self-resolves the Slack active-thread focus block
|
|
44
46
|
// from the persisted message rows, so the schema must exist for Slack-channel
|
|
@@ -90,38 +92,23 @@ const diskPressureInjector = findInjector("disk-pressure-warning");
|
|
|
90
92
|
// turn can exercise both; the seed helpers mutate and re-register the same
|
|
91
93
|
// instance. An empty (non-dirty) workspace cache resolves to no block, so
|
|
92
94
|
// disk-pressure-only tests don't trigger a directory scan.
|
|
93
|
-
let liveConversation:
|
|
94
|
-
conversationId: string;
|
|
95
|
-
workingDir: string;
|
|
96
|
-
workspaceTopLevelContext: string | null;
|
|
97
|
-
workspaceTopLevelDirty: boolean;
|
|
98
|
-
diskPressureCleanupModeActive: boolean;
|
|
99
|
-
channelCapabilities?: ChannelCapabilities;
|
|
100
|
-
trustContext?: { trustClass: string };
|
|
101
|
-
currentTurnTemporalSnapshot?: {
|
|
102
|
-
clientTimezone: string | null;
|
|
103
|
-
};
|
|
104
|
-
currentTurnInterfaceContext?: {
|
|
105
|
-
userMessageInterface: string;
|
|
106
|
-
assistantMessageInterface: string;
|
|
107
|
-
};
|
|
108
|
-
};
|
|
95
|
+
let liveConversation: Conversation;
|
|
109
96
|
|
|
110
97
|
function resetLiveConversation(): void {
|
|
111
|
-
liveConversation = {
|
|
98
|
+
liveConversation = asConversation({
|
|
112
99
|
conversationId: TEST_CONVERSATION_ID,
|
|
113
100
|
workingDir: "/workspace",
|
|
114
101
|
workspaceTopLevelContext: "",
|
|
115
102
|
workspaceTopLevelDirty: false,
|
|
116
103
|
diskPressureCleanupModeActive: false,
|
|
117
|
-
trustContext: { trustClass: "guardian" },
|
|
104
|
+
trustContext: { trustClass: "guardian", sourceChannel: "vellum" },
|
|
118
105
|
// The unified-turn-context injector sources the interface label from the
|
|
119
106
|
// live conversation's turn interface context; match the expected blocks.
|
|
120
107
|
currentTurnInterfaceContext: {
|
|
121
108
|
userMessageInterface: "macos",
|
|
122
109
|
assistantMessageInterface: "macos",
|
|
123
110
|
},
|
|
124
|
-
};
|
|
111
|
+
});
|
|
125
112
|
}
|
|
126
113
|
|
|
127
114
|
// `applyRuntimeInjections` gates the `<turn_context>` block on the live
|
|
@@ -130,24 +117,25 @@ function resetLiveConversation(): void {
|
|
|
130
117
|
function seedTemporalSnapshot(): void {
|
|
131
118
|
liveConversation.currentTurnTemporalSnapshot = {
|
|
132
119
|
clientTimezone: null,
|
|
120
|
+
timeSinceLastMessage: null,
|
|
133
121
|
};
|
|
134
|
-
setConversation(TEST_CONVERSATION_ID, liveConversation
|
|
122
|
+
setConversation(TEST_CONVERSATION_ID, liveConversation);
|
|
135
123
|
}
|
|
136
124
|
|
|
137
125
|
function seedChannelCapabilities(caps: ChannelCapabilities): void {
|
|
138
126
|
liveConversation.channelCapabilities = caps;
|
|
139
|
-
setConversation(TEST_CONVERSATION_ID, liveConversation
|
|
127
|
+
setConversation(TEST_CONVERSATION_ID, liveConversation);
|
|
140
128
|
}
|
|
141
129
|
|
|
142
130
|
function seedDiskPressure(cleanupModeActive: boolean): void {
|
|
143
131
|
liveConversation.diskPressureCleanupModeActive = cleanupModeActive;
|
|
144
|
-
setConversation(TEST_CONVERSATION_ID, liveConversation
|
|
132
|
+
setConversation(TEST_CONVERSATION_ID, liveConversation);
|
|
145
133
|
}
|
|
146
134
|
|
|
147
135
|
function seedWorkspaceContext(text: string): void {
|
|
148
136
|
liveConversation.workspaceTopLevelContext = text;
|
|
149
137
|
liveConversation.workspaceTopLevelDirty = false;
|
|
150
|
-
setConversation(TEST_CONVERSATION_ID, liveConversation
|
|
138
|
+
setConversation(TEST_CONVERSATION_ID, liveConversation);
|
|
151
139
|
}
|
|
152
140
|
|
|
153
141
|
// Persist Slack-channel rows for the turn conversation so
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Entry-name providers resolve context limits through their row's kind, so
|
|
4
|
+
// the tests control the row store directly. A dangling label (no row) falls
|
|
5
|
+
// back to the model's catalog owner.
|
|
6
|
+
const connectionRows = new Map<string, { name: string; provider: string }>([
|
|
7
|
+
["openai-work", { name: "openai-work", provider: "openai" }],
|
|
8
|
+
["my-endpoint", { name: "my-endpoint", provider: "openai-compatible" }],
|
|
9
|
+
]);
|
|
10
|
+
mock.module("../persistence/db-connection.js", () => ({
|
|
11
|
+
getDb: () => ({}),
|
|
12
|
+
}));
|
|
13
|
+
mock.module("../providers/inference/connections.js", () => ({
|
|
14
|
+
getConnection: (_db: unknown, name: string) =>
|
|
15
|
+
connectionRows.get(name) ?? null,
|
|
16
|
+
listConnections: () => [],
|
|
17
|
+
canonicalVellumConnection: () => null,
|
|
18
|
+
}));
|
|
2
19
|
|
|
3
20
|
import { resolveEffectiveContextWindow } from "../config/llm-context-resolution.js";
|
|
4
21
|
import { LLMSchema } from "../config/schemas/llm.js";
|
|
@@ -131,6 +148,61 @@ describe("resolveEffectiveContextWindow", () => {
|
|
|
131
148
|
expect(resolved.modelMaxInputTokens).toBe(1050000);
|
|
132
149
|
});
|
|
133
150
|
|
|
151
|
+
test("an entry of a catalog kind resolves the model's own context limits", () => {
|
|
152
|
+
// The entries collapse stores connection names in the provider field;
|
|
153
|
+
// the row's kind carries the model's limits, so an entry label must not
|
|
154
|
+
// fall back to the 200k default when its kind serves the model.
|
|
155
|
+
const llm = LLMSchema.parse({
|
|
156
|
+
profiles: {
|
|
157
|
+
work: { provider: "openai-work", model: "gpt-5.5" },
|
|
158
|
+
},
|
|
159
|
+
activeProfile: "work",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const resolved = resolveEffectiveContextWindow({
|
|
163
|
+
llm,
|
|
164
|
+
callSite: "mainAgent",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(resolved.modelMaxInputTokens).toBe(1050000);
|
|
168
|
+
expect(resolved.maxOutputTokens).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a custom-endpoint entry keeps the conservative default for a catalog-colliding model id", () => {
|
|
172
|
+
// An openai-compatible endpoint's "gpt-5.5" is not OpenAI's; inheriting
|
|
173
|
+
// the built-in 1.05M limit would let oversized requests through.
|
|
174
|
+
const llm = LLMSchema.parse({
|
|
175
|
+
profiles: {
|
|
176
|
+
custom: { provider: "my-endpoint", model: "gpt-5.5" },
|
|
177
|
+
},
|
|
178
|
+
activeProfile: "custom",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const resolved = resolveEffectiveContextWindow({
|
|
182
|
+
llm,
|
|
183
|
+
callSite: "mainAgent",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(resolved.modelMaxInputTokens).toBe(200000);
|
|
187
|
+
expect(resolved.maxOutputTokens).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("a label with no row falls back to the model's catalog owner", () => {
|
|
191
|
+
const llm = LLMSchema.parse({
|
|
192
|
+
profiles: {
|
|
193
|
+
gone: { provider: "deleted-entry", model: "gpt-5.5" },
|
|
194
|
+
},
|
|
195
|
+
activeProfile: "gone",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const resolved = resolveEffectiveContextWindow({
|
|
199
|
+
llm,
|
|
200
|
+
callSite: "mainAgent",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(resolved.modelMaxInputTokens).toBe(1050000);
|
|
204
|
+
});
|
|
205
|
+
|
|
134
206
|
test("unknown catalog model falls back safely to the default 200k cap", () => {
|
|
135
207
|
const llm = LLMSchema.parse({
|
|
136
208
|
callSites: {
|
|
@@ -123,11 +123,14 @@ describe("LLMSchema", () => {
|
|
|
123
123
|
expect(parsed.profileOrder).toEqual(["fast", "stale"]);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
test("
|
|
126
|
+
test("an unknown provider parses (read tolerance for entry names)", () => {
|
|
127
|
+
// The open provider schema keeps the profile; write-time membership is
|
|
128
|
+
// enforced at the profiles route and the config-write choke point, and
|
|
129
|
+
// dispatch resolves the value as a connection entry name.
|
|
127
130
|
const result = LLMSchema.safeParse({
|
|
128
131
|
profiles: { mine: { provider: "bogus-provider" } },
|
|
129
132
|
});
|
|
130
|
-
expect(result.success).toBe(
|
|
133
|
+
expect(result.success).toBe(true);
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
test("invalid temperature (negative) rejected", () => {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-level coverage for plugin-declared MCP servers appearing in
|
|
3
|
+
* `internal_mcp_list` (what `assistant mcp list` renders).
|
|
4
|
+
*
|
|
5
|
+
* The unit tests in `plugins/__tests__/mcp-servers.test.ts` cover reading
|
|
6
|
+
* `mcp.json`. This file covers the merge, and two properties that a
|
|
7
|
+
* refactor could quietly break:
|
|
8
|
+
*
|
|
9
|
+
* 1. A workspace entry of the same id wins. Getting precedence backwards
|
|
10
|
+
* would let a plugin redirect a server the user configured by hand.
|
|
11
|
+
* 2. A plugin server is never health-checked. `McpClient.connect` resolves
|
|
12
|
+
* `mcp:<serverId>:headers` and `mcp:<serverId>:tokens` from the
|
|
13
|
+
* credential store, and a plugin controls both its server key and its
|
|
14
|
+
* URL, so probing one would send a workspace credential to an endpoint
|
|
15
|
+
* the plugin chose whenever an id happens to match a stored key.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
|
|
21
|
+
|
|
22
|
+
const mockConnect = jest.fn();
|
|
23
|
+
const mockDisconnect = jest.fn();
|
|
24
|
+
/** Server ids the route actually constructed an MCP client for. */
|
|
25
|
+
const connectedServerIds: string[] = [];
|
|
26
|
+
|
|
27
|
+
mock.module("../mcp/client.js", () => ({
|
|
28
|
+
McpClient: class {
|
|
29
|
+
constructor(serverId: string) {
|
|
30
|
+
connectedServerIds.push(serverId);
|
|
31
|
+
}
|
|
32
|
+
get isConnected() {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
get lastError() {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
connect = mockConnect;
|
|
39
|
+
disconnect = mockDisconnect;
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../mcp/mcp-auth-orchestrator.js", () => ({
|
|
44
|
+
orchestrateMcpOAuthConnect: async () => ({
|
|
45
|
+
auth_url: "",
|
|
46
|
+
already_authenticated: false,
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
mock.module("../mcp/mcp-auth-state.js", () => ({
|
|
51
|
+
getMcpAuthState: () => null,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
mock.module("../mcp/mcp-oauth-provider.js", () => ({
|
|
55
|
+
// Stand in for a credential store that holds tokens for every id, which
|
|
56
|
+
// is the condition under which a leak would be observable.
|
|
57
|
+
hasMcpOAuthTokens: async () => true,
|
|
58
|
+
deleteMcpOAuthCredentials: async () => ({ ok: true, failedKeys: [] }),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
mock.module("../mcp/mcp-header-store.js", () => ({
|
|
62
|
+
getMcpHeaders: async () => ({ Authorization: "Bearer workspace-secret" }),
|
|
63
|
+
setMcpHeaders: async () => {},
|
|
64
|
+
deleteMcpHeaders: async () => true,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
mock.module("../daemon/mcp-reload-service.js", () => ({
|
|
68
|
+
reloadMcpServers: async () => {},
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
72
|
+
|
|
73
|
+
setConfig("mcp", {
|
|
74
|
+
servers: {
|
|
75
|
+
"from-workspace": {
|
|
76
|
+
transport: { type: "streamable-http", url: "https://config.example/mcp" },
|
|
77
|
+
enabled: true,
|
|
78
|
+
defaultRiskLevel: "high",
|
|
79
|
+
maxTools: 20,
|
|
80
|
+
},
|
|
81
|
+
// Deliberately shares an id with the `shadowed` plugin below.
|
|
82
|
+
shadowed: {
|
|
83
|
+
transport: { type: "streamable-http", url: "https://wins.example/mcp" },
|
|
84
|
+
enabled: true,
|
|
85
|
+
defaultRiskLevel: "low",
|
|
86
|
+
maxTools: 20,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { getWorkspacePluginsDir } = await import("../util/platform.js");
|
|
92
|
+
const { ROUTES } = await import("../runtime/routes/mcp-auth-routes.js");
|
|
93
|
+
|
|
94
|
+
const listHandler = ROUTES.find(
|
|
95
|
+
(r: { operationId: string }) => r.operationId === "internal_mcp_list",
|
|
96
|
+
)!.handler;
|
|
97
|
+
|
|
98
|
+
interface ListedServer {
|
|
99
|
+
id: string;
|
|
100
|
+
status: string;
|
|
101
|
+
source?: "workspace" | "plugin";
|
|
102
|
+
pluginName?: string;
|
|
103
|
+
defaultRiskLevel: string;
|
|
104
|
+
hasOAuth: boolean;
|
|
105
|
+
hasStaticAuth: boolean;
|
|
106
|
+
authType: string;
|
|
107
|
+
transport: { type: string; url?: string };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writePlugin(name: string, mcpJson: unknown): void {
|
|
111
|
+
const dir = join(getWorkspacePluginsDir(), name);
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join(dir, "package.json"),
|
|
115
|
+
JSON.stringify({ name, version: "1.0.0" }),
|
|
116
|
+
);
|
|
117
|
+
writeFileSync(join(dir, "mcp.json"), JSON.stringify(mcpJson));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function unabyssManifest(): unknown {
|
|
121
|
+
return {
|
|
122
|
+
mcpServers: {
|
|
123
|
+
unabyss: { type: "streamable-http", url: "https://mcp.unabyss.com" },
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function listServers(): Promise<ListedServer[]> {
|
|
129
|
+
const result = (await listHandler({})) as { servers: ListedServer[] };
|
|
130
|
+
return result.servers;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
describe("internal_mcp_list, plugin-declared servers", () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
mockConnect.mockReset();
|
|
136
|
+
mockDisconnect.mockReset();
|
|
137
|
+
connectedServerIds.length = 0;
|
|
138
|
+
rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
|
|
139
|
+
mkdirSync(getWorkspacePluginsDir(), { recursive: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("a plugin's mcp.json server appears alongside workspace servers", async () => {
|
|
143
|
+
writePlugin("unabyss", unabyssManifest());
|
|
144
|
+
|
|
145
|
+
const ids = (await listServers()).map((s) => s.id);
|
|
146
|
+
expect(ids).toContain("from-workspace");
|
|
147
|
+
expect(ids).toContain("unabyss");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("plugin servers are labelled with their origin, workspace servers are not", async () => {
|
|
151
|
+
writePlugin("unabyss", unabyssManifest());
|
|
152
|
+
|
|
153
|
+
const servers = await listServers();
|
|
154
|
+
const plugin = servers.find((s) => s.id === "unabyss")!;
|
|
155
|
+
const workspace = servers.find((s) => s.id === "from-workspace")!;
|
|
156
|
+
|
|
157
|
+
expect(plugin.source).toEqual("plugin");
|
|
158
|
+
expect(plugin.pluginName).toEqual("unabyss");
|
|
159
|
+
expect(workspace.source).toEqual("workspace");
|
|
160
|
+
expect(workspace.pluginName).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a workspace server of the same id wins over the plugin's", async () => {
|
|
164
|
+
writePlugin("shadowed", {
|
|
165
|
+
mcpServers: {
|
|
166
|
+
shadowed: { type: "streamable-http", url: "https://loses.example/mcp" },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const matches = (await listServers()).filter((s) => s.id === "shadowed");
|
|
171
|
+
|
|
172
|
+
expect(matches).toHaveLength(1);
|
|
173
|
+
expect(matches[0].source).toEqual("workspace");
|
|
174
|
+
expect(matches[0].transport.url).toEqual("https://wins.example/mcp");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("plugin servers are not health-checked, so no credential can reach them", async () => {
|
|
178
|
+
writePlugin("unabyss", unabyssManifest());
|
|
179
|
+
|
|
180
|
+
const servers = await listServers();
|
|
181
|
+
|
|
182
|
+
expect(servers.find((s) => s.id === "unabyss")!.status).toEqual("declared");
|
|
183
|
+
// The credential mocks above return a token and an Authorization header
|
|
184
|
+
// for every id. Constructing a client for the plugin server is what
|
|
185
|
+
// would ship them to the plugin-declared URL.
|
|
186
|
+
expect(connectedServerIds).toContain("from-workspace");
|
|
187
|
+
expect(connectedServerIds).not.toContain("unabyss");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("plugin servers report no assistant-owned auth even when the store has some", async () => {
|
|
191
|
+
writePlugin("unabyss", unabyssManifest());
|
|
192
|
+
|
|
193
|
+
const plugin = (await listServers()).find((s) => s.id === "unabyss")!;
|
|
194
|
+
expect(plugin.hasOAuth).toBe(false);
|
|
195
|
+
expect(plugin.hasStaticAuth).toBe(false);
|
|
196
|
+
expect(plugin.authType).toEqual("none");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("workspace servers keep reporting their auth state", async () => {
|
|
200
|
+
const workspace = (await listServers()).find(
|
|
201
|
+
(s) => s.id === "from-workspace",
|
|
202
|
+
)!;
|
|
203
|
+
expect(workspace.hasOAuth).toBe(true);
|
|
204
|
+
expect(workspace.hasStaticAuth).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("plugin servers default to low risk", async () => {
|
|
208
|
+
writePlugin("unabyss", unabyssManifest());
|
|
209
|
+
|
|
210
|
+
const servers = await listServers();
|
|
211
|
+
expect(servers.find((s) => s.id === "unabyss")!.defaultRiskLevel).toEqual(
|
|
212
|
+
"low",
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("a directory with no valid package.json is not advertised", async () => {
|
|
217
|
+
// The runtime loader would never load it, and its stdio command must
|
|
218
|
+
// not be reachable through the listing.
|
|
219
|
+
const dir = join(getWorkspacePluginsDir(), "no-manifest");
|
|
220
|
+
mkdirSync(dir, { recursive: true });
|
|
221
|
+
writeFileSync(
|
|
222
|
+
join(dir, "mcp.json"),
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
mcpServers: { evil: { type: "stdio", command: "touch" } },
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const ids = (await listServers()).map((s) => s.id);
|
|
229
|
+
expect(ids).not.toContain("no-manifest__evil");
|
|
230
|
+
expect(ids).toContain("from-workspace");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a malformed plugin manifest does not remove workspace servers", async () => {
|
|
234
|
+
const dir = join(getWorkspacePluginsDir(), "broken");
|
|
235
|
+
mkdirSync(dir, { recursive: true });
|
|
236
|
+
writeFileSync(
|
|
237
|
+
join(dir, "package.json"),
|
|
238
|
+
JSON.stringify({ name: "broken", version: "1.0.0" }),
|
|
239
|
+
);
|
|
240
|
+
writeFileSync(join(dir, "mcp.json"), "{ not json");
|
|
241
|
+
|
|
242
|
+
const ids = (await listServers()).map((s) => s.id);
|
|
243
|
+
expect(ids).toContain("from-workspace");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("no plugins installed leaves the listing unchanged", async () => {
|
|
247
|
+
const servers = await listServers();
|
|
248
|
+
expect(servers.every((s) => s.source === "workspace")).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -73,6 +73,7 @@ import type { QdrantSparseVector } from "../persistence/embeddings/qdrant-client
|
|
|
73
73
|
import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/conversation-graph-memory.js";
|
|
74
74
|
import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory/hooks/user-prompt-submit.js";
|
|
75
75
|
import type { Message } from "../providers/types.js";
|
|
76
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
76
77
|
|
|
77
78
|
/** Canonical metrics payload the graph retriever attaches to a real hit. */
|
|
78
79
|
function makeMetrics() {
|
|
@@ -172,15 +173,15 @@ function installConversation(
|
|
|
172
173
|
graphMemory: ConversationGraphMemory,
|
|
173
174
|
opts?: {
|
|
174
175
|
trusted?: boolean;
|
|
175
|
-
|
|
176
|
+
abortController?: AbortController;
|
|
176
177
|
},
|
|
177
178
|
): void {
|
|
178
179
|
currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
|
|
179
|
-
currentConversation = {
|
|
180
|
+
currentConversation = asConversation({
|
|
180
181
|
graphMemory,
|
|
181
182
|
trustContext: undefined,
|
|
182
|
-
abortController:
|
|
183
|
-
}
|
|
183
|
+
abortController: opts?.abortController ?? new AbortController(),
|
|
184
|
+
});
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
beforeEach(() => {
|
|
@@ -492,7 +493,7 @@ describe("user-prompt-submit hook (memory retrieval)", () => {
|
|
|
492
493
|
const controller = new AbortController();
|
|
493
494
|
installConversation(graphMemory, {
|
|
494
495
|
trusted: true,
|
|
495
|
-
|
|
496
|
+
abortController: controller,
|
|
496
497
|
});
|
|
497
498
|
const ctx = makeHookCtx();
|
|
498
499
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { Glob } from "bun";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ratchet over direct reads of the `messages` table outside `persistence/`.
|
|
9
|
+
*
|
|
10
|
+
* Every row in `messages` is conversation content in some state of
|
|
11
|
+
* completeness, and each read encodes a visibility decision (completed
|
|
12
|
+
* history vs. any row). `persistence/` owns that contract: named accessors in
|
|
13
|
+
* `message-reads.ts` and the history readers in `conversation-crud.ts` state
|
|
14
|
+
* their decision in their name; everything else should go through them. A
|
|
15
|
+
* direct reader that re-decides visibility on its own is how the contract
|
|
16
|
+
* fails silently, so a new direct read is a reviewed exception, not a
|
|
17
|
+
* default.
|
|
18
|
+
*
|
|
19
|
+
* The BASELINE lists every file outside `persistence/` that reads the table
|
|
20
|
+
* directly today. Each entry has been audited and carries its visibility
|
|
21
|
+
* decision as a comment at the read site. The guard fails when a file outside
|
|
22
|
+
* the baseline gains a direct read (route it through an accessor, or audit it
|
|
23
|
+
* and baseline it with a site comment explaining the decision in your PR),
|
|
24
|
+
* and when a baseline entry goes stale (remove it, so coverage cannot rot).
|
|
25
|
+
*
|
|
26
|
+
* Detection is deliberately shape-based (from/join of `messages` in drizzle
|
|
27
|
+
* builders, `FROM messages` / `JOIN messages` in raw SQL): this is a ratchet
|
|
28
|
+
* against sprawl, not a proof, and matching the read shapes keeps it free of
|
|
29
|
+
* false positives from unrelated identifiers.
|
|
30
|
+
*
|
|
31
|
+
* Regenerate after an intentional change with:
|
|
32
|
+
* UPDATE_MESSAGES_READ_BASELINE=1 bun test src/__tests__/messages-read-boundary-guard.test.ts
|
|
33
|
+
* and paste the printed list here, explaining the new read in your PR.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const SRC_REL = "src";
|
|
37
|
+
const SRC_ABS = join(process.cwd(), SRC_REL);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Audited direct readers outside persistence/, as file -> count of matching
|
|
41
|
+
* read sites. Counts, not just filenames: a new read added inside an
|
|
42
|
+
* already-baselined file must fail the ratchet too, since each site carries
|
|
43
|
+
* its own visibility decision. A same-file refactor that replaces one read
|
|
44
|
+
* with another at equal count passes; that is accepted ratchet granularity
|
|
45
|
+
* (the diff still shows the site to its reviewer).
|
|
46
|
+
*/
|
|
47
|
+
const BASELINE: Readonly<Record<string, number>> = {
|
|
48
|
+
"src/apps/app-store.ts": 1,
|
|
49
|
+
"src/daemon/credential-transcript-scrub.ts": 1,
|
|
50
|
+
"src/live-voice/live-voice-archive.ts": 1,
|
|
51
|
+
"src/monitoring/recovery/inflight-content.ts": 2,
|
|
52
|
+
"src/plugins/defaults/memory/context-search/sources/conversations.ts": 1,
|
|
53
|
+
"src/plugins/defaults/memory/graph/image-ref-utils.ts": 1,
|
|
54
|
+
"src/plugins/defaults/memory/indexer.ts": 2,
|
|
55
|
+
"src/plugins/defaults/memory/memory-retrospective-accounting.ts": 4,
|
|
56
|
+
"src/plugins/defaults/memory/substrate/sweep-job.ts": 1,
|
|
57
|
+
"src/plugins/defaults/memory/v1/graph/extraction.ts": 3,
|
|
58
|
+
"src/plugins/defaults/memory/v1/job-handlers/backfill.ts": 1,
|
|
59
|
+
"src/plugins/defaults/memory/v1/job-handlers/embedding.ts": 1,
|
|
60
|
+
"src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts": 1,
|
|
61
|
+
"src/plugins/defaults/memory/v2/harness/oracle.ts": 1,
|
|
62
|
+
"src/plugins/defaults/memory/v2/harness/replay-input.ts": 1,
|
|
63
|
+
"src/plugins/defaults/memory/v3-eval/eval-packets.ts": 2,
|
|
64
|
+
"src/plugins/defaults/memory/v3/prune.ts": 1,
|
|
65
|
+
"src/plugins/defaults/memory/v3/selection-log-store.ts": 1,
|
|
66
|
+
"src/runtime/pre-first-message-gate.ts": 1,
|
|
67
|
+
"src/runtime/routes/log-export-routes.ts": 1,
|
|
68
|
+
"src/runtime/routes/surface-conversation-resolver.ts": 2,
|
|
69
|
+
"src/telemetry/turn-events-store.ts": 2,
|
|
70
|
+
"src/telemetry/turn-trace-store.ts": 2,
|
|
71
|
+
"src/workspace/migrations/rebuild-conversation-disk-view.ts": 1,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DRIZZLE_READ =
|
|
75
|
+
/\.(?:from|innerJoin|leftJoin|rightJoin|fullJoin)\(\s*messages\s*[,)]/g;
|
|
76
|
+
const RAW_SQL_READ = /(?:FROM|JOIN)\s+messages\b/g;
|
|
77
|
+
|
|
78
|
+
function isExempt(relPath: string): boolean {
|
|
79
|
+
const parts = relPath.split(sep);
|
|
80
|
+
return (
|
|
81
|
+
parts[1] === "persistence" ||
|
|
82
|
+
parts.includes("__tests__") ||
|
|
83
|
+
relPath.endsWith(".test.ts")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function scanDirectReaders(): Promise<Map<string, number>> {
|
|
88
|
+
const glob = new Glob("**/*.ts");
|
|
89
|
+
const found = new Map<string, number>();
|
|
90
|
+
for await (const file of glob.scan({ cwd: SRC_ABS, absolute: true })) {
|
|
91
|
+
const relPath = join(SRC_REL, relative(SRC_ABS, file));
|
|
92
|
+
if (isExempt(relPath)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const source = readFileSync(file, "utf-8");
|
|
96
|
+
const count =
|
|
97
|
+
[...source.matchAll(DRIZZLE_READ)].length +
|
|
98
|
+
[...source.matchAll(RAW_SQL_READ)].length;
|
|
99
|
+
if (count > 0) {
|
|
100
|
+
found.set(relPath, count);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("messages read boundary", () => {
|
|
107
|
+
test("no new direct messages reads beyond the audited baseline", async () => {
|
|
108
|
+
const found = await scanDirectReaders();
|
|
109
|
+
|
|
110
|
+
if (process.env.UPDATE_MESSAGES_READ_BASELINE === "1") {
|
|
111
|
+
console.log(
|
|
112
|
+
JSON.stringify(Object.fromEntries([...found].sort()), null, 2),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const grown = [...found]
|
|
117
|
+
.filter(([file, count]) => count > (BASELINE[file] ?? 0))
|
|
118
|
+
.map(([file, count]) => `${file}: ${BASELINE[file] ?? 0} -> ${count}`);
|
|
119
|
+
expect(
|
|
120
|
+
grown,
|
|
121
|
+
`New direct reads of the messages table outside persistence/ (route ` +
|
|
122
|
+
`through an accessor in persistence/message-reads.ts, or audit the ` +
|
|
123
|
+
`read, state its visibility decision at the site, and baseline it):`,
|
|
124
|
+
).toEqual([]);
|
|
125
|
+
|
|
126
|
+
const shrunk = Object.entries(BASELINE)
|
|
127
|
+
.filter(([file, count]) => (found.get(file) ?? 0) < count)
|
|
128
|
+
.map(([file, count]) => `${file}: ${count} -> ${found.get(file) ?? 0}`);
|
|
129
|
+
expect(
|
|
130
|
+
shrunk,
|
|
131
|
+
"Stale baseline entries (fewer direct reads than baselined; ratchet the count down so coverage cannot rot):",
|
|
132
|
+
).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -30,10 +30,10 @@ import {
|
|
|
30
30
|
} from "bun:test";
|
|
31
31
|
|
|
32
32
|
import { _inspectHookCacheForTests } from "../hooks/hook-loader.js";
|
|
33
|
+
import { getUserHooksFor } from "../hooks/registry.js";
|
|
33
34
|
import {
|
|
34
35
|
_inspectToolCacheForTests,
|
|
35
36
|
getCachedUserTools,
|
|
36
|
-
getUserHooksFor,
|
|
37
37
|
populateCacheAtBoot,
|
|
38
38
|
reconcilePluginSourcesNow,
|
|
39
39
|
resetPluginCacheForTests,
|