@vellumai/assistant 0.11.4 → 0.11.5-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/feature-flag-registry.json +35 -19
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -28,7 +28,7 @@ mock.module("./api.js", () => ({
|
|
|
28
28
|
// the (unmocked) shared transport, so thrown test errors must be real
|
|
29
29
|
// instances or every branch under test would silently take the generic path.
|
|
30
30
|
const { SlackApiError } = await import("./web-api-transport.js");
|
|
31
|
-
const { sendSlackAssistantThreadStatus, sendSlackReply } =
|
|
31
|
+
const { sendSlackAssistantThreadStatus, sendSlackReply, updateSlackMessage } =
|
|
32
32
|
await import("./send.js");
|
|
33
33
|
|
|
34
34
|
describe("sendSlackAssistantThreadStatus", () => {
|
|
@@ -80,9 +80,8 @@ describe("sendSlackAssistantThreadStatus", () => {
|
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
describe("
|
|
83
|
+
describe("updateSlackMessage", () => {
|
|
84
84
|
const messageTs = "1700000000.000100";
|
|
85
|
-
const threadTs = "1700000000.000001";
|
|
86
85
|
const blocks: KnownBlock[] = [
|
|
87
86
|
{ type: "section", text: { type: "mrkdwn", text: "Final reply" } },
|
|
88
87
|
];
|
|
@@ -104,9 +103,7 @@ describe("sendSlackReply update path", () => {
|
|
|
104
103
|
})
|
|
105
104
|
.mockImplementationOnce(async () => ({ ok: true, ts: messageTs }));
|
|
106
105
|
|
|
107
|
-
const result = await
|
|
108
|
-
messageTs,
|
|
109
|
-
threadTs,
|
|
106
|
+
const result = await updateSlackMessage("C123", messageTs, "Final reply", {
|
|
110
107
|
blocks,
|
|
111
108
|
});
|
|
112
109
|
|
|
@@ -138,7 +135,7 @@ describe("sendSlackReply update path", () => {
|
|
|
138
135
|
});
|
|
139
136
|
|
|
140
137
|
await expect(
|
|
141
|
-
|
|
138
|
+
updateSlackMessage("C123", messageTs, "Final reply", { blocks }),
|
|
142
139
|
).rejects.toThrow();
|
|
143
140
|
|
|
144
141
|
// Two in-place chat.update attempts (with then without blocks), then give
|
|
@@ -155,7 +152,7 @@ describe("sendSlackReply update path", () => {
|
|
|
155
152
|
});
|
|
156
153
|
|
|
157
154
|
await expect(
|
|
158
|
-
|
|
155
|
+
updateSlackMessage("C123", messageTs, "Final reply", { blocks }),
|
|
159
156
|
).rejects.toThrow();
|
|
160
157
|
|
|
161
158
|
// A single failed chat.update, no chat.postMessage fallback: a transient
|
|
@@ -175,7 +172,7 @@ describe("sendSlackReply update path", () => {
|
|
|
175
172
|
});
|
|
176
173
|
|
|
177
174
|
await expect(
|
|
178
|
-
|
|
175
|
+
updateSlackMessage("C123", messageTs, "Final reply", { blocks }),
|
|
179
176
|
).rejects.toThrow();
|
|
180
177
|
|
|
181
178
|
expect(callSlackApiMock).toHaveBeenCalledTimes(1);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Button, KnownBlock } from "@slack/types";
|
|
10
10
|
import type {
|
|
11
11
|
ApprovalUIMetadata,
|
|
12
|
+
MessageAudience,
|
|
12
13
|
SlackStreamOp,
|
|
13
14
|
} from "@vellumai/gateway-client";
|
|
14
15
|
|
|
@@ -133,9 +134,8 @@ export interface SlackSendOptions {
|
|
|
133
134
|
blocks?: KnownBlock[];
|
|
134
135
|
approval?: ApprovalUIMetadata;
|
|
135
136
|
useBlocks?: boolean;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
messageTs?: string;
|
|
137
|
+
/** Absent means the whole room sees it. */
|
|
138
|
+
audience?: MessageAudience;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
export interface SlackSendResult {
|
|
@@ -210,15 +210,40 @@ function buildApprovalFallbackText(
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
*
|
|
213
|
+
* Post a Slack text message with optional Block Kit formatting.
|
|
214
214
|
*
|
|
215
|
-
*
|
|
216
|
-
* mirroring `editMessage()` in ./withdraw.ts: a failed edit throws and is
|
|
217
|
-
* never converted into a fresh `chat.postMessage`. Posting on failure would
|
|
218
|
-
* leave the original message beside a duplicate ("ghost") reply; re-delivery
|
|
219
|
-
* after a transient failure is the delivery layer's responsibility, not this
|
|
220
|
-
* function's.
|
|
215
|
+
* Always posts. Replacing an existing message is {@link updateSlackMessage}.
|
|
221
216
|
*/
|
|
217
|
+
/**
|
|
218
|
+
* Replace a Slack message in place via `chat.update`.
|
|
219
|
+
*
|
|
220
|
+
* A failed update throws rather than posting a fresh message, mirroring
|
|
221
|
+
* `editMessage()` in ./withdraw.ts: posting on failure leaves the original
|
|
222
|
+
* beside a duplicate reply. Re-delivery after a transient failure belongs to
|
|
223
|
+
* the delivery layer, not here.
|
|
224
|
+
*/
|
|
225
|
+
export async function updateSlackMessage(
|
|
226
|
+
chatId: string,
|
|
227
|
+
messageId: string,
|
|
228
|
+
text: string,
|
|
229
|
+
options?: { blocks?: readonly KnownBlock[]; useBlocks?: boolean },
|
|
230
|
+
): Promise<SlackSendResult> {
|
|
231
|
+
const blocks = resolveBlocks(
|
|
232
|
+
text,
|
|
233
|
+
options?.blocks as KnownBlock[] | undefined,
|
|
234
|
+
undefined,
|
|
235
|
+
options?.useBlocks,
|
|
236
|
+
);
|
|
237
|
+
const result = await sendWithBlockFallback(
|
|
238
|
+
"chat.update",
|
|
239
|
+
{ channel: chatId, text, ts: messageId },
|
|
240
|
+
blocks,
|
|
241
|
+
{ fallbackWithoutBlocks: true },
|
|
242
|
+
);
|
|
243
|
+
log.info({ chatId, messageId }, "Slack message updated");
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
222
247
|
export async function sendSlackReply(
|
|
223
248
|
chatId: string,
|
|
224
249
|
text: string,
|
|
@@ -231,18 +256,6 @@ export async function sendSlackReply(
|
|
|
231
256
|
options?.useBlocks,
|
|
232
257
|
);
|
|
233
258
|
|
|
234
|
-
const messageTs = options?.messageTs;
|
|
235
|
-
if (typeof messageTs === "string" && messageTs.length > 0) {
|
|
236
|
-
const result = await sendWithBlockFallback(
|
|
237
|
-
"chat.update",
|
|
238
|
-
{ channel: chatId, text, ts: messageTs },
|
|
239
|
-
blocks,
|
|
240
|
-
{ fallbackWithoutBlocks: true },
|
|
241
|
-
);
|
|
242
|
-
log.info({ chatId, messageTs }, "Slack message updated");
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
259
|
const postBase: Record<string, unknown> = { channel: chatId, text };
|
|
247
260
|
if (options?.threadTs) {
|
|
248
261
|
postBase.thread_ts = options.threadTs;
|
|
@@ -261,13 +274,12 @@ export async function sendSlackReply(
|
|
|
261
274
|
fallbackText: approvalFallbackText,
|
|
262
275
|
};
|
|
263
276
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
277
|
+
// No guard for a missing reader: the audience carries the id with it, so
|
|
278
|
+
// "restricted to one reader, but which one" cannot be expressed.
|
|
279
|
+
if (options?.audience) {
|
|
268
280
|
return sendWithBlockFallback(
|
|
269
281
|
"chat.postEphemeral",
|
|
270
|
-
{ ...postBase, user: options.
|
|
282
|
+
{ ...postBase, user: options.audience.userId },
|
|
271
283
|
blocks,
|
|
272
284
|
fallbackOptions,
|
|
273
285
|
);
|
|
@@ -372,7 +384,7 @@ export async function sendSlackAssistantThreadStatus(
|
|
|
372
384
|
channel: string,
|
|
373
385
|
threadTs: string,
|
|
374
386
|
status: string,
|
|
375
|
-
loadingMessages?: string[],
|
|
387
|
+
loadingMessages?: readonly string[],
|
|
376
388
|
): Promise<void> {
|
|
377
389
|
try {
|
|
378
390
|
const body: Record<string, unknown> = {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
sendSlackReaction,
|
|
9
9
|
sendSlackReply,
|
|
10
10
|
sendSlackStreamOp,
|
|
11
|
+
updateSlackMessage,
|
|
11
12
|
} from "./send.js";
|
|
12
13
|
|
|
13
14
|
const log = getLogger("slack-transport");
|
|
@@ -16,19 +17,16 @@ export const slackTransport: ChannelTransport = {
|
|
|
16
17
|
channel: "slack",
|
|
17
18
|
|
|
18
19
|
async deliver(ctx, payload) {
|
|
19
|
-
const { chatId, text, attachments
|
|
20
|
+
const { chatId, text, attachments } = payload;
|
|
20
21
|
const threadTs = ctx.params.threadTs;
|
|
21
22
|
|
|
22
23
|
let sentTs: string | undefined;
|
|
23
24
|
if (text) {
|
|
24
25
|
const result = await sendSlackReply(chatId, text, {
|
|
25
26
|
threadTs,
|
|
26
|
-
blocks,
|
|
27
27
|
approval: payload.approval,
|
|
28
28
|
useBlocks: payload.useBlocks,
|
|
29
|
-
|
|
30
|
-
user: payload.user,
|
|
31
|
-
messageTs: payload.messageTs,
|
|
29
|
+
audience: payload.audience,
|
|
32
30
|
});
|
|
33
31
|
sentTs = result.ts;
|
|
34
32
|
} else if (payload.approval) {
|
|
@@ -54,27 +52,29 @@ export const slackTransport: ChannelTransport = {
|
|
|
54
52
|
return { ok: true, ts: sentTs };
|
|
55
53
|
},
|
|
56
54
|
|
|
57
|
-
async
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
async edit(_ctx, target) {
|
|
56
|
+
const result = await updateSlackMessage(
|
|
57
|
+
target.chatId,
|
|
58
|
+
target.messageId,
|
|
59
|
+
target.text,
|
|
60
|
+
{ blocks: target.blocks, useBlocks: target.useBlocks },
|
|
61
|
+
);
|
|
62
|
+
return { ok: true, ts: result.ts };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async react(_ctx, target) {
|
|
62
66
|
await sendSlackReaction(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
target.chatId,
|
|
68
|
+
target.emoji,
|
|
69
|
+
target.messageId,
|
|
70
|
+
target.action,
|
|
67
71
|
);
|
|
68
72
|
return { ok: true };
|
|
69
73
|
},
|
|
70
74
|
|
|
71
|
-
async setThreadStatus(_ctx,
|
|
72
|
-
const status = payload.assistantThreadStatus;
|
|
73
|
-
if (!status) {
|
|
74
|
-
return { ok: true };
|
|
75
|
-
}
|
|
75
|
+
async setThreadStatus(_ctx, status) {
|
|
76
76
|
await sendSlackAssistantThreadStatus(
|
|
77
|
-
status.
|
|
77
|
+
status.chatId,
|
|
78
78
|
status.threadTs,
|
|
79
79
|
status.status,
|
|
80
80
|
status.loadingMessages,
|
|
@@ -82,11 +82,7 @@ export const slackTransport: ChannelTransport = {
|
|
|
82
82
|
return { ok: true };
|
|
83
83
|
},
|
|
84
84
|
|
|
85
|
-
async streamReply(_ctx,
|
|
86
|
-
|
|
87
|
-
if (!op) {
|
|
88
|
-
return { ok: true };
|
|
89
|
-
}
|
|
90
|
-
return sendSlackStreamOp(payload.chatId, op);
|
|
85
|
+
async streamReply(_ctx, chatId, op) {
|
|
86
|
+
return sendSlackStreamOp(chatId, op);
|
|
91
87
|
},
|
|
92
88
|
};
|
|
@@ -272,10 +272,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
test("typing indicators target the topic", async () => {
|
|
275
|
-
await telegramTransport.
|
|
276
|
-
topicCtx,
|
|
277
|
-
payload({ chatAction: "typing" }),
|
|
278
|
-
);
|
|
275
|
+
await telegramTransport.typing!(topicCtx, "123");
|
|
279
276
|
|
|
280
277
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendChatAction", {
|
|
281
278
|
chat_id: "123",
|
|
@@ -291,10 +288,7 @@ describe("telegramTransport topic targeting", () => {
|
|
|
291
288
|
};
|
|
292
289
|
|
|
293
290
|
await telegramTransport.deliver(bareCtx, payload({ useBlocks: false }));
|
|
294
|
-
await telegramTransport.
|
|
295
|
-
bareCtx,
|
|
296
|
-
payload({ chatAction: "typing" }),
|
|
297
|
-
);
|
|
291
|
+
await telegramTransport.typing!(bareCtx, "123");
|
|
298
292
|
|
|
299
293
|
expect(callTelegramBotApiMock).toHaveBeenCalledWith("sendMessage", {
|
|
300
294
|
chat_id: "123",
|
|
@@ -67,12 +67,9 @@ export const telegramTransport: ChannelTransport = {
|
|
|
67
67
|
return { ok: true };
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
-
async
|
|
71
|
-
await sendTelegramTypingIndicator(
|
|
72
|
-
log.debug(
|
|
73
|
-
{ chatId: payload.chatId },
|
|
74
|
-
"Telegram typing indicator delivered (direct)",
|
|
75
|
-
);
|
|
70
|
+
async typing(ctx, chatId) {
|
|
71
|
+
await sendTelegramTypingIndicator(chatId, threadOptions(ctx));
|
|
72
|
+
log.debug({ chatId }, "Telegram typing indicator delivered (direct)");
|
|
76
73
|
return { ok: true };
|
|
77
74
|
},
|
|
78
75
|
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
groupingMessageId,
|
|
5
|
+
type ProviderMessageMetadata,
|
|
6
|
+
} from "./provider-message-metadata.js";
|
|
7
|
+
import type { SlackMessageMetadata } from "./providers/slack/message-metadata.js";
|
|
8
|
+
import { readProviderMetadata } from "./read-provider-metadata.js";
|
|
9
|
+
|
|
10
|
+
const CHANNEL = "C0123CHANNEL";
|
|
11
|
+
const MESSAGE_TS = "1700000000.000100";
|
|
12
|
+
const TARGET_TS = "1699999999.000001";
|
|
13
|
+
|
|
14
|
+
function slackRow(
|
|
15
|
+
overrides: Partial<SlackMessageMetadata> = {},
|
|
16
|
+
): SlackMessageMetadata {
|
|
17
|
+
return {
|
|
18
|
+
source: "slack",
|
|
19
|
+
channelId: CHANNEL,
|
|
20
|
+
channelTs: MESSAGE_TS,
|
|
21
|
+
eventKind: "message",
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("readProviderMetadata", () => {
|
|
27
|
+
it("reads a row that only has the legacy Slack envelope", () => {
|
|
28
|
+
// Slack's own envelope is mapped on read, so no row is rewritten.
|
|
29
|
+
const metadata = JSON.stringify({
|
|
30
|
+
slackMeta: JSON.stringify(slackRow({ threadTs: TARGET_TS })),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(readProviderMetadata(metadata)).toEqual({
|
|
34
|
+
source: "slack",
|
|
35
|
+
conversationExternalId: CHANNEL,
|
|
36
|
+
messageId: MESSAGE_TS,
|
|
37
|
+
threadId: TARGET_TS,
|
|
38
|
+
eventKind: "message",
|
|
39
|
+
} satisfies ProviderMessageMetadata);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("carries the actor's provider id, not just a display name", () => {
|
|
43
|
+
// Trust is keyed on the actor everywhere else, and a channel describing
|
|
44
|
+
// who reacted needs an identity rather than a label.
|
|
45
|
+
const metadata = JSON.stringify({
|
|
46
|
+
slackMeta: JSON.stringify(
|
|
47
|
+
slackRow({ actorExternalUserId: "U0123", displayName: "Jason" }),
|
|
48
|
+
),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const meta = readProviderMetadata(metadata);
|
|
52
|
+
expect(meta?.actorExternalId).toBe("U0123");
|
|
53
|
+
expect(meta?.displayName).toBe("Jason");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("maps a reaction's target onto the neutral name", () => {
|
|
57
|
+
const metadata = JSON.stringify({
|
|
58
|
+
slackMeta: JSON.stringify(
|
|
59
|
+
slackRow({
|
|
60
|
+
eventKind: "reaction",
|
|
61
|
+
reaction: {
|
|
62
|
+
emoji: "thumbsup",
|
|
63
|
+
targetChannelTs: TARGET_TS,
|
|
64
|
+
op: "added",
|
|
65
|
+
actorDisplayName: "Jason",
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const meta = readProviderMetadata(metadata);
|
|
72
|
+
expect(meta?.eventKind).toBe("reaction");
|
|
73
|
+
// No channel gives a reaction an id of its own, so the row must not claim
|
|
74
|
+
// its target's: two rows sharing one id break resolution by message id.
|
|
75
|
+
expect(meta?.messageId).toBeUndefined();
|
|
76
|
+
expect(meta?.reaction).toEqual({
|
|
77
|
+
targetMessageId: TARGET_TS,
|
|
78
|
+
emoji: "thumbsup",
|
|
79
|
+
op: "added",
|
|
80
|
+
actorDisplayName: "Jason",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("leaves threadId absent when the row is not in a thread", () => {
|
|
85
|
+
// A thread id here asserts that a thread exists. Synthesizing one from
|
|
86
|
+
// the message's own id is what keyed conversations on threads that never
|
|
87
|
+
// did (LUM-3330, LUM-3340), so absence has to survive the mapping.
|
|
88
|
+
const metadata = JSON.stringify({
|
|
89
|
+
slackMeta: JSON.stringify(slackRow()),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const meta = readProviderMetadata(metadata);
|
|
93
|
+
expect(meta).not.toBeNull();
|
|
94
|
+
expect(meta?.threadId).toBeUndefined();
|
|
95
|
+
expect(meta?.messageId).toBe(MESSAGE_TS);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("reads a channel this repo has no code for", () => {
|
|
99
|
+
// The point of the neutral shape. A plugin channel cannot add an adapter
|
|
100
|
+
// here, so it describes its own rows directly and every channel-agnostic
|
|
101
|
+
// reader understands them with no Vellum code naming that channel.
|
|
102
|
+
const metadata = JSON.stringify({
|
|
103
|
+
providerMeta: JSON.stringify({
|
|
104
|
+
source: "plugin",
|
|
105
|
+
conversationExternalId: "room-42",
|
|
106
|
+
messageId: "evt-9",
|
|
107
|
+
threadId: "evt-1",
|
|
108
|
+
eventKind: "reaction",
|
|
109
|
+
reaction: {
|
|
110
|
+
targetMessageId: "evt-1",
|
|
111
|
+
emoji: "tada",
|
|
112
|
+
op: "added",
|
|
113
|
+
},
|
|
114
|
+
} satisfies ProviderMessageMetadata),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const meta = readProviderMetadata(metadata);
|
|
118
|
+
expect(meta?.source).toBe("plugin");
|
|
119
|
+
expect(meta?.reaction?.targetMessageId).toBe("evt-1");
|
|
120
|
+
// Grouping is what history assembly keys on, so a reaction from an
|
|
121
|
+
// unknown channel lands beside the message it was attached to.
|
|
122
|
+
expect(groupingMessageId(meta!)).toBe("evt-1");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("groups a message by its own id", () => {
|
|
126
|
+
const metadata = JSON.stringify({
|
|
127
|
+
slackMeta: JSON.stringify(slackRow()),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(groupingMessageId(readProviderMetadata(metadata)!)).toBe(MESSAGE_TS);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("keeps provider fields the neutral schema does not name", () => {
|
|
134
|
+
// The schema passes through, so a channel can carry its own fields on the
|
|
135
|
+
// same object instead of getting a second envelope.
|
|
136
|
+
const metadata = JSON.stringify({
|
|
137
|
+
providerMeta: JSON.stringify({
|
|
138
|
+
source: "slack",
|
|
139
|
+
conversationExternalId: CHANNEL,
|
|
140
|
+
messageId: MESSAGE_TS,
|
|
141
|
+
eventKind: "message",
|
|
142
|
+
slackFiles: [{ name: "diagram.png" }],
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(
|
|
147
|
+
(readProviderMetadata(metadata) as Record<string, unknown> | null)
|
|
148
|
+
?.slackFiles,
|
|
149
|
+
).toEqual([{ name: "diagram.png" }]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("reads as null when the row carries neither envelope", () => {
|
|
153
|
+
expect(readProviderMetadata(JSON.stringify({ other: 1 }))).toBeNull();
|
|
154
|
+
expect(readProviderMetadata("not json")).toBeNull();
|
|
155
|
+
expect(readProviderMetadata(null)).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { safeParseRecord } from "../util/json.js";
|
|
2
|
+
import {
|
|
3
|
+
type ProviderMessageMetadata,
|
|
4
|
+
readProviderMessageMetadata,
|
|
5
|
+
} from "./provider-message-metadata.js";
|
|
6
|
+
import {
|
|
7
|
+
readSlackMetadataFromMessageMetadata,
|
|
8
|
+
slackMetadataAsProviderMetadata,
|
|
9
|
+
} from "./providers/slack/message-metadata.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read a stored row's channel metadata in terms no single provider owns.
|
|
13
|
+
*
|
|
14
|
+
* Two ways a row can carry it, in this order.
|
|
15
|
+
*
|
|
16
|
+
* `providerMeta` is the neutral shape written directly. This is the path that
|
|
17
|
+
* matters most: a channel we have no code for, a plugin channel above all,
|
|
18
|
+
* can describe its own rows in it and every channel-agnostic reader
|
|
19
|
+
* understands them. History assembly, the conversation route and the
|
|
20
|
+
* transcript renderer then work for a channel nobody here has heard of.
|
|
21
|
+
*
|
|
22
|
+
* `slackMeta` is Slack's own envelope, mapped on read. It holds fields no
|
|
23
|
+
* other channel has an equivalent for, so Slack keeps writing it and loses
|
|
24
|
+
* nothing. A channel that writes `providerMeta` needs no adapter at all.
|
|
25
|
+
*
|
|
26
|
+
* Normalizing on read is what allows both. Writing stays where provider
|
|
27
|
+
* detail legitimately lives, and nothing is stored twice.
|
|
28
|
+
*
|
|
29
|
+
* A row with no recognized envelope, or one that fails validation, reads as
|
|
30
|
+
* null: callers already treat absent metadata as "not a channel row".
|
|
31
|
+
*/
|
|
32
|
+
export function readProviderMetadata(
|
|
33
|
+
metadata: string | null | undefined,
|
|
34
|
+
opts?: { allowFlatLegacy?: boolean },
|
|
35
|
+
): ProviderMessageMetadata | null {
|
|
36
|
+
if (!metadata) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Malformed neutral metadata falls through to the provider envelopes rather
|
|
41
|
+
// than failing the read outright.
|
|
42
|
+
const neutral = readProviderMessageMetadata(
|
|
43
|
+
safeParseRecord(metadata).providerMeta,
|
|
44
|
+
);
|
|
45
|
+
if (neutral) {
|
|
46
|
+
return neutral;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const slackMeta = readSlackMetadataFromMessageMetadata(metadata, opts);
|
|
50
|
+
return slackMeta ? slackMetadataAsProviderMetadata(slackMeta) : null;
|
|
51
|
+
}
|
|
@@ -94,6 +94,23 @@ mock.module("../../runtime/desktop-presence.js", () => ({
|
|
|
94
94
|
},
|
|
95
95
|
}));
|
|
96
96
|
|
|
97
|
+
// Defaults to unfocused, so every other case in this file exercises the
|
|
98
|
+
// unsuppressed path.
|
|
99
|
+
let webFocused = false;
|
|
100
|
+
let webPresenceShouldThrow = false;
|
|
101
|
+
const webPresenceArgs: unknown[][] = [];
|
|
102
|
+
const realWebPresence = await import("../../runtime/web-presence.js");
|
|
103
|
+
mock.module("../../runtime/web-presence.js", () => ({
|
|
104
|
+
...realWebPresence,
|
|
105
|
+
isWebConversationFocused: (...args: unknown[]) => {
|
|
106
|
+
webPresenceArgs.push(args);
|
|
107
|
+
if (webPresenceShouldThrow) {
|
|
108
|
+
throw new Error("simulated presence read failure");
|
|
109
|
+
}
|
|
110
|
+
return webFocused;
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
|
|
97
114
|
const realAttentionStore =
|
|
98
115
|
await import("../../persistence/conversation-attention-store.js");
|
|
99
116
|
mock.module("../../persistence/conversation-attention-store.js", () => ({
|
|
@@ -183,6 +200,22 @@ function makeMacOriginatedMessage(): MessageRow {
|
|
|
183
200
|
});
|
|
184
201
|
}
|
|
185
202
|
|
|
203
|
+
/**
|
|
204
|
+
* A turn opened from a plain browser tab, as opposed to the Electron desktop
|
|
205
|
+
* renderer sharing the same web bundle: `client.os` is `"web"` only when
|
|
206
|
+
* `detectClientOs()` fails to resolve an Electron host OS first.
|
|
207
|
+
*/
|
|
208
|
+
function makeWebOriginatedMessage(): MessageRow {
|
|
209
|
+
return makeMessage({
|
|
210
|
+
metadata: JSON.stringify({
|
|
211
|
+
userMessageChannel: "vellum",
|
|
212
|
+
userMessageInterface: "web",
|
|
213
|
+
client: { os: "web" },
|
|
214
|
+
clientOsFromRequest: true,
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
186
219
|
function makeAssistantRow(content: ContentBlock[]): MessageRow {
|
|
187
220
|
return makeMessage({
|
|
188
221
|
id: ASSISTANT_MESSAGE_ID,
|
|
@@ -250,9 +283,12 @@ beforeEach(() => {
|
|
|
250
283
|
messageLookups.length = 0;
|
|
251
284
|
attachmentLookups.length = 0;
|
|
252
285
|
desktopPresenceArgs.length = 0;
|
|
286
|
+
webPresenceArgs.length = 0;
|
|
253
287
|
assistantAttachments = [];
|
|
254
288
|
desktopAttended = false;
|
|
255
289
|
desktopPresenceShouldThrow = false;
|
|
290
|
+
webFocused = false;
|
|
291
|
+
webPresenceShouldThrow = false;
|
|
256
292
|
getConversationShouldThrow = false;
|
|
257
293
|
conversationRow = makeConversation();
|
|
258
294
|
assistantRow = makeAssistantRow([
|
|
@@ -433,6 +469,64 @@ describe("emitAssistantReplyNotification", () => {
|
|
|
433
469
|
});
|
|
434
470
|
});
|
|
435
471
|
|
|
472
|
+
// Same pre-gate as "desktop presence" above, scoped to a browser tab
|
|
473
|
+
// instead of the whole app.
|
|
474
|
+
describe("web presence", () => {
|
|
475
|
+
beforeEach(() => {
|
|
476
|
+
initiatingRow = makeWebOriginatedMessage();
|
|
477
|
+
webFocused = true;
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("marks the signal source-active while a web tab reports itself focused on this conversation", async () => {
|
|
481
|
+
await run();
|
|
482
|
+
|
|
483
|
+
expect(emitCalls).toHaveLength(1);
|
|
484
|
+
expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(true);
|
|
485
|
+
// Scoped to this conversation, unlike desktop attendance: a focused tab
|
|
486
|
+
// only speaks for the conversation it is actually looking at.
|
|
487
|
+
expect(webPresenceArgs).toEqual([[CONVERSATION_ID]]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("leaves the signal live when no web tab is focused on this conversation", async () => {
|
|
491
|
+
webFocused = false;
|
|
492
|
+
|
|
493
|
+
await run();
|
|
494
|
+
|
|
495
|
+
expect(emitCalls).toHaveLength(1);
|
|
496
|
+
expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("leaves the signal live when the web presence flag is off", async () => {
|
|
500
|
+
setOverridesForTesting({ "web-presence-suppression": false });
|
|
501
|
+
|
|
502
|
+
await run();
|
|
503
|
+
|
|
504
|
+
expect(emitCalls).toHaveLength(1);
|
|
505
|
+
expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(false);
|
|
506
|
+
expect(webPresenceArgs).toEqual([]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("leaves the signal live when the web presence read throws", async () => {
|
|
510
|
+
webPresenceShouldThrow = true;
|
|
511
|
+
|
|
512
|
+
await run();
|
|
513
|
+
|
|
514
|
+
expect(emitCalls).toHaveLength(1);
|
|
515
|
+
expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(false);
|
|
516
|
+
expect(warnCalls).toHaveLength(1);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("marks the signal source-active for a turn opened from the macOS app when a web tab is focused here", async () => {
|
|
520
|
+
initiatingRow = makeMacOriginatedMessage();
|
|
521
|
+
|
|
522
|
+
await run();
|
|
523
|
+
|
|
524
|
+
expect(emitCalls).toHaveLength(1);
|
|
525
|
+
expect(emitCalls[0].attentionHints.visibleInSourceNow).toBe(true);
|
|
526
|
+
expect(webPresenceArgs).toEqual([[CONVERSATION_ID]]);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
436
530
|
test("omits requestedTitle when the conversation has no title", async () => {
|
|
437
531
|
conversationRow = makeConversation({ title: " " });
|
|
438
532
|
|