@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
|
@@ -1794,3 +1794,72 @@ describe("forkConversation + memory_retrospective_state", () => {
|
|
|
1794
1794
|
expect(fork.slackContextCompactionWatermarkAt).toBe(compactedAt);
|
|
1795
1795
|
});
|
|
1796
1796
|
});
|
|
1797
|
+
|
|
1798
|
+
describe("forkConversation with unfinalized rows", () => {
|
|
1799
|
+
beforeEach(() => {
|
|
1800
|
+
resetTables();
|
|
1801
|
+
});
|
|
1802
|
+
|
|
1803
|
+
test("skips unfinalized rows and anchors the fork at the last finalized message", async () => {
|
|
1804
|
+
const source = createConversation("Mid-turn fork");
|
|
1805
|
+
await addMessage(source.id, "user", "question", { skipIndexing: true });
|
|
1806
|
+
const lastFinalized = await addMessage(
|
|
1807
|
+
source.id,
|
|
1808
|
+
"assistant",
|
|
1809
|
+
"completed reply",
|
|
1810
|
+
{ skipIndexing: true },
|
|
1811
|
+
);
|
|
1812
|
+
const streaming = await addMessage(
|
|
1813
|
+
source.id,
|
|
1814
|
+
"assistant",
|
|
1815
|
+
"partial reply still being written",
|
|
1816
|
+
{ skipIndexing: true },
|
|
1817
|
+
);
|
|
1818
|
+
rawRun(
|
|
1819
|
+
"test:markUnfinalized",
|
|
1820
|
+
"UPDATE messages SET finalized = 0 WHERE id = ?",
|
|
1821
|
+
streaming.id,
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
1825
|
+
const forkMessages = getMessages(fork.id);
|
|
1826
|
+
|
|
1827
|
+
// The invariant, not mere presence: exactly the finalized rows were
|
|
1828
|
+
// copied, every copy reads as finalized, no copy descends from the
|
|
1829
|
+
// unfinalized source row, and the lineage anchor is a row the fork
|
|
1830
|
+
// actually holds.
|
|
1831
|
+
expect(forkMessages).toHaveLength(2);
|
|
1832
|
+
expect(forkMessages.every((row) => row.finalized === 1)).toBe(true);
|
|
1833
|
+
const forkedFromIds = forkMessages.map(
|
|
1834
|
+
(row) =>
|
|
1835
|
+
(parseMetadata(row.metadata) as { forkSourceMessageId?: string })
|
|
1836
|
+
?.forkSourceMessageId,
|
|
1837
|
+
);
|
|
1838
|
+
expect(forkedFromIds).not.toContain(streaming.id);
|
|
1839
|
+
expect(fork.forkParentMessageId).toBe(lastFinalized.id);
|
|
1840
|
+
// The source still owns its in-flight row untouched.
|
|
1841
|
+
expect(getMessages(source.id)).toHaveLength(3);
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1844
|
+
test("a stale mid-history unfinalized row is skipped without shifting the anchor", async () => {
|
|
1845
|
+
const source = createConversation("Stale row fork");
|
|
1846
|
+
await addMessage(source.id, "user", "first", { skipIndexing: true });
|
|
1847
|
+
const stale = await addMessage(source.id, "assistant", "orphaned partial", {
|
|
1848
|
+
skipIndexing: true,
|
|
1849
|
+
});
|
|
1850
|
+
const tail = await addMessage(source.id, "user", "latest", {
|
|
1851
|
+
skipIndexing: true,
|
|
1852
|
+
});
|
|
1853
|
+
rawRun(
|
|
1854
|
+
"test:markUnfinalized",
|
|
1855
|
+
"UPDATE messages SET finalized = 0 WHERE id = ?",
|
|
1856
|
+
stale.id,
|
|
1857
|
+
);
|
|
1858
|
+
|
|
1859
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
1860
|
+
const forkMessages = getMessages(fork.id);
|
|
1861
|
+
|
|
1862
|
+
expect(forkMessages).toHaveLength(2);
|
|
1863
|
+
expect(fork.forkParentMessageId).toBe(tail.id);
|
|
1864
|
+
});
|
|
1865
|
+
});
|
|
@@ -350,3 +350,70 @@ describe("referential forking", () => {
|
|
|
350
350
|
expect(textOf(getMessages(source.id))).toHaveLength(4);
|
|
351
351
|
});
|
|
352
352
|
});
|
|
353
|
+
|
|
354
|
+
describe("referential forking with unfinalized rows", () => {
|
|
355
|
+
beforeEach(() => {
|
|
356
|
+
resetTables();
|
|
357
|
+
clearConfig();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("ancestor rows are hidden while unfinalized and appear once finalized", async () => {
|
|
361
|
+
const source = await seedSource("Launch");
|
|
362
|
+
const midRow = getMessages(source.id)[1]!;
|
|
363
|
+
getDb()
|
|
364
|
+
.update(messages)
|
|
365
|
+
.set({ finalized: 0 })
|
|
366
|
+
.where(eq(messages.id, midRow.id))
|
|
367
|
+
.run();
|
|
368
|
+
|
|
369
|
+
setForkStrategy("reference");
|
|
370
|
+
const fork = await forkConversationForRetrospective({
|
|
371
|
+
conversationId: source.id,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// The fork's lineage read excludes the ancestor's in-flight row; the
|
|
375
|
+
// source's own read keeps it.
|
|
376
|
+
expect(textOf(getMessages(fork.id))).toEqual([
|
|
377
|
+
"draft a launch plan",
|
|
378
|
+
"tweak the timeline",
|
|
379
|
+
"updated",
|
|
380
|
+
]);
|
|
381
|
+
expect(getMessages(source.id)).toHaveLength(4);
|
|
382
|
+
|
|
383
|
+
getDb()
|
|
384
|
+
.update(messages)
|
|
385
|
+
.set({ finalized: 1 })
|
|
386
|
+
.where(eq(messages.id, midRow.id))
|
|
387
|
+
.run();
|
|
388
|
+
|
|
389
|
+
// Once the row finalizes it enters the inherited window on its own.
|
|
390
|
+
expect(textOf(getMessages(fork.id))).toEqual([
|
|
391
|
+
"draft a launch plan",
|
|
392
|
+
"here is a first pass",
|
|
393
|
+
"tweak the timeline",
|
|
394
|
+
"updated",
|
|
395
|
+
]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("an unfinalized tail is not the fork anchor", async () => {
|
|
399
|
+
const source = await seedSource("Launch");
|
|
400
|
+
const tail = getMessages(source.id).at(-1)!;
|
|
401
|
+
getDb()
|
|
402
|
+
.update(messages)
|
|
403
|
+
.set({ finalized: 0 })
|
|
404
|
+
.where(eq(messages.id, tail.id))
|
|
405
|
+
.run();
|
|
406
|
+
|
|
407
|
+
setForkStrategy("reference");
|
|
408
|
+
const fork = await forkConversationForRetrospective({
|
|
409
|
+
conversationId: source.id,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(fork.forkParentMessageId).not.toBe(tail.id);
|
|
413
|
+
expect(textOf(getMessages(fork.id))).toEqual([
|
|
414
|
+
"draft a launch plan",
|
|
415
|
+
"here is a first pass",
|
|
416
|
+
"tweak the timeline",
|
|
417
|
+
]);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
@@ -723,3 +723,27 @@ describe("forkConversationForRetrospective — compacted source", () => {
|
|
|
723
723
|
});
|
|
724
724
|
});
|
|
725
725
|
});
|
|
726
|
+
|
|
727
|
+
describe("forkConversationForRetrospective with unfinalized rows", () => {
|
|
728
|
+
beforeEach(() => {
|
|
729
|
+
resetTables();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("skips the unfinalized tail and anchors at the last finalized message", async () => {
|
|
733
|
+
const source = await seedSource("Mid-turn retro");
|
|
734
|
+
const tail = getMessages(source.id).at(-1)!;
|
|
735
|
+
const db = getDb();
|
|
736
|
+
db.run(`UPDATE messages SET finalized = 0 WHERE id = '${tail.id}'`);
|
|
737
|
+
|
|
738
|
+
const fork = await forkConversationForRetrospective({
|
|
739
|
+
conversationId: source.id,
|
|
740
|
+
});
|
|
741
|
+
const forkMessages = getMessages(fork.id);
|
|
742
|
+
|
|
743
|
+
// Invariant: only finalized rows are copied, and the lineage anchor is a
|
|
744
|
+
// row the fork actually holds.
|
|
745
|
+
expect(forkMessages).toHaveLength(3);
|
|
746
|
+
expect(forkMessages.every((row) => row.finalized === 1)).toBe(true);
|
|
747
|
+
expect(fork.forkParentMessageId).not.toBe(tail.id);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live-call question notifier persists its row after the voice turn has
|
|
3
|
+
* settled. The per-turn trust field is not cleared at release, so at that
|
|
4
|
+
* moment it can still hold the finished turn's actor while voice cleanup has
|
|
5
|
+
* restored the conversation's resting trust. The row must carry the owner,
|
|
6
|
+
* not the stale turn actor.
|
|
7
|
+
*/
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
const added: Array<{ metadata?: Record<string, unknown> }> = [];
|
|
11
|
+
mock.module("../persistence/conversation-crud.js", () => ({
|
|
12
|
+
addMessage: async (
|
|
13
|
+
_conversationId: string,
|
|
14
|
+
_role: string,
|
|
15
|
+
_content: string,
|
|
16
|
+
options?: { metadata?: Record<string, unknown> },
|
|
17
|
+
) => {
|
|
18
|
+
added.push({ metadata: options?.metadata });
|
|
19
|
+
return { id: "msg-1", deduplicated: false };
|
|
20
|
+
},
|
|
21
|
+
provenanceFromTrustContext: (ctx?: { trustClass?: string }) =>
|
|
22
|
+
ctx ? { provenanceTrustClass: ctx.trustClass } : {},
|
|
23
|
+
}));
|
|
24
|
+
mock.module("../calls/call-store.js", () => ({
|
|
25
|
+
getCallSession: () => ({ toNumber: "+15550100" }),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { fireCallQuestionNotifier } from "../calls/call-state.js";
|
|
29
|
+
import { registerConversationNotifiers } from "../daemon/conversation-notifiers.js";
|
|
30
|
+
|
|
31
|
+
describe("live-call question provenance", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
added.length = 0;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a row persisted after the turn settled carries the owner, not the stale turn actor", async () => {
|
|
37
|
+
registerConversationNotifiers("conv-n1", {
|
|
38
|
+
sendToClient: () => {},
|
|
39
|
+
messages: [],
|
|
40
|
+
// Voice cleanup restored the owner; the per-turn field still holds the
|
|
41
|
+
// finished voice turn's contact.
|
|
42
|
+
trustContext: {
|
|
43
|
+
trustClass: "guardian",
|
|
44
|
+
sourceChannel: "vellum",
|
|
45
|
+
},
|
|
46
|
+
currentTurnTrustContext: {
|
|
47
|
+
trustClass: "trusted_contact",
|
|
48
|
+
sourceChannel: "phone",
|
|
49
|
+
requesterExternalUserId: "caller-1",
|
|
50
|
+
},
|
|
51
|
+
} as never);
|
|
52
|
+
|
|
53
|
+
fireCallQuestionNotifier("conv-n1", "sess-1", "Which slot works?");
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
55
|
+
|
|
56
|
+
expect(added.length).toBe(1);
|
|
57
|
+
expect(added[0]?.metadata?.provenanceTrustClass).toBe("guardian");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from "../agent/loop.js";
|
|
12
12
|
import type { AssistantEvent } from "../api/index.js";
|
|
13
13
|
import type { Message, ProviderResponse } from "../providers/types.js";
|
|
14
|
+
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
14
15
|
import { stampAndBuffer } from "../runtime/assistant-stream-state.js";
|
|
15
16
|
import { createAbortReason } from "../util/abort-reasons.js";
|
|
16
17
|
import { setConfig } from "./helpers/set-config.js";
|
|
@@ -79,10 +80,12 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
79
80
|
setConversationOriginInterfaceIfUnset: () => {},
|
|
80
81
|
updateConversationContextWindow: () => {},
|
|
81
82
|
deleteMessageById: () => {},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// Mirrors the real mapping so provenance assertions observe what production
|
|
84
|
+
// would stamp, rather than a placeholder.
|
|
85
|
+
provenanceFromTrustContext: (ctx?: { trustClass?: string }) =>
|
|
86
|
+
ctx
|
|
87
|
+
? { provenanceTrustClass: ctx.trustClass }
|
|
88
|
+
: { provenanceTrustClass: "unknown" },
|
|
86
89
|
getConversationOriginInterface: () => null,
|
|
87
90
|
getConversationOriginChannel: () => null,
|
|
88
91
|
getMessages: () => [],
|
|
@@ -597,6 +600,55 @@ describe("Conversation message queue", () => {
|
|
|
597
600
|
await new Promise((r) => setTimeout(r, 10));
|
|
598
601
|
});
|
|
599
602
|
|
|
603
|
+
test("a drained slash exchange is stamped with its sender's provenance, not the slot's", async () => {
|
|
604
|
+
// Provenance gates memory extraction and untrusted-content wrapping, so
|
|
605
|
+
// the stamped class must be the actor whose message this is. The unknown-
|
|
606
|
+
// slash drain branch persists an exchange for the queued sender; if the
|
|
607
|
+
// guardian wrote the conversation slot while that message waited, the
|
|
608
|
+
// rows must still carry the sender's class.
|
|
609
|
+
const conversation = makeConversation();
|
|
610
|
+
await conversation.loadFromDb();
|
|
611
|
+
|
|
612
|
+
conversation.setTrustContext({
|
|
613
|
+
trustClass: "trusted_contact",
|
|
614
|
+
sourceChannel: "slack",
|
|
615
|
+
requesterExternalUserId: "U-contact",
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const p1 = conversation.processMessage({
|
|
619
|
+
content: "msg-1",
|
|
620
|
+
attachments: [],
|
|
621
|
+
onEvent: () => {},
|
|
622
|
+
requestId: "req-1",
|
|
623
|
+
});
|
|
624
|
+
await waitForPendingRun(1);
|
|
625
|
+
|
|
626
|
+
conversation.enqueueMessage({
|
|
627
|
+
content: "/definitely-not-a-command",
|
|
628
|
+
requestId: "req-slash",
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// The guardian moves the slot while the contact's message waits.
|
|
632
|
+
conversation.setTrustContext({
|
|
633
|
+
trustClass: "guardian",
|
|
634
|
+
sourceChannel: "vellum",
|
|
635
|
+
requesterExternalUserId: "guardian-principal",
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
capturedAddMessages.length = 0;
|
|
639
|
+
await resolveRun(0);
|
|
640
|
+
await p1;
|
|
641
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
642
|
+
|
|
643
|
+
const exchange = capturedAddMessages.filter((m) =>
|
|
644
|
+
m.content.includes("definitely-not-a-command"),
|
|
645
|
+
);
|
|
646
|
+
expect(exchange.length).toBeGreaterThan(0);
|
|
647
|
+
for (const row of exchange) {
|
|
648
|
+
expect(row.metadata?.provenanceTrustClass).toBe("trusted_contact");
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
600
652
|
test("a drained turn keeps its sender's trust once the agent loop is running", async () => {
|
|
601
653
|
// Exercises the real drain -> runAgentLoop ordering against a live
|
|
602
654
|
// Conversation. The loop re-initializes the per-turn trust snapshot on
|
|
@@ -3872,3 +3924,124 @@ describe("subagent notification user_message_echo suppression", () => {
|
|
|
3872
3924
|
await new Promise((r) => setTimeout(r, 10));
|
|
3873
3925
|
});
|
|
3874
3926
|
});
|
|
3927
|
+
|
|
3928
|
+
describe("interactive drain sender rebind", () => {
|
|
3929
|
+
beforeEach(() => {
|
|
3930
|
+
pendingRuns = [];
|
|
3931
|
+
});
|
|
3932
|
+
|
|
3933
|
+
test("a drained interactive message rebinds the hub sender for its turn, then restores the pre-drain state", async () => {
|
|
3934
|
+
const conversation = makeConversation();
|
|
3935
|
+
await conversation.loadFromDb();
|
|
3936
|
+
|
|
3937
|
+
const p1 = conversation.processMessage({
|
|
3938
|
+
content: "msg-1",
|
|
3939
|
+
attachments: [],
|
|
3940
|
+
onEvent: () => {},
|
|
3941
|
+
requestId: "req-1",
|
|
3942
|
+
});
|
|
3943
|
+
await waitForPendingRun(1);
|
|
3944
|
+
|
|
3945
|
+
// Queue an interactive item the way the /v1/messages route does when the
|
|
3946
|
+
// conversation is busy.
|
|
3947
|
+
const result = conversation.enqueueMessage({
|
|
3948
|
+
content: "msg-2",
|
|
3949
|
+
requestId: "req-2",
|
|
3950
|
+
onEvent: () => {},
|
|
3951
|
+
isInteractive: true,
|
|
3952
|
+
});
|
|
3953
|
+
expect(result.queued).toBe(true);
|
|
3954
|
+
|
|
3955
|
+
// The interactive turn that finishes ahead of the drain resets the
|
|
3956
|
+
// conversation-level sender to a no-op (process-message.ts contract).
|
|
3957
|
+
// The drained turn must not inherit that dead sender: the
|
|
3958
|
+
// PermissionPrompter reads the conversation-level sender, so a
|
|
3959
|
+
// confirmation_request raised during the drained turn would otherwise
|
|
3960
|
+
// reach no client and hang until the permission timeout.
|
|
3961
|
+
conversation.updateClient(() => {}, true);
|
|
3962
|
+
|
|
3963
|
+
await resolveRun(0);
|
|
3964
|
+
await p1;
|
|
3965
|
+
await waitForPendingRun(2);
|
|
3966
|
+
|
|
3967
|
+
expect(conversation.getCurrentSender()).toBe(broadcastMessage);
|
|
3968
|
+
expect(conversation.hasNoClient).toBe(false);
|
|
3969
|
+
|
|
3970
|
+
await resolveRun(1);
|
|
3971
|
+
await waitForCondition(
|
|
3972
|
+
() => conversation.getCurrentSender() !== broadcastMessage,
|
|
3973
|
+
);
|
|
3974
|
+
expect(conversation.hasNoClient).toBe(true);
|
|
3975
|
+
});
|
|
3976
|
+
|
|
3977
|
+
test("a drained interactive message restores a live pre-drain binding instead of clearing it", async () => {
|
|
3978
|
+
const conversation = makeConversation();
|
|
3979
|
+
await conversation.loadFromDb();
|
|
3980
|
+
|
|
3981
|
+
const p1 = conversation.processMessage({
|
|
3982
|
+
content: "msg-1",
|
|
3983
|
+
attachments: [],
|
|
3984
|
+
onEvent: () => {},
|
|
3985
|
+
requestId: "req-1",
|
|
3986
|
+
});
|
|
3987
|
+
await waitForPendingRun(1);
|
|
3988
|
+
|
|
3989
|
+
conversation.enqueueMessage({
|
|
3990
|
+
content: "msg-2",
|
|
3991
|
+
requestId: "req-2",
|
|
3992
|
+
onEvent: () => {},
|
|
3993
|
+
isInteractive: true,
|
|
3994
|
+
});
|
|
3995
|
+
|
|
3996
|
+
// A live binding predates the drained turn (the send route installs one
|
|
3997
|
+
// when a message is queued behind a non-interactive turn, which performs
|
|
3998
|
+
// no restore). The drain's restore must preserve it for out-of-turn
|
|
3999
|
+
// producers (call transcript and completion notifiers), not clear it.
|
|
4000
|
+
conversation.updateClient(broadcastMessage, false);
|
|
4001
|
+
|
|
4002
|
+
await resolveRun(0);
|
|
4003
|
+
await p1;
|
|
4004
|
+
await waitForPendingRun(2);
|
|
4005
|
+
|
|
4006
|
+
expect(conversation.getCurrentSender()).toBe(broadcastMessage);
|
|
4007
|
+
expect(conversation.hasNoClient).toBe(false);
|
|
4008
|
+
|
|
4009
|
+
await resolveRun(1);
|
|
4010
|
+
// The drained turn has ended; the pre-drain live binding must survive.
|
|
4011
|
+
await waitForCondition(() => !conversation.isProcessing());
|
|
4012
|
+
expect(conversation.getCurrentSender()).toBe(broadcastMessage);
|
|
4013
|
+
expect(conversation.hasNoClient).toBe(false);
|
|
4014
|
+
});
|
|
4015
|
+
|
|
4016
|
+
test("a drained non-interactive message leaves the no-op sender in place", async () => {
|
|
4017
|
+
const conversation = makeConversation();
|
|
4018
|
+
await conversation.loadFromDb();
|
|
4019
|
+
|
|
4020
|
+
const p1 = conversation.processMessage({
|
|
4021
|
+
content: "msg-1",
|
|
4022
|
+
attachments: [],
|
|
4023
|
+
onEvent: () => {},
|
|
4024
|
+
requestId: "req-1",
|
|
4025
|
+
});
|
|
4026
|
+
await waitForPendingRun(1);
|
|
4027
|
+
|
|
4028
|
+
conversation.enqueueMessage({
|
|
4029
|
+
content: "msg-2",
|
|
4030
|
+
requestId: "req-2",
|
|
4031
|
+
onEvent: () => {},
|
|
4032
|
+
isInteractive: false,
|
|
4033
|
+
});
|
|
4034
|
+
|
|
4035
|
+
conversation.updateClient(() => {}, true);
|
|
4036
|
+
|
|
4037
|
+
await resolveRun(0);
|
|
4038
|
+
await p1;
|
|
4039
|
+
await waitForPendingRun(2);
|
|
4040
|
+
|
|
4041
|
+
expect(conversation.getCurrentSender()).not.toBe(broadcastMessage);
|
|
4042
|
+
expect(conversation.hasNoClient).toBe(true);
|
|
4043
|
+
|
|
4044
|
+
await resolveRun(1);
|
|
4045
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
4046
|
+
});
|
|
4047
|
+
});
|