@vellumai/assistant 0.11.3 → 0.11.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +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 +139 -37
- 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 +17 -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 +177 -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__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- 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/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -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/api/surfaces.ts +7 -3
- 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 +19 -3
- 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/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- 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 +83 -12
- 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 +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- 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/plugin-api/vision-support.test.ts +1 -1
- 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-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -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/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- 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/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { getDocumentById } from "../documents/document-store.js";
|
|
4
|
+
import { getSqlite } from "../persistence/db-connection.js";
|
|
5
|
+
import {
|
|
6
|
+
executeDocumentCreate,
|
|
7
|
+
executeDocumentUpdate,
|
|
8
|
+
} from "../tools/document/document-tool.js";
|
|
9
|
+
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
10
|
+
import { resetDbForTesting } from "./db-test-helpers.js";
|
|
11
|
+
|
|
12
|
+
const CONVERSATION_ID = "conv-current";
|
|
13
|
+
|
|
14
|
+
/** Long enough to clear the store's minimum-duplicate-length floor. */
|
|
15
|
+
const OPENING =
|
|
16
|
+
"Remote work reshaped the office long before anyone had a name for it.";
|
|
17
|
+
|
|
18
|
+
function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
|
|
19
|
+
return {
|
|
20
|
+
workingDir: "/tmp/project",
|
|
21
|
+
conversationId: CONVERSATION_ID,
|
|
22
|
+
trustClass: "trusted_contact",
|
|
23
|
+
executionChannel: "slack",
|
|
24
|
+
sendToClient: () => {},
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseResult<T>(result: ToolExecutionResult): T {
|
|
30
|
+
return JSON.parse(result.content) as T;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bootstrapDocumentTables(): void {
|
|
34
|
+
resetDbForTesting();
|
|
35
|
+
const raw = getSqlite();
|
|
36
|
+
raw.exec(/*sql*/ `
|
|
37
|
+
DROP TABLE IF EXISTS document_conversations;
|
|
38
|
+
DROP TABLE IF EXISTS documents;
|
|
39
|
+
DROP TABLE IF EXISTS conversations;
|
|
40
|
+
|
|
41
|
+
CREATE TABLE conversations (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE documents (
|
|
47
|
+
surface_id TEXT PRIMARY KEY,
|
|
48
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
49
|
+
title TEXT NOT NULL,
|
|
50
|
+
content TEXT NOT NULL,
|
|
51
|
+
word_count INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
created_at INTEGER NOT NULL,
|
|
53
|
+
updated_at INTEGER NOT NULL,
|
|
54
|
+
workspace_path TEXT
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE document_conversations (
|
|
58
|
+
surface_id TEXT NOT NULL,
|
|
59
|
+
conversation_id TEXT NOT NULL,
|
|
60
|
+
created_at INTEGER NOT NULL,
|
|
61
|
+
PRIMARY KEY (surface_id, conversation_id),
|
|
62
|
+
FOREIGN KEY (surface_id) REFERENCES documents(surface_id) ON DELETE CASCADE
|
|
63
|
+
);
|
|
64
|
+
`);
|
|
65
|
+
raw
|
|
66
|
+
.query(`INSERT OR IGNORE INTO conversations (id, created_at) VALUES (?, ?)`)
|
|
67
|
+
.run(CONVERSATION_ID, Date.now());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function seedDocument(surfaceId: string, content: string): void {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const raw = getSqlite();
|
|
73
|
+
raw
|
|
74
|
+
.query(
|
|
75
|
+
`INSERT INTO documents (surface_id, conversation_id, title, content, word_count, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
76
|
+
)
|
|
77
|
+
.run(
|
|
78
|
+
surfaceId,
|
|
79
|
+
CONVERSATION_ID,
|
|
80
|
+
"Draft",
|
|
81
|
+
content,
|
|
82
|
+
content.split(/\s+/).filter(Boolean).length,
|
|
83
|
+
now,
|
|
84
|
+
now,
|
|
85
|
+
);
|
|
86
|
+
raw
|
|
87
|
+
.query(
|
|
88
|
+
`INSERT OR IGNORE INTO document_conversations (surface_id, conversation_id, created_at) VALUES (?, ?, ?)`,
|
|
89
|
+
)
|
|
90
|
+
.run(surfaceId, CONVERSATION_ID, now);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("document append idempotency", () => {
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
bootstrapDocumentTables();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("an opening passed to both document_create and the first append lands once", () => {
|
|
99
|
+
const created = executeDocumentCreate(
|
|
100
|
+
{ title: "Draft", initial_content: `# Draft\n\n${OPENING}` },
|
|
101
|
+
makeContext(),
|
|
102
|
+
);
|
|
103
|
+
const surfaceId = parseResult<{ surface_id: string }>(created).surface_id;
|
|
104
|
+
|
|
105
|
+
const result = executeDocumentUpdate(
|
|
106
|
+
{
|
|
107
|
+
surface_id: surfaceId,
|
|
108
|
+
content: `# Draft\n\n${OPENING}\n\nThe second paragraph carries the argument forward.`,
|
|
109
|
+
mode: "append",
|
|
110
|
+
},
|
|
111
|
+
makeContext(),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(result.isError).toBe(false);
|
|
115
|
+
expect(getDocumentById(surfaceId)?.content).toBe(
|
|
116
|
+
`# Draft\n\n${OPENING}\n\nThe second paragraph carries the argument forward.`,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("an append that only restates the tail writes nothing", () => {
|
|
121
|
+
seedDocument("doc-restate", `# Draft\n\n${OPENING}`);
|
|
122
|
+
|
|
123
|
+
const result = executeDocumentUpdate(
|
|
124
|
+
{ surface_id: "doc-restate", content: OPENING, mode: "append" },
|
|
125
|
+
makeContext(),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result.isError).toBe(false);
|
|
129
|
+
const body = parseResult<{ success: boolean; message: string }>(result);
|
|
130
|
+
expect(body.success).toBe(true);
|
|
131
|
+
expect(body.message).toBe(
|
|
132
|
+
"Nothing appended: the content is already at the end of the document",
|
|
133
|
+
);
|
|
134
|
+
expect(getDocumentById("doc-restate")?.content).toBe(
|
|
135
|
+
`# Draft\n\n${OPENING}`,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("the client is told what landed, not what was submitted", () => {
|
|
140
|
+
seedDocument("doc-client", OPENING);
|
|
141
|
+
const sent: { markdown: unknown; mode: unknown }[] = [];
|
|
142
|
+
|
|
143
|
+
executeDocumentUpdate(
|
|
144
|
+
{
|
|
145
|
+
surface_id: "doc-client",
|
|
146
|
+
content: `${OPENING}\n\nAnd then the rest.`,
|
|
147
|
+
mode: "append",
|
|
148
|
+
},
|
|
149
|
+
makeContext({
|
|
150
|
+
sendToClient: (message) => {
|
|
151
|
+
if (message.type === "document_editor_update") {
|
|
152
|
+
sent.push({ markdown: message.markdown, mode: message.mode });
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(sent).toEqual([{ markdown: "And then the rest.", mode: "append" }]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── Legitimate repetition the guard must not eat ──────────────────
|
|
162
|
+
|
|
163
|
+
test("keeps a short repeated line, even back to back", () => {
|
|
164
|
+
seedDocument("doc-refrain", "## Checklist\n\nTODO");
|
|
165
|
+
|
|
166
|
+
executeDocumentUpdate(
|
|
167
|
+
{ surface_id: "doc-refrain", content: "TODO\n\nTODO", mode: "append" },
|
|
168
|
+
makeContext(),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(getDocumentById("doc-refrain")?.content).toBe(
|
|
172
|
+
"## Checklist\n\nTODO\n\nTODO\n\nTODO",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("keeps a repeated heading followed by new prose", () => {
|
|
177
|
+
seedDocument("doc-heading", "# Weekly Notes\n\n## Monday");
|
|
178
|
+
|
|
179
|
+
executeDocumentUpdate(
|
|
180
|
+
{
|
|
181
|
+
surface_id: "doc-heading",
|
|
182
|
+
content: "## Monday\n\nShipped the importer.",
|
|
183
|
+
mode: "append",
|
|
184
|
+
},
|
|
185
|
+
makeContext(),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(getDocumentById("doc-heading")?.content).toBe(
|
|
189
|
+
"# Weekly Notes\n\n## Monday\n\n## Monday\n\nShipped the importer.",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("keeps a long paragraph that repeats something other than the tail", () => {
|
|
194
|
+
seedDocument("doc-midway", `${OPENING}\n\nA later paragraph ends the doc.`);
|
|
195
|
+
|
|
196
|
+
executeDocumentUpdate(
|
|
197
|
+
{ surface_id: "doc-midway", content: OPENING, mode: "append" },
|
|
198
|
+
makeContext(),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(getDocumentById("doc-midway")?.content).toBe(
|
|
202
|
+
`${OPENING}\n\nA later paragraph ends the doc.\n\n${OPENING}`,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("keeps content whose repetition starts partway into the append", () => {
|
|
207
|
+
seedDocument("doc-partway", `# Draft\n\n${OPENING}`);
|
|
208
|
+
|
|
209
|
+
executeDocumentUpdate(
|
|
210
|
+
{
|
|
211
|
+
surface_id: "doc-partway",
|
|
212
|
+
content: `A fresh lead-in.\n\n${OPENING}`,
|
|
213
|
+
mode: "append",
|
|
214
|
+
},
|
|
215
|
+
makeContext(),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(getDocumentById("doc-partway")?.content).toBe(
|
|
219
|
+
`# Draft\n\n${OPENING}\n\nA fresh lead-in.\n\n${OPENING}`,
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("replace mode is never deduplicated", () => {
|
|
224
|
+
seedDocument("doc-replace", OPENING);
|
|
225
|
+
|
|
226
|
+
executeDocumentUpdate(
|
|
227
|
+
{ surface_id: "doc-replace", content: OPENING, mode: "replace" },
|
|
228
|
+
makeContext(),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(getDocumentById("doc-replace")?.content).toBe(OPENING);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -145,7 +145,6 @@ describe("Slack edit propagation", () => {
|
|
|
145
145
|
conversationExternalId: seeded.conversationExternalId,
|
|
146
146
|
externalMessageId: nextEditEventId(),
|
|
147
147
|
sourceMessageId: seeded.channelTs,
|
|
148
|
-
canonicalAssistantId: "self",
|
|
149
148
|
assistantId: "self",
|
|
150
149
|
content: "new text",
|
|
151
150
|
});
|
|
@@ -187,7 +186,6 @@ describe("Slack edit propagation", () => {
|
|
|
187
186
|
externalMessageId: nextEditEventId(),
|
|
188
187
|
sourceMessageId: seeded.channelTs,
|
|
189
188
|
sourceThreadId: threadTs,
|
|
190
|
-
canonicalAssistantId: "self",
|
|
191
189
|
assistantId: "self",
|
|
192
190
|
content: "new text",
|
|
193
191
|
});
|
|
@@ -216,7 +214,6 @@ describe("Slack edit propagation", () => {
|
|
|
216
214
|
conversationExternalId: seeded.conversationExternalId,
|
|
217
215
|
externalMessageId: nextEditEventId(),
|
|
218
216
|
sourceMessageId: seeded.channelTs,
|
|
219
|
-
canonicalAssistantId: "self",
|
|
220
217
|
assistantId: "self",
|
|
221
218
|
content: "first edit",
|
|
222
219
|
});
|
|
@@ -239,7 +236,6 @@ describe("Slack edit propagation", () => {
|
|
|
239
236
|
conversationExternalId: seeded.conversationExternalId,
|
|
240
237
|
externalMessageId: nextEditEventId(),
|
|
241
238
|
sourceMessageId: seeded.channelTs,
|
|
242
|
-
canonicalAssistantId: "self",
|
|
243
239
|
assistantId: "self",
|
|
244
240
|
content: "second edit",
|
|
245
241
|
});
|
|
@@ -272,7 +268,6 @@ describe("Slack edit propagation", () => {
|
|
|
272
268
|
conversationExternalId: seeded.conversationExternalId,
|
|
273
269
|
externalMessageId: nextEditEventId(),
|
|
274
270
|
sourceMessageId: seeded.channelTs,
|
|
275
|
-
canonicalAssistantId: "self",
|
|
276
271
|
assistantId: "self",
|
|
277
272
|
// Same text as stored -- simulates a Slack unfurl `message_changed`
|
|
278
273
|
// where only attachments changed.
|
|
@@ -313,7 +308,6 @@ describe("Slack edit propagation", () => {
|
|
|
313
308
|
conversationExternalId: seeded.conversationExternalId,
|
|
314
309
|
externalMessageId: nextEditEventId(),
|
|
315
310
|
sourceMessageId: seeded.channelTs,
|
|
316
|
-
canonicalAssistantId: "self",
|
|
317
311
|
assistantId: "self",
|
|
318
312
|
content: "edited searchable text",
|
|
319
313
|
});
|
|
@@ -341,7 +335,6 @@ describe("Slack edit propagation", () => {
|
|
|
341
335
|
// sourceMessageId points at a ts that was never stored.
|
|
342
336
|
externalMessageId: nextEditEventId(),
|
|
343
337
|
sourceMessageId: "9999.0000",
|
|
344
|
-
canonicalAssistantId: "self",
|
|
345
338
|
assistantId: "self",
|
|
346
339
|
content: "new text",
|
|
347
340
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed fixture factories for actor identity in tests.
|
|
3
|
+
*
|
|
4
|
+
* A fixture built here is a complete, type-checked `TrustContext` or
|
|
5
|
+
* `AuthContext` with conservative defaults; a test overrides only the fields
|
|
6
|
+
* its scenario is about, and the compiler guards the rest. Hand-rolled
|
|
7
|
+
* literals cannot make that guarantee: a missing required field needs a cast
|
|
8
|
+
* to satisfy the checker, and the cast then hides every other mistake in the
|
|
9
|
+
* literal.
|
|
10
|
+
*
|
|
11
|
+
* Type-only imports keep this helper out of the runtime import graph, per the
|
|
12
|
+
* test-machinery isolation rules in `assistant/CLAUDE.md`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { TrustContext } from "../../daemon/trust-context-types.js";
|
|
16
|
+
import type { AuthContext, Scope } from "../../runtime/auth/types.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A complete `TrustContext`. Defaults to the guardian on the vellum channel;
|
|
20
|
+
* pass overrides for anything the test cares about.
|
|
21
|
+
*/
|
|
22
|
+
export function mockTrustContext(
|
|
23
|
+
overrides: Partial<TrustContext> = {},
|
|
24
|
+
): TrustContext {
|
|
25
|
+
return {
|
|
26
|
+
sourceChannel: "vellum",
|
|
27
|
+
trustClass: "guardian",
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A complete `AuthContext`. Defaults to a local principal with no scopes, the
|
|
34
|
+
* conservative shape, so a test that forgets to opt in exercises the
|
|
35
|
+
* restrictive branch rather than a silently permissive one.
|
|
36
|
+
*/
|
|
37
|
+
export function mockAuthContext(
|
|
38
|
+
overrides: Partial<AuthContext> = {},
|
|
39
|
+
): AuthContext {
|
|
40
|
+
return {
|
|
41
|
+
subject: "local:self:test",
|
|
42
|
+
principalType: "local",
|
|
43
|
+
assistantId: "self",
|
|
44
|
+
scopeProfile: "local_v1",
|
|
45
|
+
scopes: new Set<Scope>(),
|
|
46
|
+
policyEpoch: 0,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -26,7 +26,19 @@ import type { ChannelCapabilities } from "../../daemon/conversation-runtime-asse
|
|
|
26
26
|
export function asConversation<TExtra extends object = object>(
|
|
27
27
|
mock: Partial<Conversation> & TExtra,
|
|
28
28
|
): Conversation & TExtra {
|
|
29
|
-
|
|
29
|
+
// Trust accessors, derived from the double's own fields exactly as the real
|
|
30
|
+
// class derives them, so a double that sets (or later mutates) the fields
|
|
31
|
+
// resolves the same values production would. Supplied only when the double
|
|
32
|
+
// does not bring its own, and reading through `merged` rather than `mock`
|
|
33
|
+
// so a test that reassigns the fields on the returned object is honoured.
|
|
34
|
+
const merged: Partial<Conversation> & TExtra = {
|
|
35
|
+
getTurnTrust: () => merged.currentTurnTrustContext,
|
|
36
|
+
getTrustContext: () => merged.trustContext,
|
|
37
|
+
getTurnOrRestingTrust: () =>
|
|
38
|
+
merged.currentTurnTrustContext ?? merged.trustContext,
|
|
39
|
+
...mock,
|
|
40
|
+
};
|
|
41
|
+
return merged as unknown as Conversation & TExtra;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
/**
|
|
@@ -67,6 +67,7 @@ const { registerDefaultPluginInjectors } =
|
|
|
67
67
|
await import("../plugins/defaults/index.js");
|
|
68
68
|
import { eq } from "drizzle-orm";
|
|
69
69
|
|
|
70
|
+
import type { InterfaceId } from "../channels/types.js";
|
|
70
71
|
import {
|
|
71
72
|
clearConversations,
|
|
72
73
|
setConversation,
|
|
@@ -86,6 +87,7 @@ import type { Message } from "../providers/types.js";
|
|
|
86
87
|
import { getSubagentManager } from "../subagent/index.js";
|
|
87
88
|
import type { SubagentState } from "../subagent/types.js";
|
|
88
89
|
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
90
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
89
91
|
|
|
90
92
|
// `applyRuntimeInjections` self-resolves the Slack active-thread focus block
|
|
91
93
|
// from the persisted message rows, so the schema must exist for Slack-channel
|
|
@@ -153,26 +155,30 @@ function seedWorkspaceContext(
|
|
|
153
155
|
text: string,
|
|
154
156
|
currentTurnTemporalSnapshot?: {
|
|
155
157
|
clientTimezone: string | null;
|
|
158
|
+
timeSinceLastMessage: string | null;
|
|
156
159
|
},
|
|
157
|
-
interfaceName?:
|
|
160
|
+
interfaceName?: InterfaceId,
|
|
158
161
|
): void {
|
|
159
|
-
setConversation(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
162
|
+
setConversation(
|
|
163
|
+
TEST_CONVERSATION_ID,
|
|
164
|
+
asConversation({
|
|
165
|
+
conversationId: TEST_CONVERSATION_ID,
|
|
166
|
+
workingDir: "/sandbox",
|
|
167
|
+
workspaceTopLevelContext: text,
|
|
168
|
+
workspaceTopLevelDirty: false,
|
|
169
|
+
currentTurnTemporalSnapshot,
|
|
170
|
+
currentTurnInterfaceContext: interfaceName
|
|
171
|
+
? {
|
|
172
|
+
userMessageInterface: interfaceName,
|
|
173
|
+
assistantMessageInterface: interfaceName,
|
|
174
|
+
}
|
|
175
|
+
: undefined,
|
|
176
|
+
// Mirrors Conversation.getSubagentChildren: the `<active_subagents>` block
|
|
177
|
+
// is sourced from the live conversation, which delegates to the manager.
|
|
178
|
+
getSubagentChildren: () =>
|
|
179
|
+
getSubagentManager().getChildrenOf(TEST_CONVERSATION_ID),
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
// `applyRuntimeInjections` gates the `<turn_context>` block on the live
|
|
@@ -181,13 +187,19 @@ function seedWorkspaceContext(
|
|
|
181
187
|
// fall back to the runtime-assembly fallback conversation, so seed the snapshot
|
|
182
188
|
// there.
|
|
183
189
|
function seedFallbackTemporalSnapshot(): void {
|
|
184
|
-
setConversation(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
setConversation(
|
|
191
|
+
"runtime-assembly-fallback",
|
|
192
|
+
asConversation({
|
|
193
|
+
conversationId: "runtime-assembly-fallback",
|
|
194
|
+
workingDir: "/sandbox",
|
|
195
|
+
workspaceTopLevelContext: "",
|
|
196
|
+
workspaceTopLevelDirty: false,
|
|
197
|
+
currentTurnTemporalSnapshot: {
|
|
198
|
+
clientTimezone: null,
|
|
199
|
+
timeSinceLastMessage: null,
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
// Persist Slack-channel rows for the turn conversation so
|
|
@@ -484,7 +496,11 @@ describe("injector chain", () => {
|
|
|
484
496
|
seedSubagentChild(TEST_CONVERSATION_ID, subagentChild);
|
|
485
497
|
const subagentBlock = buildSubagentStatusBlock([subagentChild])!;
|
|
486
498
|
|
|
487
|
-
seedWorkspaceContext(
|
|
499
|
+
seedWorkspaceContext(
|
|
500
|
+
workspaceText,
|
|
501
|
+
{ clientTimezone: null, timeSinceLastMessage: null },
|
|
502
|
+
"macos",
|
|
503
|
+
);
|
|
488
504
|
const result = await applyRuntimeInjections(runMessages, {
|
|
489
505
|
...makeTurnContext(),
|
|
490
506
|
});
|
|
@@ -562,20 +578,23 @@ describe("injector chain", () => {
|
|
|
562
578
|
createdAt: 1700000005_000,
|
|
563
579
|
},
|
|
564
580
|
]);
|
|
565
|
-
setConversation(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
581
|
+
setConversation(
|
|
582
|
+
TEST_CONVERSATION_ID,
|
|
583
|
+
asConversation({
|
|
584
|
+
conversationId: TEST_CONVERSATION_ID,
|
|
585
|
+
workingDir: "/sandbox",
|
|
586
|
+
workspaceTopLevelContext: "",
|
|
587
|
+
workspaceTopLevelDirty: false,
|
|
588
|
+
trustContext: { trustClass: "guardian", sourceChannel: "vellum" },
|
|
589
|
+
channelCapabilities: {
|
|
590
|
+
channel: "slack",
|
|
591
|
+
dashboardCapable: false,
|
|
592
|
+
supportsDynamicUi: false,
|
|
593
|
+
supportsVoiceInput: false,
|
|
594
|
+
chatType: "channel",
|
|
595
|
+
},
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
579
598
|
const result = await applyRuntimeInjections(originalRun, {
|
|
580
599
|
...makeTurnContext(),
|
|
581
600
|
});
|
|
@@ -614,7 +633,10 @@ describe("injector chain", () => {
|
|
|
614
633
|
timestamp: FIXED_TURN_TIMESTAMP,
|
|
615
634
|
interfaceName: "web",
|
|
616
635
|
});
|
|
617
|
-
seedWorkspaceContext("", {
|
|
636
|
+
seedWorkspaceContext("", {
|
|
637
|
+
clientTimezone: null,
|
|
638
|
+
timeSinceLastMessage: null,
|
|
639
|
+
});
|
|
618
640
|
seedSubagentChild(
|
|
619
641
|
TEST_CONVERSATION_ID,
|
|
620
642
|
makeSubagentState("sub-1", "worker", "running"),
|
|
@@ -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: {
|