@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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<channel_capabilities>` must not accumulate one copy per turn, and
|
|
3
|
+
* deduplicating it must not disturb the cross-turn byte-identity that prompt
|
|
4
|
+
* caching depends on (see `prompt-cache-cross-turn-stability.test.ts`, which
|
|
5
|
+
* owns that invariant end to end).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
dedupeChannelCapabilityBlocks,
|
|
11
|
+
preModelCallSanitize,
|
|
12
|
+
} from "../context/outbound-sanitize.js";
|
|
13
|
+
import type { Message } from "../providers/types.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Fixtures, shaped like what runtime injection actually produces: the block is
|
|
17
|
+
// its OWN text content block, prepended to the turn-starting user message by
|
|
18
|
+
// `injectChannelCapabilityContext`, and then frozen into history.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const slackCapabilities = (dynamicUi = false): string =>
|
|
22
|
+
[
|
|
23
|
+
"<channel_capabilities>",
|
|
24
|
+
"channel: slack",
|
|
25
|
+
"dashboard_capable: false",
|
|
26
|
+
`supports_dynamic_ui: ${dynamicUi}`,
|
|
27
|
+
"supports_voice_input: false",
|
|
28
|
+
"",
|
|
29
|
+
"CHANNEL CONSTRAINTS:",
|
|
30
|
+
"- Do NOT reference the dashboard UI, settings panels, or visual preference pickers.",
|
|
31
|
+
'- Do NOT use app_create. Only use ui_show/ui_update for card surfaces with template: "task_progress"; present all other information as text.',
|
|
32
|
+
"- Present information as well-formatted text instead of dynamic UI.",
|
|
33
|
+
"</channel_capabilities>",
|
|
34
|
+
].join("\n");
|
|
35
|
+
|
|
36
|
+
const CAPS = slackCapabilities();
|
|
37
|
+
|
|
38
|
+
const turnContextBlock = (turn: number): string =>
|
|
39
|
+
[
|
|
40
|
+
"<turn_context>",
|
|
41
|
+
`current_time: 2026-05-21 (Thursday) 10:${String(turn).padStart(2, "0")}:00 -05:00 (America/Chicago)`,
|
|
42
|
+
"interface: slack",
|
|
43
|
+
"trust_class: guardian",
|
|
44
|
+
"</turn_context>",
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
const userTurn = (turn: number, caps: string = CAPS): Message => ({
|
|
48
|
+
role: "user",
|
|
49
|
+
content: [
|
|
50
|
+
{ type: "text", text: turnContextBlock(turn) },
|
|
51
|
+
{ type: "text", text: caps },
|
|
52
|
+
{ type: "text", text: `User message ${turn}.` },
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const assistantTurn = (turn: number): Message => ({
|
|
57
|
+
role: "assistant",
|
|
58
|
+
content: [{ type: "text", text: `Reply ${turn}.` }],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** An N-turn conversation, every user row carrying its own injected copy. */
|
|
62
|
+
const conversation = (turns: number): Message[] => {
|
|
63
|
+
const messages: Message[] = [];
|
|
64
|
+
for (let turn = 1; turn <= turns; turn++) {
|
|
65
|
+
messages.push(userTurn(turn));
|
|
66
|
+
if (turn < turns) {
|
|
67
|
+
messages.push(assistantTurn(turn));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return messages;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const allText = (messages: Message[]): string =>
|
|
74
|
+
messages
|
|
75
|
+
.flatMap((m) =>
|
|
76
|
+
m.content.filter((b) => b.type === "text").map((b) => b.text),
|
|
77
|
+
)
|
|
78
|
+
.join("\n");
|
|
79
|
+
|
|
80
|
+
const countOccurrences = (haystack: string, needle: string): number =>
|
|
81
|
+
haystack.split(needle).length - 1;
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
describe("<channel_capabilities> does not accumulate across a conversation", () => {
|
|
86
|
+
test("a 50-turn conversation carries exactly one copy", () => {
|
|
87
|
+
const raw = conversation(50);
|
|
88
|
+
expect(countOccurrences(allText(raw), "<channel_capabilities>")).toBe(50);
|
|
89
|
+
|
|
90
|
+
const text = allText(preModelCallSanitize(raw));
|
|
91
|
+
expect(countOccurrences(text, "<channel_capabilities>")).toBe(1);
|
|
92
|
+
expect(countOccurrences(text, "CHANNEL CONSTRAINTS:")).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("the surviving copy is the first one, unmodified", () => {
|
|
96
|
+
const sanitized = preModelCallSanitize(conversation(6));
|
|
97
|
+
expect(sanitized[0].content).toEqual(conversation(6)[0].content);
|
|
98
|
+
expect(allText([sanitized[2]])).not.toContain("<channel_capabilities>");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("outbound size stops growing with the repeated block", () => {
|
|
102
|
+
const rawGrowth =
|
|
103
|
+
allText(conversation(45)).length - allText(conversation(5)).length;
|
|
104
|
+
const sanitizedGrowth =
|
|
105
|
+
allText(preModelCallSanitize(conversation(45))).length -
|
|
106
|
+
allText(preModelCallSanitize(conversation(5))).length;
|
|
107
|
+
|
|
108
|
+
// Every extra raw turn pays for a full copy of the block; the deduplicated
|
|
109
|
+
// projection pays nothing for it.
|
|
110
|
+
expect((rawGrowth - sanitizedGrowth) / 40).toBeGreaterThanOrEqual(
|
|
111
|
+
CAPS.length,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("everything else on the row is untouched", () => {
|
|
116
|
+
const sanitized = preModelCallSanitize(conversation(4));
|
|
117
|
+
// `<turn_context>` is deliberately left alone: it carries a fresh
|
|
118
|
+
// timestamp each turn and historical copies are read back by the
|
|
119
|
+
// compactor's tail resolution.
|
|
120
|
+
expect(countOccurrences(allText(sanitized), "<turn_context>")).toBe(4);
|
|
121
|
+
// The dropped block is the only thing that goes: the row keeps its
|
|
122
|
+
// `<turn_context>` and the user's own text, in order.
|
|
123
|
+
expect(sanitized[6].content).toEqual([
|
|
124
|
+
{ type: "text", text: turnContextBlock(4) },
|
|
125
|
+
{ type: "text", text: "User message 4." },
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("cross-turn byte stability", () => {
|
|
131
|
+
test("a message renders identically no matter how many turns follow it", () => {
|
|
132
|
+
// The prompt cache is only readable when turn N's messages recur
|
|
133
|
+
// byte-identically at the same index in turn N+1. Deduplication must
|
|
134
|
+
// therefore depend only on what sits ABOVE a message, never on how far it
|
|
135
|
+
// is from the end.
|
|
136
|
+
const turn2 = preModelCallSanitize(conversation(2));
|
|
137
|
+
const turn20 = preModelCallSanitize(conversation(20));
|
|
138
|
+
|
|
139
|
+
turn2.forEach((message, index) => {
|
|
140
|
+
expect(JSON.stringify(turn20[index])).toBe(JSON.stringify(message));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("the turn-starting message is not treated specially", () => {
|
|
145
|
+
const sanitized = preModelCallSanitize(conversation(3));
|
|
146
|
+
const turnStart = sanitized[sanitized.length - 1];
|
|
147
|
+
// Turn 3 opened this turn and still drops its duplicate copy, which is
|
|
148
|
+
// exactly what keeps it stable once turn 4 arrives.
|
|
149
|
+
expect(allText([turnStart])).not.toContain("<channel_capabilities>");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("is idempotent", () => {
|
|
153
|
+
const once = preModelCallSanitize(conversation(6));
|
|
154
|
+
expect(preModelCallSanitize(once)).toEqual(once);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("changes and safety", () => {
|
|
159
|
+
test("capabilities that actually change mid-conversation are kept", () => {
|
|
160
|
+
const changed = slackCapabilities(true);
|
|
161
|
+
const history: Message[] = [
|
|
162
|
+
userTurn(1),
|
|
163
|
+
assistantTurn(1),
|
|
164
|
+
userTurn(2),
|
|
165
|
+
assistantTurn(2),
|
|
166
|
+
userTurn(3, changed),
|
|
167
|
+
assistantTurn(3),
|
|
168
|
+
userTurn(4, changed),
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const sanitized = dedupeChannelCapabilityBlocks(history);
|
|
172
|
+
const text = allText(sanitized);
|
|
173
|
+
expect(countOccurrences(text, "<channel_capabilities>")).toBe(2);
|
|
174
|
+
expect(text).toContain("supports_dynamic_ui: false");
|
|
175
|
+
expect(text).toContain("supports_dynamic_ui: true");
|
|
176
|
+
// Turn 2 and turn 4 are the repeats and are the ones that go.
|
|
177
|
+
expect(allText([sanitized[2]])).not.toContain("<channel_capabilities>");
|
|
178
|
+
expect(allText([sanitized[6]])).not.toContain("<channel_capabilities>");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a conversation with a single copy is returned untouched", () => {
|
|
182
|
+
const history = conversation(1);
|
|
183
|
+
expect(dedupeChannelCapabilityBlocks(history)).toBe(history);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("user-authored text that merely mentions the tag is left alone", () => {
|
|
187
|
+
const authored =
|
|
188
|
+
"why does <channel_capabilities> say dashboard_capable: false?";
|
|
189
|
+
const history: Message[] = [
|
|
190
|
+
{ role: "user", content: [{ type: "text", text: authored }] },
|
|
191
|
+
assistantTurn(1),
|
|
192
|
+
{ role: "user", content: [{ type: "text", text: authored }] },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
expect(dedupeChannelCapabilityBlocks(history)).toBe(history);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("a user row made only of the block is never emptied or dropped", () => {
|
|
199
|
+
const history: Message[] = [
|
|
200
|
+
userTurn(1),
|
|
201
|
+
assistantTurn(1),
|
|
202
|
+
{ role: "user", content: [{ type: "text", text: CAPS }] },
|
|
203
|
+
assistantTurn(2),
|
|
204
|
+
userTurn(3),
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const sanitized = dedupeChannelCapabilityBlocks(history);
|
|
208
|
+
expect(sanitized).toHaveLength(5);
|
|
209
|
+
expect(sanitized[2].content).toEqual(history[2].content);
|
|
210
|
+
// The all-block row is retained, so it becomes the baseline the later
|
|
211
|
+
// duplicate is measured against and turn 3 still drops.
|
|
212
|
+
expect(allText([sanitized[4]])).not.toContain("<channel_capabilities>");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -504,23 +504,23 @@ describe("channel-delivery-store", () => {
|
|
|
504
504
|
expect(r1.conversationId).not.toBe(r2.conversationId);
|
|
505
505
|
});
|
|
506
506
|
|
|
507
|
-
test("same chat
|
|
508
|
-
const r1 = recordInbound("telegram", "chat-1", "msg-1"
|
|
509
|
-
|
|
510
|
-
});
|
|
511
|
-
const r2 = recordInbound("telegram", "chat-1", "msg-2", {
|
|
512
|
-
assistantId: "asst-B",
|
|
513
|
-
});
|
|
507
|
+
test("the same chat keeps the same conversation", () => {
|
|
508
|
+
const r1 = recordInbound("telegram", "chat-1", "msg-1");
|
|
509
|
+
const r2 = recordInbound("telegram", "chat-1", "msg-2");
|
|
514
510
|
|
|
515
|
-
expect(r1.conversationId).
|
|
511
|
+
expect(r1.conversationId).toBe(r2.conversationId);
|
|
516
512
|
});
|
|
517
513
|
|
|
518
|
-
test("
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
514
|
+
test("binds under the self-scoped key", () => {
|
|
515
|
+
// The prefix is a stored format, not an internal detail. It reads like a
|
|
516
|
+
// leftover of the assistant id that used to be interpolated into it, so
|
|
517
|
+
// pin it: rows already written under `asst:self:` stop resolving if a
|
|
518
|
+
// later tidy-up drops it.
|
|
519
|
+
const { conversationId } = recordInbound("telegram", "chat-1", "msg-1");
|
|
520
|
+
|
|
521
|
+
expect(
|
|
522
|
+
getConversationByKey("asst:self:telegram:chat-1")?.conversationId,
|
|
523
|
+
).toBe(conversationId);
|
|
524
524
|
});
|
|
525
525
|
|
|
526
526
|
test("external bindings allow multiple Slack thread anchors per channel", () => {
|
|
@@ -877,13 +877,13 @@ describe("loadConfig startup behavior", () => {
|
|
|
877
877
|
raw.llm.defaultProvider,
|
|
878
878
|
);
|
|
879
879
|
expect(effective.balanced?.provider).toBe("openai");
|
|
880
|
-
expect(effective.balanced?.model).toBe("gpt-5.
|
|
880
|
+
expect(effective.balanced?.model).toBe("gpt-5.6-luna");
|
|
881
881
|
expect(effective.balanced?.provider_connection).toBe("openai-personal");
|
|
882
882
|
expect(effective.balanced?.source).toBe("managed");
|
|
883
883
|
expect(effective["quality-optimized"]?.provider).toBe("openai");
|
|
884
|
-
expect(effective["quality-optimized"]?.model).toBe("gpt-5.
|
|
884
|
+
expect(effective["quality-optimized"]?.model).toBe("gpt-5.6-sol");
|
|
885
885
|
expect(effective["cost-optimized"]?.provider).toBe("openai");
|
|
886
|
-
expect(effective["cost-optimized"]?.model).toBe("gpt-5.
|
|
886
|
+
expect(effective["cost-optimized"]?.model).toBe("gpt-5.6-luna");
|
|
887
887
|
});
|
|
888
888
|
|
|
889
889
|
test("off-platform hatch with a provider outside the named matrix columns resolves through the shared BYOK templates", () => {
|
|
@@ -398,11 +398,19 @@ describe("AssistantConfigSchema", () => {
|
|
|
398
398
|
}
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
-
test("
|
|
401
|
+
test("parses an unknown provider (read tolerance for entry names)", () => {
|
|
402
|
+
// The provider schema is an open string: a stored value outside the
|
|
403
|
+
// known set parses instead of stripping its profile, and dispatch
|
|
404
|
+
// resolves it as a connection entry name (or fails explainably).
|
|
405
|
+
// Write-time membership is enforced at the profiles route and the
|
|
406
|
+
// config-write choke point.
|
|
402
407
|
const result = AssistantConfigSchema.safeParse({
|
|
403
|
-
llm: { profiles: { custom: { provider: "
|
|
408
|
+
llm: { profiles: { custom: { provider: "anthropic-work" } } },
|
|
404
409
|
});
|
|
405
|
-
expect(result.success).toBe(
|
|
410
|
+
expect(result.success).toBe(true);
|
|
411
|
+
if (result.success) {
|
|
412
|
+
expect(result.data.llm.profiles.custom?.provider).toBe("anthropic-work");
|
|
413
|
+
}
|
|
406
414
|
});
|
|
407
415
|
|
|
408
416
|
test("accepts the vellum and chatgpt routing identities with a routable model", () => {
|
|
@@ -940,8 +948,6 @@ describe("AssistantConfigSchema", () => {
|
|
|
940
948
|
endpointDecisionTimeoutMs: 1200,
|
|
941
949
|
endpointExtensionMs: 1500,
|
|
942
950
|
endpointMaxExtensions: 2,
|
|
943
|
-
ackFirstDeltaTimeoutMs: 2500,
|
|
944
|
-
ackGenerationTimeoutMs: 600,
|
|
945
951
|
progress: {
|
|
946
952
|
enabled: true,
|
|
947
953
|
opsThreshold: 3,
|
|
@@ -952,6 +958,12 @@ describe("AssistantConfigSchema", () => {
|
|
|
952
958
|
generationTimeoutMs: 1500,
|
|
953
959
|
},
|
|
954
960
|
},
|
|
961
|
+
flux: {
|
|
962
|
+
turnEnd: { enabled: false },
|
|
963
|
+
model: "flux-general-en",
|
|
964
|
+
eotThreshold: 0.7,
|
|
965
|
+
eotTimeoutMs: 5000,
|
|
966
|
+
},
|
|
955
967
|
maxSessionDurationSeconds: 1800,
|
|
956
968
|
archiveAudio: false,
|
|
957
969
|
});
|
|
@@ -976,6 +988,8 @@ describe("AssistantConfigSchema", () => {
|
|
|
976
988
|
// Unspecified vad fields still get defaults
|
|
977
989
|
expect(result.liveVoice.vad.maxTurnDurationMs).toBe(30000);
|
|
978
990
|
expect(result.liveVoice.maxSessionDurationSeconds).toBe(900);
|
|
991
|
+
// A partial liveVoice override leaves Flux turn-end disabled by default.
|
|
992
|
+
expect(result.liveVoice.flux.turnEnd.enabled).toBe(false);
|
|
979
993
|
});
|
|
980
994
|
|
|
981
995
|
test("accepts a liveVoice.vad.bargeInMinSpeechMs of 0 (guard disabled)", () => {
|
|
@@ -2340,16 +2354,17 @@ describe("loadConfig with schema validation", () => {
|
|
|
2340
2354
|
expect(config.llm.callSites?.mainAgent?.maxTokens).toBe(4096);
|
|
2341
2355
|
});
|
|
2342
2356
|
|
|
2343
|
-
test("
|
|
2344
|
-
//
|
|
2345
|
-
//
|
|
2357
|
+
test("keeps an unknown provider on load (read tolerance for entry names)", () => {
|
|
2358
|
+
// The open provider schema keeps the whole profile: an unknown value is
|
|
2359
|
+
// resolved as a connection entry name at dispatch (or fails there
|
|
2360
|
+
// explainably) rather than being leaf-stripped on read.
|
|
2346
2361
|
writeConfig({
|
|
2347
2362
|
llm: {
|
|
2348
|
-
profiles: { custom: { provider: "
|
|
2363
|
+
profiles: { custom: { provider: "unknown-provider", model: "gpt-4" } },
|
|
2349
2364
|
},
|
|
2350
2365
|
});
|
|
2351
2366
|
const config = loadConfig();
|
|
2352
|
-
expect(config.llm.profiles.custom?.provider).
|
|
2367
|
+
expect(config.llm.profiles.custom?.provider).toBe("unknown-provider");
|
|
2353
2368
|
expect(config.llm.profiles.custom?.model).toBe("gpt-4");
|
|
2354
2369
|
});
|
|
2355
2370
|
|
|
@@ -325,6 +325,8 @@ mock.module("../persistence/llm-request-log-store.js", () => ({
|
|
|
325
325
|
|
|
326
326
|
import type { Conversation } from "../daemon/conversation.js";
|
|
327
327
|
import { runAgentLoopImpl } from "../daemon/conversation-agent-loop.js";
|
|
328
|
+
import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
|
|
329
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
328
330
|
|
|
329
331
|
// ── Test helpers ─────────────────────────────────────────────────────
|
|
330
332
|
|
|
@@ -363,7 +365,7 @@ function makeCtx(
|
|
|
363
365
|
];
|
|
364
366
|
};
|
|
365
367
|
|
|
366
|
-
return {
|
|
368
|
+
return asConversation({
|
|
367
369
|
conversationId: "test-conv",
|
|
368
370
|
messages: [
|
|
369
371
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
@@ -433,12 +435,7 @@ function makeCtx(
|
|
|
433
435
|
skillProjectionCache:
|
|
434
436
|
new Map() as unknown as Conversation["skillProjectionCache"],
|
|
435
437
|
|
|
436
|
-
usageStats: {
|
|
437
|
-
totalInputTokens: 0,
|
|
438
|
-
totalOutputTokens: 0,
|
|
439
|
-
totalEstimatedCost: 0,
|
|
440
|
-
model: "",
|
|
441
|
-
},
|
|
438
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
442
439
|
turnCount: 0,
|
|
443
440
|
|
|
444
441
|
lastAssistantAttachments: [],
|
|
@@ -456,12 +453,12 @@ function makeCtx(
|
|
|
456
453
|
getQueueDepth: () => 0,
|
|
457
454
|
hasQueuedMessages: () => false,
|
|
458
455
|
canHandoffAtCheckpoint: () => false,
|
|
459
|
-
drainQueue: (_reason?:
|
|
456
|
+
drainQueue: async (_reason?: QueueDrainReason) => {},
|
|
460
457
|
// Forwards to drainQueue so tests that spy the drain observe the agent
|
|
461
458
|
// loop's post-turn kick through the guarded entry point.
|
|
462
459
|
kickDrainQueue(
|
|
463
|
-
this: { drainQueue: (reason?:
|
|
464
|
-
reason:
|
|
460
|
+
this: { drainQueue: (reason?: QueueDrainReason) => Promise<void> },
|
|
461
|
+
reason: QueueDrainReason = "loop_complete",
|
|
465
462
|
_origin?: string,
|
|
466
463
|
) {
|
|
467
464
|
return this.drainQueue(reason);
|
|
@@ -493,7 +490,7 @@ function makeCtx(
|
|
|
493
490
|
} as unknown as Conversation["graphMemory"],
|
|
494
491
|
|
|
495
492
|
...overrides,
|
|
496
|
-
}
|
|
493
|
+
});
|
|
497
494
|
}
|
|
498
495
|
|
|
499
496
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
@@ -472,6 +472,8 @@ mock.module("../memory/archive-store.js", () => ({
|
|
|
472
472
|
import { AgentLoop } from "../agent/loop.js";
|
|
473
473
|
import type { Conversation } from "../daemon/conversation.js";
|
|
474
474
|
import { runAgentLoopImpl } from "../daemon/conversation-agent-loop.js";
|
|
475
|
+
import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
|
|
476
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
475
477
|
import {
|
|
476
478
|
createMockProvider,
|
|
477
479
|
type ScriptedResponse,
|
|
@@ -519,7 +521,7 @@ function makeCtx(
|
|
|
519
521
|
toolExecutor,
|
|
520
522
|
});
|
|
521
523
|
|
|
522
|
-
const ctx = {
|
|
524
|
+
const ctx = asConversation({
|
|
523
525
|
conversationId: "test-conv",
|
|
524
526
|
messages: [
|
|
525
527
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
@@ -572,12 +574,7 @@ function makeCtx(
|
|
|
572
574
|
skillProjectionCache:
|
|
573
575
|
new Map() as unknown as Conversation["skillProjectionCache"],
|
|
574
576
|
|
|
575
|
-
usageStats: {
|
|
576
|
-
totalInputTokens: 0,
|
|
577
|
-
totalOutputTokens: 0,
|
|
578
|
-
totalEstimatedCost: 0,
|
|
579
|
-
model: "",
|
|
580
|
-
},
|
|
577
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
581
578
|
turnCount: 0,
|
|
582
579
|
|
|
583
580
|
lastAssistantAttachments: [],
|
|
@@ -595,12 +592,12 @@ function makeCtx(
|
|
|
595
592
|
getQueueDepth: () => 0,
|
|
596
593
|
hasQueuedMessages: () => false,
|
|
597
594
|
canHandoffAtCheckpoint: () => false,
|
|
598
|
-
drainQueue: (_reason?:
|
|
595
|
+
drainQueue: async (_reason?: QueueDrainReason) => {},
|
|
599
596
|
// Forwards to drainQueue so tests that spy the drain observe the agent
|
|
600
597
|
// loop's post-turn kick through the guarded entry point.
|
|
601
598
|
kickDrainQueue(
|
|
602
|
-
this: { drainQueue: (reason?:
|
|
603
|
-
reason:
|
|
599
|
+
this: { drainQueue: (reason?: QueueDrainReason) => Promise<void> },
|
|
600
|
+
reason: QueueDrainReason = "loop_complete",
|
|
604
601
|
_origin?: string,
|
|
605
602
|
) {
|
|
606
603
|
return this.drainQueue(reason);
|
|
@@ -632,7 +629,7 @@ function makeCtx(
|
|
|
632
629
|
} as unknown as Conversation["graphMemory"],
|
|
633
630
|
|
|
634
631
|
...ctxOverrides,
|
|
635
|
-
}
|
|
632
|
+
});
|
|
636
633
|
// The convergence driver resolves the turn-scoped overflow ladder off the
|
|
637
634
|
// manager; give every fake manager the ladder methods unless a test supplied
|
|
638
635
|
// its own.
|
|
@@ -268,7 +268,18 @@ const setConversationHistoryStrippedAtMock = mock(
|
|
|
268
268
|
const updateConversationSlackContextWatermarkMock = mock(
|
|
269
269
|
(_conversationId: string, _watermarkTs: string, _compactedAt?: number) => {},
|
|
270
270
|
);
|
|
271
|
-
|
|
271
|
+
interface MockConversationRow {
|
|
272
|
+
conversationType?: string;
|
|
273
|
+
source?: string;
|
|
274
|
+
contextSummary?: string | null;
|
|
275
|
+
contextCompactedMessageCount?: number;
|
|
276
|
+
contextCompactedAt?: number | null;
|
|
277
|
+
slackContextCompactionWatermarkTs?: string | null;
|
|
278
|
+
lastNotifiedInferenceProfile?: string | null;
|
|
279
|
+
processingStartedAt?: string | null;
|
|
280
|
+
[key: string]: unknown;
|
|
281
|
+
}
|
|
282
|
+
let mockConversationRow: MockConversationRow = {
|
|
272
283
|
id: "conv-1",
|
|
273
284
|
createdAt: 1_700_000_000_000,
|
|
274
285
|
contextSummary: null,
|
|
@@ -675,7 +686,9 @@ import {
|
|
|
675
686
|
applyCompactionResult,
|
|
676
687
|
runAgentLoopImpl,
|
|
677
688
|
} from "../daemon/conversation-agent-loop.js";
|
|
689
|
+
import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
|
|
678
690
|
import { settleTurnTail } from "../daemon/turn-tail-chain.js";
|
|
691
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
679
692
|
import {
|
|
680
693
|
createMockProvider,
|
|
681
694
|
type ScriptedResponse,
|
|
@@ -731,7 +744,7 @@ function makeCtx(
|
|
|
731
744
|
toolExecutor,
|
|
732
745
|
});
|
|
733
746
|
|
|
734
|
-
const ctx = {
|
|
747
|
+
const ctx = asConversation({
|
|
735
748
|
conversationId: "test-conv",
|
|
736
749
|
messages: [
|
|
737
750
|
{ role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
@@ -791,12 +804,7 @@ function makeCtx(
|
|
|
791
804
|
skillProjectionCache:
|
|
792
805
|
new Map() as unknown as Conversation["skillProjectionCache"],
|
|
793
806
|
|
|
794
|
-
usageStats: {
|
|
795
|
-
totalInputTokens: 0,
|
|
796
|
-
totalOutputTokens: 0,
|
|
797
|
-
totalEstimatedCost: 0,
|
|
798
|
-
model: "",
|
|
799
|
-
},
|
|
807
|
+
usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
|
|
800
808
|
turnCount: 0,
|
|
801
809
|
|
|
802
810
|
lastAssistantAttachments: [],
|
|
@@ -814,12 +822,12 @@ function makeCtx(
|
|
|
814
822
|
getQueueDepth: () => 0,
|
|
815
823
|
hasQueuedMessages: () => false,
|
|
816
824
|
canHandoffAtCheckpoint: () => false,
|
|
817
|
-
drainQueue: (_reason?:
|
|
825
|
+
drainQueue: async (_reason?: QueueDrainReason) => {},
|
|
818
826
|
// Forwards to drainQueue so tests that spy the drain observe the agent
|
|
819
827
|
// loop's post-turn kick through the guarded entry point.
|
|
820
828
|
kickDrainQueue(
|
|
821
|
-
this: { drainQueue: (reason?:
|
|
822
|
-
reason:
|
|
829
|
+
this: { drainQueue: (reason?: QueueDrainReason) => Promise<void> },
|
|
830
|
+
reason: QueueDrainReason = "loop_complete",
|
|
823
831
|
_origin?: string,
|
|
824
832
|
) {
|
|
825
833
|
return this.drainQueue(reason);
|
|
@@ -851,7 +859,7 @@ function makeCtx(
|
|
|
851
859
|
} as unknown as Conversation["graphMemory"],
|
|
852
860
|
|
|
853
861
|
...ctxOverrides,
|
|
854
|
-
}
|
|
862
|
+
});
|
|
855
863
|
// Reactive overflow recovery resolves the turn-scoped reduction ladder off
|
|
856
864
|
// the manager; give every fake manager the ladder methods unless a test
|
|
857
865
|
// supplied its own.
|
|
@@ -1795,7 +1803,7 @@ describe("session-agent-loop", () => {
|
|
|
1795
1803
|
],
|
|
1796
1804
|
toolExecutor: async () => ({ content: "file content", isError: false }),
|
|
1797
1805
|
canHandoffAtCheckpoint: () => true,
|
|
1798
|
-
}
|
|
1806
|
+
});
|
|
1799
1807
|
|
|
1800
1808
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1801
1809
|
|
|
@@ -1826,7 +1834,7 @@ describe("session-agent-loop", () => {
|
|
|
1826
1834
|
],
|
|
1827
1835
|
toolExecutor: async () => ({ content: "content", isError: false }),
|
|
1828
1836
|
canHandoffAtCheckpoint: () => false,
|
|
1829
|
-
}
|
|
1837
|
+
});
|
|
1830
1838
|
|
|
1831
1839
|
await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
|
|
1832
1840
|
|
|
@@ -1975,13 +1983,13 @@ describe("session-agent-loop", () => {
|
|
|
1975
1983
|
|
|
1976
1984
|
test("drains queue after completion", async () => {
|
|
1977
1985
|
// GIVEN a real loop that answers in a single text turn
|
|
1978
|
-
let drainReason:
|
|
1986
|
+
let drainReason: QueueDrainReason | undefined;
|
|
1979
1987
|
const ctx = makeCtx({
|
|
1980
1988
|
providerResponses: [textResponse("ok")],
|
|
1981
|
-
drainQueue: (reason
|
|
1989
|
+
drainQueue: async (reason?: QueueDrainReason) => {
|
|
1982
1990
|
drainReason = reason;
|
|
1983
1991
|
},
|
|
1984
|
-
}
|
|
1992
|
+
});
|
|
1985
1993
|
|
|
1986
1994
|
// WHEN the orchestrator runs the turn to completion
|
|
1987
1995
|
await runAgentLoopImpl(ctx, "hi", "msg-1", () => {});
|
|
@@ -2018,10 +2026,10 @@ describe("session-agent-loop", () => {
|
|
|
2018
2026
|
abortController,
|
|
2019
2027
|
// Fire the watchdog quickly instead of the ~45s production default.
|
|
2020
2028
|
abortWatchdogMs: 30,
|
|
2021
|
-
drainQueue: (reason
|
|
2029
|
+
drainQueue: async (reason?: QueueDrainReason) => {
|
|
2022
2030
|
drainReason = reason;
|
|
2023
2031
|
},
|
|
2024
|
-
}
|
|
2032
|
+
});
|
|
2025
2033
|
|
|
2026
2034
|
try {
|
|
2027
2035
|
// WHEN the orchestrator runs the turn
|
|
@@ -2520,7 +2528,7 @@ describe("session-agent-loop", () => {
|
|
|
2520
2528
|
},
|
|
2521
2529
|
],
|
|
2522
2530
|
toolExecutor: async () => ({ content: "file content", isError: false }),
|
|
2523
|
-
}
|
|
2531
|
+
});
|
|
2524
2532
|
|
|
2525
2533
|
await runAgentLoopImpl(ctx, "hello", "msg-user-daily", () => {});
|
|
2526
2534
|
|
|
@@ -832,3 +832,66 @@ describe("conversation-attention-store", () => {
|
|
|
832
832
|
});
|
|
833
833
|
});
|
|
834
834
|
});
|
|
835
|
+
|
|
836
|
+
describe("attention watermarks and streaming rows", () => {
|
|
837
|
+
test("state init from an upgraded database anchors on the latest finalized reply", () => {
|
|
838
|
+
clearTables();
|
|
839
|
+
ensureConversation("conv-1");
|
|
840
|
+
insertAssistantMessage("conv-1", "msg-done", 1_000);
|
|
841
|
+
getDb()
|
|
842
|
+
.insert(messages)
|
|
843
|
+
.values({
|
|
844
|
+
id: "msg-streaming",
|
|
845
|
+
conversationId: "conv-1",
|
|
846
|
+
role: "assistant",
|
|
847
|
+
content: "still being written",
|
|
848
|
+
createdAt: 2_000,
|
|
849
|
+
metadata: null,
|
|
850
|
+
finalized: 0,
|
|
851
|
+
})
|
|
852
|
+
.run();
|
|
853
|
+
|
|
854
|
+
// No attention row exists, so the seen signal initializes state from the
|
|
855
|
+
// messages table. The streaming row is newest but is not a reply yet;
|
|
856
|
+
// anchoring the watermark on it would classify the finished reply as
|
|
857
|
+
// already seen the moment it completes.
|
|
858
|
+
recordConversationSeenSignal({
|
|
859
|
+
conversationId: "conv-1",
|
|
860
|
+
sourceChannel: "vellum",
|
|
861
|
+
signalType: "macos_conversation_opened",
|
|
862
|
+
confidence: "explicit",
|
|
863
|
+
source: "desktop-client",
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
const state = getAttentionStateByConversationIds(["conv-1"]).get("conv-1")!;
|
|
867
|
+
expect(state.latestAssistantMessageId).toBe("msg-done");
|
|
868
|
+
expect(state.lastSeenAssistantMessageId).toBe("msg-done");
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
describe("markConversationUnread with only a streaming reply", () => {
|
|
873
|
+
test("reports no assistant message rather than rewinding onto the streaming row", () => {
|
|
874
|
+
clearTables();
|
|
875
|
+
ensureConversation("conv-1");
|
|
876
|
+
getDb()
|
|
877
|
+
.insert(messages)
|
|
878
|
+
.values({
|
|
879
|
+
id: "msg-streaming-only",
|
|
880
|
+
conversationId: "conv-1",
|
|
881
|
+
role: "assistant",
|
|
882
|
+
content: "still being written",
|
|
883
|
+
createdAt: 1_000,
|
|
884
|
+
metadata: null,
|
|
885
|
+
finalized: 0,
|
|
886
|
+
})
|
|
887
|
+
.run();
|
|
888
|
+
|
|
889
|
+
// The write-path projection anchors at the finalize seam
|
|
890
|
+
// (`reserveMessage` deliberately skips it), so attention state for this
|
|
891
|
+
// conversation says "no reply yet". The rewind fallback must agree with
|
|
892
|
+
// the projection rather than anchoring on the streaming row.
|
|
893
|
+
expect(() => markConversationUnread("conv-1")).toThrow(
|
|
894
|
+
/no assistant message/i,
|
|
895
|
+
);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
@@ -10,10 +10,6 @@ mock.module("../config/env.js", () => ({
|
|
|
10
10
|
hasUngatedHttpAuthDisabled: () => false,
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
-
mock.module("../daemon/conversation-store.js", () => ({
|
|
14
|
-
destroyActiveConversation: () => {},
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
13
|
mock.module("../daemon/handlers/conversations.js", () => ({
|
|
18
14
|
cancelGeneration: () => true,
|
|
19
15
|
clearAllConversations: async () => 0,
|