@vellumai/assistant 0.8.2 → 0.8.4
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 -12
- package/docker-entrypoint.sh +13 -2
- package/docker-init-apt-root.sh +79 -6
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +945 -36
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +112 -57
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +77 -3
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +7 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +340 -3
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +323 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +57 -101
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +208 -22
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +89 -37
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +124 -0
- package/src/config/feature-flag-registry.json +111 -23
- package/src/config/llm-resolver.ts +66 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +4 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +51 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +80 -13
- package/src/context/token-estimator.ts +72 -31
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
- package/src/daemon/conversation-agent-loop.ts +252 -56
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +144 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -12
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +449 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +80 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +67 -65
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +72 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/trust-context.ts +6 -0
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
- package/src/heartbeat/heartbeat-service.ts +35 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +20 -3
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +22 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +4 -22
- package/src/memory/memory-retrospective-job.ts +438 -21
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +20 -0
- package/src/memory/migrations/registry.ts +33 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +2 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +221 -17
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +69 -29
- package/src/memory/v2/page-index.ts +246 -19
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +18 -3
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +136 -27
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +118 -26
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +44 -45
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -76
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +51 -10
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +24 -21
- package/src/providers/inference/auth.ts +15 -3
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +50 -5
- package/src/providers/model-catalog.ts +244 -242
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +215 -25
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +46 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +212 -57
- package/src/runtime/auth/route-policy.ts +20 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +7 -51
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +130 -12
- package/src/runtime/routes/conversation-routes.ts +288 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +14 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
- package/src/runtime/routes/integrations/a2a.ts +294 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/subagent/manager.ts +2 -0
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/registry.ts +2 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +41 -2
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the assistant_tool pass-through path in the notification
|
|
3
|
+
* decision engine. When a producer hands us a verbatim message via
|
|
4
|
+
* contextPayload.requestedMessage, the engine must skip the LLM call
|
|
5
|
+
* entirely and use the producer-supplied copy as-is.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
// ── Mocks (must precede imports from mocked modules) ──────────────────
|
|
11
|
+
|
|
12
|
+
mock.module("../../channels/config.js", () => ({
|
|
13
|
+
getDeliverableChannels: () => ["vellum", "telegram"],
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
mock.module("../decisions-store.js", () => ({
|
|
17
|
+
createDecision: () => {},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module("../preference-summary.js", () => ({
|
|
21
|
+
getPreferenceSummary: () => undefined,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
mock.module("../conversation-candidates.js", () => ({
|
|
25
|
+
buildConversationCandidates: () => undefined,
|
|
26
|
+
serializeCandidatesForPrompt: () => undefined,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
mock.module("../../prompts/persona-resolver.js", () => ({
|
|
30
|
+
resolveGuardianPersona: () => null,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module("../../prompts/system-prompt.js", () => ({
|
|
34
|
+
buildCoreIdentityContext: () => null,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
mock.module("../../contacts/contact-store.js", () => ({
|
|
38
|
+
listGuardianChannels: () => null,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Provider mock — if `getConfiguredProvider` is ever called by the
|
|
42
|
+
// assistant_tool pass-through path, this throw makes the test fail
|
|
43
|
+
// loudly instead of silently exercising the LLM path.
|
|
44
|
+
mock.module("../../providers/provider-send-message.js", () => ({
|
|
45
|
+
getConfiguredProvider: async () => {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"getConfiguredProvider should NOT be invoked for assistant_tool pass-through",
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
createTimeout: () => ({
|
|
51
|
+
signal: new AbortController().signal,
|
|
52
|
+
cleanup: () => {},
|
|
53
|
+
}),
|
|
54
|
+
extractToolUse: () => null,
|
|
55
|
+
userMessage: (text: string) => ({ role: "user", content: text }),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
mock.module("../../util/logger.js", () => ({
|
|
59
|
+
getLogger: () =>
|
|
60
|
+
new Proxy({} as Record<string, unknown>, {
|
|
61
|
+
get: () => () => {},
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// ── Imports (after all mocks) ─────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
import { enforceRoutingIntent, evaluateSignal } from "../decision-engine.js";
|
|
68
|
+
import type { NotificationSignal } from "../signal.js";
|
|
69
|
+
import type { NotificationChannel } from "../types.js";
|
|
70
|
+
|
|
71
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function makeAssistantToolSignal(
|
|
74
|
+
overrides?: Partial<NotificationSignal>,
|
|
75
|
+
): NotificationSignal {
|
|
76
|
+
return {
|
|
77
|
+
signalId: "sig-assistant-tool-test-1",
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
sourceChannel: "assistant_tool",
|
|
80
|
+
sourceContextId: "tool-call-1",
|
|
81
|
+
sourceEventName: "user.send_notification",
|
|
82
|
+
contextPayload: {
|
|
83
|
+
requestedMessage: "exact verbatim text here",
|
|
84
|
+
requestedTitle: "Custom Title",
|
|
85
|
+
},
|
|
86
|
+
attentionHints: {
|
|
87
|
+
requiresAction: false,
|
|
88
|
+
urgency: "low",
|
|
89
|
+
isAsyncBackground: false,
|
|
90
|
+
visibleInSourceNow: false,
|
|
91
|
+
},
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Tests ─────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe("assistant_tool pass-through in notification decision engine", () => {
|
|
99
|
+
test("uses producer-supplied title and body verbatim, no LLM call", async () => {
|
|
100
|
+
const signal = makeAssistantToolSignal();
|
|
101
|
+
const decision = await evaluateSignal(signal, [
|
|
102
|
+
"vellum",
|
|
103
|
+
"telegram",
|
|
104
|
+
] as NotificationChannel[]);
|
|
105
|
+
|
|
106
|
+
expect(decision.shouldNotify).toBe(true);
|
|
107
|
+
expect(decision.selectedChannels).toContain("vellum");
|
|
108
|
+
expect(decision.renderedCopy.vellum?.body).toBe("exact verbatim text here");
|
|
109
|
+
expect(decision.renderedCopy.vellum?.title).toBe("Custom Title");
|
|
110
|
+
expect(decision.conversationActions?.vellum?.action).toBe("start_new");
|
|
111
|
+
expect(decision.reasoningSummary).toBe("assistant_tool pass-through");
|
|
112
|
+
expect(decision.fallbackUsed).toBe(false);
|
|
113
|
+
expect(decision.confidence).toBe(1.0);
|
|
114
|
+
expect(decision.dedupeKey).toBe(signal.signalId);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("derives title from body when requestedTitle is not supplied", async () => {
|
|
118
|
+
const signal = makeAssistantToolSignal({
|
|
119
|
+
contextPayload: {
|
|
120
|
+
requestedMessage: "First sentence. Second sentence follows here.",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const decision = await evaluateSignal(signal, [
|
|
124
|
+
"vellum",
|
|
125
|
+
] as NotificationChannel[]);
|
|
126
|
+
|
|
127
|
+
expect(decision.shouldNotify).toBe(true);
|
|
128
|
+
expect(decision.renderedCopy.vellum?.body).toBe(
|
|
129
|
+
"First sentence. Second sentence follows here.",
|
|
130
|
+
);
|
|
131
|
+
expect(decision.renderedCopy.vellum?.title).toBe("First sentence.");
|
|
132
|
+
expect(decision.reasoningSummary).toBe("assistant_tool pass-through");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("critical urgency selects all available channels", async () => {
|
|
136
|
+
const signal = makeAssistantToolSignal({
|
|
137
|
+
attentionHints: {
|
|
138
|
+
requiresAction: true,
|
|
139
|
+
urgency: "critical",
|
|
140
|
+
isAsyncBackground: false,
|
|
141
|
+
visibleInSourceNow: false,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const availableChannels = ["vellum", "telegram"] as NotificationChannel[];
|
|
145
|
+
const decision = await evaluateSignal(signal, availableChannels);
|
|
146
|
+
|
|
147
|
+
expect(decision.shouldNotify).toBe(true);
|
|
148
|
+
expect(decision.selectedChannels).toEqual(
|
|
149
|
+
expect.arrayContaining(availableChannels),
|
|
150
|
+
);
|
|
151
|
+
expect(decision.selectedChannels.length).toBe(availableChannels.length);
|
|
152
|
+
expect(decision.renderedCopy.vellum?.body).toBe("exact verbatim text here");
|
|
153
|
+
expect(decision.renderedCopy.telegram?.body).toBe(
|
|
154
|
+
"exact verbatim text here",
|
|
155
|
+
);
|
|
156
|
+
expect(decision.conversationActions?.vellum?.action).toBe("start_new");
|
|
157
|
+
expect(decision.conversationActions?.telegram?.action).toBe("start_new");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("threads contextPayload.deepLinkMetadata through to decision.deepLinkTarget", async () => {
|
|
161
|
+
const signal = makeAssistantToolSignal({
|
|
162
|
+
contextPayload: {
|
|
163
|
+
requestedMessage: "with deep link",
|
|
164
|
+
deepLinkMetadata: { route: "settings", anchor: "notifications" },
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const decision = await evaluateSignal(signal, [
|
|
168
|
+
"vellum",
|
|
169
|
+
] as NotificationChannel[]);
|
|
170
|
+
|
|
171
|
+
expect(decision.deepLinkTarget).toEqual({
|
|
172
|
+
route: "settings",
|
|
173
|
+
anchor: "notifications",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("omits deepLinkTarget when deepLinkMetadata is not a plain object", async () => {
|
|
178
|
+
const signal = makeAssistantToolSignal({
|
|
179
|
+
contextPayload: {
|
|
180
|
+
requestedMessage: "no deep link",
|
|
181
|
+
deepLinkMetadata: ["not", "a", "plain", "object"],
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const decision = await evaluateSignal(signal, [
|
|
185
|
+
"vellum",
|
|
186
|
+
] as NotificationChannel[]);
|
|
187
|
+
|
|
188
|
+
expect(decision.deepLinkTarget).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("preferredChannels adds to the default channel set (additive, not replacement)", async () => {
|
|
192
|
+
const signal = makeAssistantToolSignal({
|
|
193
|
+
contextPayload: {
|
|
194
|
+
requestedMessage: "also push to telegram",
|
|
195
|
+
preferredChannels: ["telegram"],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const decision = await evaluateSignal(signal, [
|
|
199
|
+
"vellum",
|
|
200
|
+
"telegram",
|
|
201
|
+
] as NotificationChannel[]);
|
|
202
|
+
|
|
203
|
+
// Vellum (canonical inbox) stays in selectedChannels; telegram is
|
|
204
|
+
// added on top. `--preferred-channels` is additive push, never a
|
|
205
|
+
// replacement for the inbox.
|
|
206
|
+
expect(decision.selectedChannels).toContain("vellum");
|
|
207
|
+
expect(decision.selectedChannels).toContain("telegram");
|
|
208
|
+
expect(decision.selectedChannels.length).toBe(2);
|
|
209
|
+
expect(decision.renderedCopy.vellum?.body).toBe("also push to telegram");
|
|
210
|
+
expect(decision.renderedCopy.telegram?.body).toBe("also push to telegram");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("urgent + preferredChannels keeps urgent's full broadcast intact", async () => {
|
|
214
|
+
const signal = makeAssistantToolSignal({
|
|
215
|
+
contextPayload: {
|
|
216
|
+
requestedMessage: "urgent broadcast",
|
|
217
|
+
requestedTitle: "Heads up",
|
|
218
|
+
preferredChannels: ["telegram"],
|
|
219
|
+
},
|
|
220
|
+
attentionHints: {
|
|
221
|
+
requiresAction: true,
|
|
222
|
+
urgency: "critical",
|
|
223
|
+
isAsyncBackground: false,
|
|
224
|
+
visibleInSourceNow: false,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const available = ["vellum", "telegram", "slack"] as NotificationChannel[];
|
|
228
|
+
const decision = await evaluateSignal(signal, available);
|
|
229
|
+
|
|
230
|
+
// Urgent broadcasts to every available channel; the additive union
|
|
231
|
+
// with preferredChannels is idempotent (telegram already included).
|
|
232
|
+
expect(decision.selectedChannels).toEqual(
|
|
233
|
+
expect.arrayContaining(available),
|
|
234
|
+
);
|
|
235
|
+
expect(decision.selectedChannels.length).toBe(available.length);
|
|
236
|
+
for (const ch of available) {
|
|
237
|
+
expect(decision.renderedCopy[ch]?.body).toBe("urgent broadcast");
|
|
238
|
+
expect(decision.renderedCopy[ch]?.title).toBe("Heads up");
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("routing-intent expansion to all_channels preserves verbatim copy on added channels", async () => {
|
|
243
|
+
const signal = makeAssistantToolSignal({
|
|
244
|
+
contextPayload: {
|
|
245
|
+
requestedMessage: "verbatim broadcast body",
|
|
246
|
+
requestedTitle: "verbatim broadcast title",
|
|
247
|
+
},
|
|
248
|
+
routingIntent: "all_channels",
|
|
249
|
+
});
|
|
250
|
+
const connected = ["vellum", "telegram"] as NotificationChannel[];
|
|
251
|
+
const decision = await evaluateSignal(signal, connected);
|
|
252
|
+
const enforced = enforceRoutingIntent(
|
|
253
|
+
decision,
|
|
254
|
+
"all_channels",
|
|
255
|
+
connected,
|
|
256
|
+
"assistant_tool",
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(enforced.selectedChannels).toEqual(
|
|
260
|
+
expect.arrayContaining(["vellum", "telegram"]),
|
|
261
|
+
);
|
|
262
|
+
for (const ch of enforced.selectedChannels) {
|
|
263
|
+
expect(enforced.renderedCopy[ch]?.body).toBe("verbatim broadcast body");
|
|
264
|
+
expect(enforced.renderedCopy[ch]?.title).toBe("verbatim broadcast title");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("preferredChannels falls back to default channel set when no overlap with availableChannels", async () => {
|
|
269
|
+
const signal = makeAssistantToolSignal({
|
|
270
|
+
contextPayload: {
|
|
271
|
+
requestedMessage: "fyi",
|
|
272
|
+
preferredChannels: ["disconnected_channel"],
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const decision = await evaluateSignal(signal, [
|
|
276
|
+
"vellum",
|
|
277
|
+
"telegram",
|
|
278
|
+
] as NotificationChannel[]);
|
|
279
|
+
|
|
280
|
+
expect(decision.selectedChannels).toEqual(["vellum"]);
|
|
281
|
+
expect(decision.renderedCopy.vellum?.body).toBe("fyi");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the deterministic pre-send checks.
|
|
3
|
+
*
|
|
4
|
+
* Focus: the rendered-copy quality check that suppresses notifications
|
|
5
|
+
* with empty bodies or bodies that leak the raw source event name
|
|
6
|
+
* (the `buildGenericCopy` fallback path).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
mock.module("../../util/logger.js", () => ({
|
|
12
|
+
getLogger: () =>
|
|
13
|
+
new Proxy({} as Record<string, unknown>, {
|
|
14
|
+
get: () => () => {},
|
|
15
|
+
}),
|
|
16
|
+
truncateForLog: (value: string) => value,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { getDb } from "../../memory/db-connection.js";
|
|
20
|
+
import { initializeDb } from "../../memory/db-init.js";
|
|
21
|
+
import { notificationEvents } from "../../memory/schema.js";
|
|
22
|
+
import {
|
|
23
|
+
type DeterministicCheckContext,
|
|
24
|
+
runDeterministicChecks,
|
|
25
|
+
} from "../deterministic-checks.js";
|
|
26
|
+
import type { NotificationSignal } from "../signal.js";
|
|
27
|
+
import type { NotificationDecision } from "../types.js";
|
|
28
|
+
|
|
29
|
+
initializeDb();
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
getDb().delete(notificationEvents).run();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function makeSignal(
|
|
36
|
+
overrides?: Partial<NotificationSignal>,
|
|
37
|
+
): NotificationSignal {
|
|
38
|
+
return {
|
|
39
|
+
signalId: `sig-${crypto.randomUUID()}`,
|
|
40
|
+
createdAt: Date.now(),
|
|
41
|
+
sourceChannel: "scheduler",
|
|
42
|
+
sourceContextId: "ctx-1",
|
|
43
|
+
sourceEventName: "schedule.notify",
|
|
44
|
+
contextPayload: {},
|
|
45
|
+
attentionHints: {
|
|
46
|
+
requiresAction: false,
|
|
47
|
+
urgency: "low",
|
|
48
|
+
isAsyncBackground: false,
|
|
49
|
+
visibleInSourceNow: false,
|
|
50
|
+
},
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeDecision(
|
|
56
|
+
overrides?: Partial<NotificationDecision>,
|
|
57
|
+
): NotificationDecision {
|
|
58
|
+
return {
|
|
59
|
+
shouldNotify: true,
|
|
60
|
+
selectedChannels: ["vellum"],
|
|
61
|
+
reasoningSummary: "test",
|
|
62
|
+
renderedCopy: {
|
|
63
|
+
vellum: { title: "Reminder", body: "Time to drink water" },
|
|
64
|
+
},
|
|
65
|
+
dedupeKey: `dk-${crypto.randomUUID()}`,
|
|
66
|
+
confidence: 0.9,
|
|
67
|
+
fallbackUsed: false,
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const context: DeterministicCheckContext = {
|
|
73
|
+
connectedChannels: ["vellum"],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
describe("checkRenderedCopyQuality (via runDeterministicChecks)", () => {
|
|
77
|
+
test("passes when body is real non-empty text", async () => {
|
|
78
|
+
const result = await runDeterministicChecks(
|
|
79
|
+
makeSignal(),
|
|
80
|
+
makeDecision(),
|
|
81
|
+
context,
|
|
82
|
+
);
|
|
83
|
+
expect(result.passed).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("fails when body is empty", async () => {
|
|
87
|
+
const decision = makeDecision({
|
|
88
|
+
renderedCopy: {
|
|
89
|
+
vellum: { title: "Reminder", body: "" },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const result = await runDeterministicChecks(
|
|
93
|
+
makeSignal(),
|
|
94
|
+
decision,
|
|
95
|
+
context,
|
|
96
|
+
);
|
|
97
|
+
expect(result.passed).toBe(false);
|
|
98
|
+
expect(result.reason).toContain("empty");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("fails when body is whitespace only", async () => {
|
|
102
|
+
const decision = makeDecision({
|
|
103
|
+
renderedCopy: {
|
|
104
|
+
vellum: { title: "Reminder", body: " \n " },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const result = await runDeterministicChecks(
|
|
108
|
+
makeSignal(),
|
|
109
|
+
decision,
|
|
110
|
+
context,
|
|
111
|
+
);
|
|
112
|
+
expect(result.passed).toBe(false);
|
|
113
|
+
expect(result.reason).toContain("empty");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("fails when body is the raw source event name", async () => {
|
|
117
|
+
const signal = makeSignal({ sourceEventName: "user.send_notification" });
|
|
118
|
+
const decision = makeDecision({
|
|
119
|
+
renderedCopy: {
|
|
120
|
+
vellum: { title: "Reminder", body: "user.send_notification" },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
124
|
+
expect(result.passed).toBe(false);
|
|
125
|
+
expect(result.reason).toContain("fallback leak");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("fails when body matches the normalized source event name", async () => {
|
|
129
|
+
const signal = makeSignal({ sourceEventName: "user.send_notification" });
|
|
130
|
+
const decision = makeDecision({
|
|
131
|
+
renderedCopy: {
|
|
132
|
+
vellum: { title: "Reminder", body: "user send notification" },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
136
|
+
expect(result.passed).toBe(false);
|
|
137
|
+
expect(result.reason).toContain("fallback leak");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("passes when channel was appended post-decision (urgency-forced vellum prepend)", async () => {
|
|
141
|
+
// Regression: emit-signal.ts prepends `vellum` to selectedChannels for
|
|
142
|
+
// high/critical urgency without populating renderedCopy.vellum. The
|
|
143
|
+
// broadcaster's composeFallbackCopy rescue handles those channels at
|
|
144
|
+
// delivery time, so the deterministic check must not fail-closed here.
|
|
145
|
+
const signal = makeSignal({
|
|
146
|
+
attentionHints: {
|
|
147
|
+
requiresAction: false,
|
|
148
|
+
urgency: "high",
|
|
149
|
+
isAsyncBackground: false,
|
|
150
|
+
visibleInSourceNow: false,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const decision = makeDecision({
|
|
154
|
+
selectedChannels: ["vellum", "telegram"],
|
|
155
|
+
renderedCopy: {
|
|
156
|
+
telegram: { title: "Reminder", body: "Time to drink water" },
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const result = await runDeterministicChecks(signal, decision, {
|
|
160
|
+
connectedChannels: ["vellum", "telegram"],
|
|
161
|
+
});
|
|
162
|
+
expect(result.passed).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("passes when enforceRoutingIntent expanded channels post-decision", async () => {
|
|
166
|
+
// Regression: enforceRoutingIntent can expand selectedChannels to
|
|
167
|
+
// all_channels / multi_channel without populating renderedCopy for the
|
|
168
|
+
// added channels. Broadcaster fallback covers them — check must allow.
|
|
169
|
+
const decision = makeDecision({
|
|
170
|
+
selectedChannels: ["vellum", "telegram", "slack"],
|
|
171
|
+
renderedCopy: {
|
|
172
|
+
vellum: { title: "Reminder", body: "Time to drink water" },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const result = await runDeterministicChecks(makeSignal(), decision, {
|
|
176
|
+
connectedChannels: ["vellum", "telegram", "slack"],
|
|
177
|
+
});
|
|
178
|
+
expect(result.passed).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("still validates body quality for channels with rendered copy", async () => {
|
|
182
|
+
// Even when some channels lack copy (broadcaster fallback territory),
|
|
183
|
+
// channels that DO have copy must still pass the empty/event-name checks.
|
|
184
|
+
const signal = makeSignal({ sourceEventName: "user.send_notification" });
|
|
185
|
+
const decision = makeDecision({
|
|
186
|
+
selectedChannels: ["vellum", "telegram"],
|
|
187
|
+
renderedCopy: {
|
|
188
|
+
telegram: { title: "Reminder", body: "user.send_notification" },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const result = await runDeterministicChecks(signal, decision, {
|
|
192
|
+
connectedChannels: ["vellum", "telegram"],
|
|
193
|
+
});
|
|
194
|
+
expect(result.passed).toBe(false);
|
|
195
|
+
expect(result.reason).toContain("fallback leak");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("fails when no selected channel has copy and fallback body is empty", async () => {
|
|
199
|
+
// Silent-no-delivery guard: if every selected channel is missing from
|
|
200
|
+
// renderedCopy AND the broadcaster's composeFallbackCopy can't produce
|
|
201
|
+
// a usable body (no template for sourceEventName → buildGenericCopy
|
|
202
|
+
// returns body=""), the gate must fail-closed rather than letting
|
|
203
|
+
// dispatchDecision report 0/N sent.
|
|
204
|
+
const signal = makeSignal({ sourceEventName: "user.send_notification" });
|
|
205
|
+
const decision = makeDecision({
|
|
206
|
+
selectedChannels: ["vellum"],
|
|
207
|
+
renderedCopy: {},
|
|
208
|
+
});
|
|
209
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
210
|
+
expect(result.passed).toBe(false);
|
|
211
|
+
expect(result.reason).toContain("fallback");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("passes when no selected channel has copy but fallback yields a usable body", async () => {
|
|
215
|
+
// schedule.notify has a copy-composer template that produces a usable
|
|
216
|
+
// body even with empty payload — the broadcaster's fallback path will
|
|
217
|
+
// deliver, so the deterministic gate must allow it through.
|
|
218
|
+
const signal = makeSignal({ sourceEventName: "schedule.notify" });
|
|
219
|
+
const decision = makeDecision({
|
|
220
|
+
selectedChannels: ["vellum"],
|
|
221
|
+
renderedCopy: {},
|
|
222
|
+
});
|
|
223
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
224
|
+
expect(result.passed).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("passes when shouldNotify is false regardless of copy contents", async () => {
|
|
228
|
+
const signal = makeSignal({ sourceEventName: "user.send_notification" });
|
|
229
|
+
const decision = makeDecision({
|
|
230
|
+
shouldNotify: false,
|
|
231
|
+
// Empty body + event-name body would both fail the copy check if
|
|
232
|
+
// shouldNotify were true. Short-circuit must skip the check.
|
|
233
|
+
renderedCopy: {
|
|
234
|
+
vellum: { title: "", body: "" },
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
238
|
+
expect(result.passed).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("passes assistant_tool pass-through even when body matches normalized event name", async () => {
|
|
242
|
+
// The pass-through path produces verbatim user-supplied body text.
|
|
243
|
+
// A coincidental match with the source event name is the user's
|
|
244
|
+
// intent, not a fallback leak — the check must not suppress it.
|
|
245
|
+
const signal = makeSignal({
|
|
246
|
+
sourceChannel: "assistant_tool",
|
|
247
|
+
sourceEventName: "assistant.share",
|
|
248
|
+
});
|
|
249
|
+
const decision = makeDecision({
|
|
250
|
+
reasoningSummary: "assistant_tool pass-through",
|
|
251
|
+
renderedCopy: {
|
|
252
|
+
vellum: { title: "Assistant share", body: "assistant share" },
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
256
|
+
expect(result.passed).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("fails assistant_tool pass-through with empty body (empty-body branch still fires)", async () => {
|
|
260
|
+
const signal = makeSignal({ sourceChannel: "assistant_tool" });
|
|
261
|
+
const decision = makeDecision({
|
|
262
|
+
reasoningSummary: "assistant_tool pass-through",
|
|
263
|
+
renderedCopy: {
|
|
264
|
+
vellum: { title: "Reminder", body: "" },
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
268
|
+
expect(result.passed).toBe(false);
|
|
269
|
+
expect(result.reason).toContain("empty");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("still fails non-pass-through decision when body matches event name", async () => {
|
|
273
|
+
// Regression guard: the pass-through short-circuit must not weaken
|
|
274
|
+
// the check for LLM/fallback paths.
|
|
275
|
+
const signal = makeSignal({ sourceEventName: "user.send_notification" });
|
|
276
|
+
const decision = makeDecision({
|
|
277
|
+
reasoningSummary: "llm classification",
|
|
278
|
+
renderedCopy: {
|
|
279
|
+
vellum: { title: "Reminder", body: "user.send_notification" },
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const result = await runDeterministicChecks(signal, decision, context);
|
|
283
|
+
expect(result.passed).toBe(false);
|
|
284
|
+
expect(result.reason).toContain("fallback leak");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -144,6 +144,7 @@ describe("emitNotificationSignal home-feed wire-up", () => {
|
|
|
144
144
|
sourceEventName: "schedule.notify",
|
|
145
145
|
sourceChannel: "scheduler",
|
|
146
146
|
sourceContextId: "conv-source-1",
|
|
147
|
+
contextPayload: { title: "Background job done" },
|
|
147
148
|
attentionHints: {
|
|
148
149
|
requiresAction: false,
|
|
149
150
|
urgency: "medium",
|
|
@@ -158,7 +159,10 @@ describe("emitNotificationSignal home-feed wire-up", () => {
|
|
|
158
159
|
expect(appended.id).toBe(`notif:${result.signalId}`);
|
|
159
160
|
expect(appended.title).toBe("Background job done");
|
|
160
161
|
expect(appended.summary).toBe("Summary of what happened.");
|
|
161
|
-
|
|
162
|
+
// The feed item's conversationId points to the source conversation
|
|
163
|
+
// that emitted the signal, not the conversation the notification
|
|
164
|
+
// pipeline spawned to handle it.
|
|
165
|
+
expect(appended.conversationId).toBe("conv-source-1");
|
|
162
166
|
});
|
|
163
167
|
|
|
164
168
|
test("interactive standard conversation does NOT trigger appendFeedItem", async () => {
|