@vellumai/assistant 0.11.4 → 0.11.5-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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__/config-loader-backfill.test.ts +19 -10
- 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__/managed-profile-guard.test.ts +4 -2
- 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/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- 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/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- 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/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- 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
|
@@ -2,7 +2,10 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import type { FeedItem } from "../../home/feed-types.js";
|
|
4
4
|
import type { NotificationSignal } from "../signal.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
NotificationDecision,
|
|
7
|
+
NotificationDeliveryResult,
|
|
8
|
+
} from "../types.js";
|
|
6
9
|
|
|
7
10
|
// ── Module mocks ───────────────────────────────────────────────────────
|
|
8
11
|
//
|
|
@@ -13,8 +16,40 @@ import type { NotificationDecision } from "../types.js";
|
|
|
13
16
|
|
|
14
17
|
const appendCalls: FeedItem[] = [];
|
|
15
18
|
const conversationLookups: string[] = [];
|
|
19
|
+
const messageAppends: Array<{
|
|
20
|
+
conversationId: string;
|
|
21
|
+
role: string;
|
|
22
|
+
content: string;
|
|
23
|
+
options?: { skipIndexing?: boolean };
|
|
24
|
+
}> = [];
|
|
25
|
+
const messageRewrites: Array<{ messageId: string; content: string }> = [];
|
|
26
|
+
/** messageId -> the conversation it belongs to, for the scoped lookup. */
|
|
27
|
+
const messageOwners = new Map<string, string>();
|
|
16
28
|
let conversationRow: { conversationType: string } | null = null;
|
|
17
29
|
let conversationLookupShouldThrow = false;
|
|
30
|
+
let messageAppendShouldThrow = false;
|
|
31
|
+
let messageRewriteShouldThrow = false;
|
|
32
|
+
let messageLookupShouldThrow = false;
|
|
33
|
+
|
|
34
|
+
let feedItemSchemaShouldReject = false;
|
|
35
|
+
const messagesInvalidated: string[] = [];
|
|
36
|
+
|
|
37
|
+
mock.module("../../runtime/sync/resource-sync-events.js", () => ({
|
|
38
|
+
publishConversationMessagesChanged: (conversationId: string) => {
|
|
39
|
+
messagesInvalidated.push(conversationId);
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../../home/feed-types.js", () => ({
|
|
44
|
+
feedItemSchema: {
|
|
45
|
+
parse: (item: unknown) => {
|
|
46
|
+
if (feedItemSchemaShouldReject) {
|
|
47
|
+
throw new Error("simulated schema rejection");
|
|
48
|
+
}
|
|
49
|
+
return item;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
18
53
|
|
|
19
54
|
mock.module("../../home/feed-writer.js", () => ({
|
|
20
55
|
appendFeedItem: async (item: FeedItem) => {
|
|
@@ -30,10 +65,38 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
30
65
|
}
|
|
31
66
|
return conversationRow;
|
|
32
67
|
},
|
|
68
|
+
addMessage: async (
|
|
69
|
+
conversationId: string,
|
|
70
|
+
role: string,
|
|
71
|
+
content: string,
|
|
72
|
+
options?: { skipIndexing?: boolean },
|
|
73
|
+
) => {
|
|
74
|
+
if (messageAppendShouldThrow) {
|
|
75
|
+
throw new Error("simulated message write failure");
|
|
76
|
+
}
|
|
77
|
+
messageAppends.push({ conversationId, role, content, options });
|
|
78
|
+
return { id: `msg-${messageAppends.length}` };
|
|
79
|
+
},
|
|
80
|
+
getMessageById: (messageId: string, conversationId?: string) => {
|
|
81
|
+
if (messageLookupShouldThrow) {
|
|
82
|
+
throw new Error("simulated message lookup failure");
|
|
83
|
+
}
|
|
84
|
+
const owner = messageOwners.get(messageId);
|
|
85
|
+
if (!owner || (conversationId && owner !== conversationId)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return { id: messageId, conversationId: owner };
|
|
89
|
+
},
|
|
90
|
+
updateMessageContent: (messageId: string, content: string) => {
|
|
91
|
+
if (messageRewriteShouldThrow) {
|
|
92
|
+
throw new Error("simulated message rewrite failure");
|
|
93
|
+
}
|
|
94
|
+
messageRewrites.push({ messageId, content });
|
|
95
|
+
},
|
|
33
96
|
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
34
97
|
}));
|
|
35
98
|
|
|
36
|
-
const { writeHomeFeedItemForSignal } =
|
|
99
|
+
const { updateFeedItemConversationMessage, writeHomeFeedItemForSignal } =
|
|
37
100
|
await import("../home-feed-side-effect.js");
|
|
38
101
|
|
|
39
102
|
// ── Test fixtures ──────────────────────────────────────────────────────
|
|
@@ -58,6 +121,19 @@ function makeSignal(
|
|
|
58
121
|
};
|
|
59
122
|
}
|
|
60
123
|
|
|
124
|
+
function makeVellumDelivery(
|
|
125
|
+
overrides: Partial<NotificationDeliveryResult> = {},
|
|
126
|
+
): NotificationDeliveryResult {
|
|
127
|
+
return {
|
|
128
|
+
channel: "vellum",
|
|
129
|
+
destination: "vellum",
|
|
130
|
+
status: "sent",
|
|
131
|
+
conversationId: "conv-source-1",
|
|
132
|
+
messageId: "msg-paired",
|
|
133
|
+
...overrides,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
61
137
|
function makeDecision(
|
|
62
138
|
overrides: Partial<NotificationDecision> = {},
|
|
63
139
|
): NotificationDecision {
|
|
@@ -76,8 +152,16 @@ function makeDecision(
|
|
|
76
152
|
beforeEach(() => {
|
|
77
153
|
appendCalls.length = 0;
|
|
78
154
|
conversationLookups.length = 0;
|
|
155
|
+
messageAppends.length = 0;
|
|
156
|
+
messageRewrites.length = 0;
|
|
157
|
+
messageOwners.clear();
|
|
79
158
|
conversationRow = null;
|
|
80
159
|
conversationLookupShouldThrow = false;
|
|
160
|
+
messageAppendShouldThrow = false;
|
|
161
|
+
messageRewriteShouldThrow = false;
|
|
162
|
+
messageLookupShouldThrow = false;
|
|
163
|
+
feedItemSchemaShouldReject = false;
|
|
164
|
+
messagesInvalidated.length = 0;
|
|
81
165
|
});
|
|
82
166
|
|
|
83
167
|
describe("writeHomeFeedItemForSignal", () => {
|
|
@@ -261,7 +345,7 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
261
345
|
const item = await writeHomeFeedItemForSignal(
|
|
262
346
|
signal,
|
|
263
347
|
decision,
|
|
264
|
-
"paired-delivery-conv-id",
|
|
348
|
+
makeVellumDelivery({ conversationId: "paired-delivery-conv-id" }),
|
|
265
349
|
);
|
|
266
350
|
|
|
267
351
|
expect(item).not.toBeNull();
|
|
@@ -287,7 +371,7 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
287
371
|
const item = await writeHomeFeedItemForSignal(
|
|
288
372
|
signal,
|
|
289
373
|
decision,
|
|
290
|
-
"paired-delivery-conv-id",
|
|
374
|
+
makeVellumDelivery({ conversationId: "paired-delivery-conv-id" }),
|
|
291
375
|
);
|
|
292
376
|
|
|
293
377
|
expect(item).not.toBeNull();
|
|
@@ -840,4 +924,452 @@ describe("writeHomeFeedItemForSignal", () => {
|
|
|
840
924
|
expect(item?.noteworthy).toBe(true);
|
|
841
925
|
expect(appendCalls[0]!.noteworthy).toBe(true);
|
|
842
926
|
});
|
|
927
|
+
|
|
928
|
+
describe("body append into the feed item's conversation", () => {
|
|
929
|
+
test("appends the card summary when no vellum conversation was paired", async () => {
|
|
930
|
+
// The Slack/Telegram-only routing case: nothing paired a vellum
|
|
931
|
+
// conversation, so this seam is the only writer that can put the body
|
|
932
|
+
// where the card's button points.
|
|
933
|
+
conversationRow = { conversationType: "background" };
|
|
934
|
+
const signal = makeSignal({
|
|
935
|
+
contextPayload: { title: "Nightly briefing" },
|
|
936
|
+
});
|
|
937
|
+
const decision = makeDecision({
|
|
938
|
+
selectedChannels: ["telegram"],
|
|
939
|
+
renderedCopy: {
|
|
940
|
+
telegram: { title: "Nightly briefing", body: "Three things today." },
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
945
|
+
|
|
946
|
+
expect(item?.conversationId).toBe("conv-source-1");
|
|
947
|
+
expect(messageAppends).toHaveLength(1);
|
|
948
|
+
expect(messageAppends[0]).toMatchObject({
|
|
949
|
+
conversationId: "conv-source-1",
|
|
950
|
+
role: "assistant",
|
|
951
|
+
content: "Three things today.",
|
|
952
|
+
options: { skipIndexing: true },
|
|
953
|
+
});
|
|
954
|
+
expect(item?.metadata?.notificationConversationMessageId).toBe("msg-1");
|
|
955
|
+
// A client with the conversation open refetches only on the messages
|
|
956
|
+
// tag; `addMessage` publishes the metadata tag alone.
|
|
957
|
+
expect(messagesInvalidated).toEqual(["conv-source-1"]);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test("writes exactly the summary the card renders", async () => {
|
|
961
|
+
// The button's promise is that the conversation holds what the card
|
|
962
|
+
// shows, so the two read from the same resolved text.
|
|
963
|
+
conversationRow = { conversationType: "background" };
|
|
964
|
+
const signal = makeSignal();
|
|
965
|
+
const decision = makeDecision({
|
|
966
|
+
selectedChannels: ["slack"],
|
|
967
|
+
renderedCopy: {
|
|
968
|
+
slack: {
|
|
969
|
+
title: "Digest",
|
|
970
|
+
body: "Short popup line.",
|
|
971
|
+
conversationSeedMessage:
|
|
972
|
+
"**Digest**\n\n- First item\n- Second item",
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
978
|
+
|
|
979
|
+
expect(messageAppends[0]!.content).toBe(item!.summary);
|
|
980
|
+
expect(messageAppends[0]!.content).toBe(
|
|
981
|
+
"**Digest**\n\n- First item\n- Second item",
|
|
982
|
+
);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("records the paired row when vellum already wrote the body", async () => {
|
|
986
|
+
// `pairDeliveryWithConversation` wrote the body, so nothing is written
|
|
987
|
+
// again here, but the card still records the row so an edit can reach it
|
|
988
|
+
// when the delivery walk does not.
|
|
989
|
+
conversationRow = { conversationType: "background" };
|
|
990
|
+
const signal = makeSignal();
|
|
991
|
+
const decision = makeDecision({
|
|
992
|
+
selectedChannels: ["vellum"],
|
|
993
|
+
renderedCopy: {
|
|
994
|
+
vellum: { title: "Nightly briefing", body: "Three things today." },
|
|
995
|
+
},
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const item = await writeHomeFeedItemForSignal(
|
|
999
|
+
signal,
|
|
1000
|
+
decision,
|
|
1001
|
+
makeVellumDelivery(),
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
expect(item?.conversationId).toBe("conv-source-1");
|
|
1005
|
+
expect(messageAppends).toHaveLength(0);
|
|
1006
|
+
expect(item?.metadata?.notificationConversationMessageId).toBe(
|
|
1007
|
+
"msg-paired",
|
|
1008
|
+
);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("skips the append when a sentinel source context leaves no target", async () => {
|
|
1012
|
+
// No resolvable conversation means the client hides the button, so
|
|
1013
|
+
// there is nothing to keep honest.
|
|
1014
|
+
conversationRow = null;
|
|
1015
|
+
const signal = makeSignal({
|
|
1016
|
+
sourceContextId: "job-1700000000",
|
|
1017
|
+
attentionHints: {
|
|
1018
|
+
requiresAction: false,
|
|
1019
|
+
urgency: "medium",
|
|
1020
|
+
isAsyncBackground: true,
|
|
1021
|
+
visibleInSourceNow: false,
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
const decision = makeDecision({
|
|
1025
|
+
selectedChannels: ["telegram"],
|
|
1026
|
+
renderedCopy: {
|
|
1027
|
+
telegram: { title: "Retries exhausted", body: "Gave up after 3." },
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
1032
|
+
|
|
1033
|
+
expect(item).not.toBeNull();
|
|
1034
|
+
expect(item?.conversationId).toBeUndefined();
|
|
1035
|
+
expect(messageAppends).toHaveLength(0);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test("writes no message when the card is rejected", async () => {
|
|
1039
|
+
// The body only belongs in a conversation because a card sends the user
|
|
1040
|
+
// there, so a rejected card must not leave one behind.
|
|
1041
|
+
conversationRow = { conversationType: "background" };
|
|
1042
|
+
feedItemSchemaShouldReject = true;
|
|
1043
|
+
const signal = makeSignal();
|
|
1044
|
+
const decision = makeDecision({
|
|
1045
|
+
selectedChannels: ["telegram"],
|
|
1046
|
+
renderedCopy: {
|
|
1047
|
+
telegram: { title: "Nightly briefing", body: "Three things today." },
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
1052
|
+
|
|
1053
|
+
expect(item).toBeNull();
|
|
1054
|
+
expect(appendCalls).toHaveLength(0);
|
|
1055
|
+
expect(messageAppends).toHaveLength(0);
|
|
1056
|
+
expect(messagesInvalidated).toHaveLength(0);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
test("a failed message write leaves the persisted feed item intact", async () => {
|
|
1060
|
+
conversationRow = { conversationType: "background" };
|
|
1061
|
+
messageAppendShouldThrow = true;
|
|
1062
|
+
const signal = makeSignal();
|
|
1063
|
+
const decision = makeDecision({
|
|
1064
|
+
selectedChannels: ["telegram"],
|
|
1065
|
+
renderedCopy: {
|
|
1066
|
+
telegram: { title: "Nightly briefing", body: "Three things today." },
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
1071
|
+
|
|
1072
|
+
expect(item).not.toBeNull();
|
|
1073
|
+
expect(appendCalls).toHaveLength(1);
|
|
1074
|
+
expect(messageAppends).toHaveLength(0);
|
|
1075
|
+
expect(item?.metadata?.notificationConversationMessageId).toBeUndefined();
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
test("records the paired row when the vellum delivery failed", async () => {
|
|
1079
|
+
// The row exists whatever the delivery reported, and a failed delivery
|
|
1080
|
+
// is exactly the case the edit path's delivery walk skips.
|
|
1081
|
+
conversationRow = { conversationType: "background" };
|
|
1082
|
+
const signal = makeSignal();
|
|
1083
|
+
const decision = makeDecision({
|
|
1084
|
+
selectedChannels: ["vellum"],
|
|
1085
|
+
renderedCopy: {
|
|
1086
|
+
vellum: { title: "Nightly briefing", body: "Three things today." },
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
const item = await writeHomeFeedItemForSignal(
|
|
1091
|
+
signal,
|
|
1092
|
+
decision,
|
|
1093
|
+
makeVellumDelivery({ status: "failed", messageId: "msg-paired" }),
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
expect(item?.metadata?.notificationConversationMessageId).toBe(
|
|
1097
|
+
"msg-paired",
|
|
1098
|
+
);
|
|
1099
|
+
// The row already exists, so nothing is written a second time.
|
|
1100
|
+
expect(messageAppends).toHaveLength(0);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test("records nothing when the paired row sits outside the card's conversation", async () => {
|
|
1104
|
+
// Guardian producers pair a fresh conversation rather than the one the
|
|
1105
|
+
// card opens, so its row is not the card's to rewrite.
|
|
1106
|
+
conversationRow = { conversationType: "background" };
|
|
1107
|
+
const signal = makeSignal();
|
|
1108
|
+
const decision = makeDecision({
|
|
1109
|
+
selectedChannels: ["vellum"],
|
|
1110
|
+
renderedCopy: {
|
|
1111
|
+
vellum: { title: "Approval needed", body: "Allow this?" },
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
const item = await writeHomeFeedItemForSignal(
|
|
1116
|
+
signal,
|
|
1117
|
+
decision,
|
|
1118
|
+
makeVellumDelivery({
|
|
1119
|
+
conversationId: "conv-guardian-request",
|
|
1120
|
+
messageId: "msg-guardian",
|
|
1121
|
+
}),
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
expect(item?.conversationId).toBe("conv-source-1");
|
|
1125
|
+
expect(item?.metadata?.notificationConversationMessageId).toBeUndefined();
|
|
1126
|
+
expect(messageAppends).toHaveLength(0);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test("records the paired row when it backs a sentinel card", async () => {
|
|
1130
|
+
// A sentinel `sourceContextId` puts the paired conversation behind the
|
|
1131
|
+
// button, so its row is the one the card carries.
|
|
1132
|
+
conversationRow = null;
|
|
1133
|
+
const signal = makeSignal({
|
|
1134
|
+
sourceContextId: "job-1700000000",
|
|
1135
|
+
attentionHints: {
|
|
1136
|
+
requiresAction: false,
|
|
1137
|
+
urgency: "medium",
|
|
1138
|
+
isAsyncBackground: true,
|
|
1139
|
+
visibleInSourceNow: false,
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
const decision = makeDecision({
|
|
1143
|
+
selectedChannels: ["vellum"],
|
|
1144
|
+
renderedCopy: {
|
|
1145
|
+
vellum: { title: "Retries exhausted", body: "Gave up after 3." },
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
const item = await writeHomeFeedItemForSignal(
|
|
1150
|
+
signal,
|
|
1151
|
+
decision,
|
|
1152
|
+
makeVellumDelivery({ conversationId: "conv-paired", messageId: "m-p" }),
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
expect(item?.conversationId).toBe("conv-paired");
|
|
1156
|
+
expect(item?.metadata?.notificationConversationMessageId).toBe("m-p");
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
test("strips a producer-supplied handle from the card metadata", async () => {
|
|
1160
|
+
// `contextPayload` reaches the card verbatim, and this key addresses a
|
|
1161
|
+
// row for rewriting, so a producer must not be able to set it.
|
|
1162
|
+
conversationRow = { conversationType: "background" };
|
|
1163
|
+
const signal = makeSignal({
|
|
1164
|
+
contextPayload: {
|
|
1165
|
+
title: "Nightly briefing",
|
|
1166
|
+
notificationConversationMessageId: "msg-somebody-elses",
|
|
1167
|
+
},
|
|
1168
|
+
});
|
|
1169
|
+
const decision = makeDecision({
|
|
1170
|
+
selectedChannels: ["vellum"],
|
|
1171
|
+
renderedCopy: {
|
|
1172
|
+
vellum: { title: "Nightly briefing", body: "Three things today." },
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
const item = await writeHomeFeedItemForSignal(
|
|
1177
|
+
signal,
|
|
1178
|
+
decision,
|
|
1179
|
+
makeVellumDelivery(),
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
expect(item?.metadata?.notificationConversationMessageId).toBe(
|
|
1183
|
+
"msg-paired",
|
|
1184
|
+
);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("drops a producer-supplied handle when the card records none", async () => {
|
|
1188
|
+
conversationRow = { conversationType: "background" };
|
|
1189
|
+
const signal = makeSignal({
|
|
1190
|
+
contextPayload: {
|
|
1191
|
+
title: "Approval needed",
|
|
1192
|
+
notificationConversationMessageId: "msg-somebody-elses",
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
const decision = makeDecision({
|
|
1196
|
+
selectedChannels: ["vellum"],
|
|
1197
|
+
renderedCopy: {
|
|
1198
|
+
vellum: { title: "Approval needed", body: "Allow this?" },
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
const item = await writeHomeFeedItemForSignal(
|
|
1203
|
+
signal,
|
|
1204
|
+
decision,
|
|
1205
|
+
makeVellumDelivery({ conversationId: "conv-guardian-request" }),
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
expect(item?.metadata?.notificationConversationMessageId).toBeUndefined();
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
test("a producer-supplied handle never displaces the real one", async () => {
|
|
1212
|
+
conversationRow = { conversationType: "background" };
|
|
1213
|
+
const signal = makeSignal({
|
|
1214
|
+
contextPayload: {
|
|
1215
|
+
title: "Nightly briefing",
|
|
1216
|
+
notificationConversationMessageId: "msg-somebody-elses",
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
const decision = makeDecision({
|
|
1220
|
+
selectedChannels: ["telegram"],
|
|
1221
|
+
renderedCopy: {
|
|
1222
|
+
telegram: { title: "Nightly briefing", body: "Three things today." },
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
1227
|
+
|
|
1228
|
+
expect(item?.metadata?.notificationConversationMessageId).toBe("msg-1");
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
test("preserves producer metadata alongside the message handle", async () => {
|
|
1232
|
+
conversationRow = { conversationType: "background" };
|
|
1233
|
+
const signal = makeSignal({
|
|
1234
|
+
contextPayload: { title: "Nightly briefing", scheduleId: "sched-7" },
|
|
1235
|
+
});
|
|
1236
|
+
const decision = makeDecision({
|
|
1237
|
+
selectedChannels: ["telegram"],
|
|
1238
|
+
renderedCopy: {
|
|
1239
|
+
telegram: { title: "Nightly briefing", body: "Three things today." },
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
const item = await writeHomeFeedItemForSignal(signal, decision);
|
|
1244
|
+
|
|
1245
|
+
expect(item?.metadata).toMatchObject({
|
|
1246
|
+
title: "Nightly briefing",
|
|
1247
|
+
scheduleId: "sched-7",
|
|
1248
|
+
notificationConversationMessageId: "msg-1",
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
describe("updateFeedItemConversationMessage", () => {
|
|
1254
|
+
function makeItem(metadata?: Record<string, unknown>): FeedItem {
|
|
1255
|
+
return {
|
|
1256
|
+
id: "notif:sig-test-1",
|
|
1257
|
+
type: "notification",
|
|
1258
|
+
priority: 50,
|
|
1259
|
+
title: "Nightly briefing",
|
|
1260
|
+
summary: "Three things today.",
|
|
1261
|
+
timestamp: "2026-08-19T00:00:00.000Z",
|
|
1262
|
+
createdAt: "2026-08-19T00:00:00.000Z",
|
|
1263
|
+
status: "new",
|
|
1264
|
+
category: "scheduling",
|
|
1265
|
+
noteworthy: false,
|
|
1266
|
+
fromAssistant: false,
|
|
1267
|
+
conversationId: "conv-source-1",
|
|
1268
|
+
...(metadata ? { metadata } : {}),
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
test("rewrites the message the card owns", () => {
|
|
1273
|
+
messageOwners.set("msg-9", "conv-source-1");
|
|
1274
|
+
|
|
1275
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1276
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1277
|
+
"Four things now.",
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
expect(rewritten).toBe(true);
|
|
1281
|
+
expect(messageRewrites).toEqual([
|
|
1282
|
+
{ messageId: "msg-9", content: "Four things now." },
|
|
1283
|
+
]);
|
|
1284
|
+
expect(messagesInvalidated).toEqual(["conv-source-1"]);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test("reports no rewrite when the card owns no message", () => {
|
|
1288
|
+
// The vellum-delivered case: the delivery row carries the handle and
|
|
1289
|
+
// the adapter rewrites it, so there is nothing to do here.
|
|
1290
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1291
|
+
makeItem({ scheduleId: "sched-7" }),
|
|
1292
|
+
"Four things now.",
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
expect(rewritten).toBe(false);
|
|
1296
|
+
expect(messageRewrites).toHaveLength(0);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test("refuses a handle addressing a row outside the card's conversation", () => {
|
|
1300
|
+
// Nothing should be able to point the rewrite at an unrelated message.
|
|
1301
|
+
messageOwners.set("msg-9", "conv-somebody-elses");
|
|
1302
|
+
|
|
1303
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1304
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1305
|
+
"Four things now.",
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
expect(rewritten).toBe(false);
|
|
1309
|
+
expect(messageRewrites).toHaveLength(0);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
test("refuses a handle whose row no longer exists", () => {
|
|
1313
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1314
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1315
|
+
"Four things now.",
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
expect(rewritten).toBe(false);
|
|
1319
|
+
expect(messageRewrites).toHaveLength(0);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
test("stands down when the delivery walk already rewrote the row", () => {
|
|
1323
|
+
messageOwners.set("msg-9", "conv-source-1");
|
|
1324
|
+
|
|
1325
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1326
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1327
|
+
"Four things now.",
|
|
1328
|
+
new Set(["msg-9"]),
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
expect(rewritten).toBe(false);
|
|
1332
|
+
expect(messageRewrites).toHaveLength(0);
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
test("rewrites when the walk touched some other row", () => {
|
|
1336
|
+
messageOwners.set("msg-9", "conv-source-1");
|
|
1337
|
+
|
|
1338
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1339
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1340
|
+
"Four things now.",
|
|
1341
|
+
new Set(["1700000000.0001"]),
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
expect(rewritten).toBe(true);
|
|
1345
|
+
expect(messageRewrites).toHaveLength(1);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test("reports no rewrite when the ownership lookup throws", () => {
|
|
1349
|
+
// The edit has already patched the feed and run the channel updates by
|
|
1350
|
+
// the time this runs, so a store that cannot answer must not abort it.
|
|
1351
|
+
messageOwners.set("msg-9", "conv-source-1");
|
|
1352
|
+
messageLookupShouldThrow = true;
|
|
1353
|
+
|
|
1354
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1355
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1356
|
+
"Four things now.",
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
expect(rewritten).toBe(false);
|
|
1360
|
+
expect(messageRewrites).toHaveLength(0);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
test("reports no rewrite when the store write throws", () => {
|
|
1364
|
+
messageOwners.set("msg-9", "conv-source-1");
|
|
1365
|
+
messageRewriteShouldThrow = true;
|
|
1366
|
+
|
|
1367
|
+
const rewritten = updateFeedItemConversationMessage(
|
|
1368
|
+
makeItem({ notificationConversationMessageId: "msg-9" }),
|
|
1369
|
+
"Four things now.",
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
expect(rewritten).toBe(false);
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
843
1375
|
});
|
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
|
|
21
21
|
import type { AssistantEvent } from "../../api/index.js";
|
|
22
22
|
import type { InterfaceId } from "../../channels/types.js";
|
|
23
|
+
import { updateMessageContent } from "../../persistence/conversation-crud.js";
|
|
24
|
+
import { publishConversationMessagesChanged } from "../../runtime/sync/resource-sync-events.js";
|
|
23
25
|
import { getLogger } from "../../util/logger.js";
|
|
24
26
|
import type {
|
|
25
27
|
ChannelAdapter,
|
|
26
28
|
ChannelDeliveryPayload,
|
|
27
29
|
ChannelDestination,
|
|
30
|
+
ChannelUpdateContext,
|
|
31
|
+
ChannelUpdatePayload,
|
|
28
32
|
DeliveryResult,
|
|
29
33
|
NotificationChannel,
|
|
30
34
|
} from "../types.js";
|
|
@@ -132,4 +136,55 @@ export class VellumAdapter implements ChannelAdapter {
|
|
|
132
136
|
return { success: false, error: message };
|
|
133
137
|
}
|
|
134
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Rewrite the conversation message this delivery persisted.
|
|
142
|
+
*
|
|
143
|
+
* A vellum delivery has no channel-native message to patch, but it does
|
|
144
|
+
* carry the id of the row written into the conversation the notification
|
|
145
|
+
* card links to. Editing a notification patches the feed item, so without
|
|
146
|
+
* this the card would show the new body while "Go to Conversation" opened
|
|
147
|
+
* the old one.
|
|
148
|
+
*
|
|
149
|
+
* That row holds the body, and the feed rewrites its summary only when the
|
|
150
|
+
* patch carries one, so a title-only patch leaves the row alone to keep the
|
|
151
|
+
* two in step. A delivery carrying no message id has no row to rewrite.
|
|
152
|
+
*/
|
|
153
|
+
async update(
|
|
154
|
+
delivery: ChannelUpdateContext,
|
|
155
|
+
patch: ChannelUpdatePayload,
|
|
156
|
+
): Promise<DeliveryResult> {
|
|
157
|
+
if (!delivery.messageId) {
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
error:
|
|
161
|
+
"missing_message_id: this delivery persisted no conversation message",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (patch.body === undefined) {
|
|
165
|
+
log.info(
|
|
166
|
+
{ deliveryId: delivery.deliveryId, messageId: delivery.messageId },
|
|
167
|
+
"Vellum notification edit carried no body, conversation message left as is",
|
|
168
|
+
);
|
|
169
|
+
return { success: true, messageId: delivery.messageId };
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
updateMessageContent(delivery.messageId, patch.body);
|
|
173
|
+
if (delivery.conversationId) {
|
|
174
|
+
publishConversationMessagesChanged(delivery.conversationId);
|
|
175
|
+
}
|
|
176
|
+
log.info(
|
|
177
|
+
{ deliveryId: delivery.deliveryId, messageId: delivery.messageId },
|
|
178
|
+
"Vellum notification conversation message updated",
|
|
179
|
+
);
|
|
180
|
+
return { success: true, messageId: delivery.messageId };
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
183
|
+
log.error(
|
|
184
|
+
{ err, deliveryId: delivery.deliveryId, messageId: delivery.messageId },
|
|
185
|
+
"Failed to update Vellum notification conversation message",
|
|
186
|
+
);
|
|
187
|
+
return { success: false, error: message };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
135
190
|
}
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
import type { Button, CardBlock, ContextBlock, KnownBlock } from "@slack/types";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
sendSlackReply,
|
|
19
|
+
updateSlackMessage,
|
|
20
|
+
} from "../../messaging/providers/slack/send.js";
|
|
18
21
|
import { APPROVAL_INSTRUCTION_BLOCK_ID_PREFIX } from "../../messaging/providers/slack/withdraw.js";
|
|
19
22
|
import type { ApprovalUIMetadata } from "../../runtime/channel-approval-types.js";
|
|
20
23
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -476,10 +479,12 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
476
479
|
return { success: false, error: "no body or title supplied for update" };
|
|
477
480
|
}
|
|
478
481
|
try {
|
|
479
|
-
const result = await
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
482
|
+
const result = await updateSlackMessage(
|
|
483
|
+
delivery.destination,
|
|
484
|
+
delivery.messageId,
|
|
485
|
+
text,
|
|
486
|
+
{ useBlocks: true },
|
|
487
|
+
);
|
|
483
488
|
log.info(
|
|
484
489
|
{ chatId: delivery.destination, messageTs: delivery.messageId },
|
|
485
490
|
"Slack notification updated",
|