@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
|
@@ -201,6 +201,22 @@ describe("client OS per-row evidence marker", () => {
|
|
|
201
201
|
expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(true);
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
// Mirrors the Mac-originated case above: a plain browser tab reports
|
|
205
|
+
// `client.os: "web"`, while the Electron desktop renderer resolves its own
|
|
206
|
+
// host OS first and persists `macos`/`windows` instead.
|
|
207
|
+
test("marks an OS this row's own transport reported (web)", async () => {
|
|
208
|
+
const ctx = createWebTurnContext("web");
|
|
209
|
+
await persistQueuedMessageBody(ctx, {
|
|
210
|
+
content: "hello",
|
|
211
|
+
requestId: "req-transport-os-web",
|
|
212
|
+
requestClientOs: "web",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(lastUserMetadata().client).toEqual({ os: "web" });
|
|
216
|
+
expect(lastUserMetadata().clientOsFromRequest).toBe(true);
|
|
217
|
+
expect(isMacOriginatedUserMessage(lastUserMetadata())).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
204
220
|
test("marks an OS this row's own request headers reported", async () => {
|
|
205
221
|
const ctx = createWebTurnContext(undefined);
|
|
206
222
|
await persistQueuedMessageBody(ctx, {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction must never bake invalid image bytes into the rebuilt history.
|
|
3
|
+
*
|
|
4
|
+
* Retained images are re-hydrated from the attachment store and re-optimized
|
|
5
|
+
* at compaction time. If that pipeline yields bytes that no longer parse as an
|
|
6
|
+
* image (e.g. a corrupt conversion-cache entry), embedding them would make the
|
|
7
|
+
* provider reject EVERY subsequent turn with a 400: the conversation wedges
|
|
8
|
+
* until the block is manually removed. `buildRetainedImageBlocks` drops such
|
|
9
|
+
* payloads instead.
|
|
10
|
+
*/
|
|
11
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
buildRetainedImageBlocks,
|
|
15
|
+
collectImageManifest,
|
|
16
|
+
} from "../context/compactor.js";
|
|
17
|
+
import { attachInlineAttachmentToMessage } from "../persistence/attachments-store.js";
|
|
18
|
+
import {
|
|
19
|
+
addMessage,
|
|
20
|
+
createConversation,
|
|
21
|
+
} from "../persistence/conversation-crud.js";
|
|
22
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
23
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
24
|
+
|
|
25
|
+
await initializeDb();
|
|
26
|
+
|
|
27
|
+
// 1x1 transparent PNG.
|
|
28
|
+
const PNG_1X1_BASE64 =
|
|
29
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
|
|
30
|
+
|
|
31
|
+
// Declared image/png, but the bytes are not any known image format:
|
|
32
|
+
// the stand-in for a corrupted payload surviving in the pipeline.
|
|
33
|
+
const GARBAGE_BASE64 = Buffer.from("definitely not an image").toString(
|
|
34
|
+
"base64",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function resetTables(): void {
|
|
38
|
+
const db = getDb();
|
|
39
|
+
db.run("DELETE FROM message_attachments");
|
|
40
|
+
db.run("DELETE FROM attachments");
|
|
41
|
+
db.run("DELETE FROM messages");
|
|
42
|
+
db.run("DELETE FROM conversations");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function addImageMessage(
|
|
46
|
+
conversationId: string,
|
|
47
|
+
filename: string,
|
|
48
|
+
dataBase64: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const inserted = await addMessage(
|
|
51
|
+
conversationId,
|
|
52
|
+
"user",
|
|
53
|
+
JSON.stringify([{ type: "text", text: filename }]),
|
|
54
|
+
{
|
|
55
|
+
metadata: { provenanceTrustClass: "guardian" },
|
|
56
|
+
skipIndexing: true,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
await attachInlineAttachmentToMessage(
|
|
60
|
+
inserted.id,
|
|
61
|
+
0,
|
|
62
|
+
filename,
|
|
63
|
+
"image/png",
|
|
64
|
+
dataBase64,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("buildRetainedImageBlocks corrupt-bytes gate", () => {
|
|
69
|
+
beforeEach(resetTables);
|
|
70
|
+
|
|
71
|
+
test("valid image bytes are retained", async () => {
|
|
72
|
+
const conv = createConversation();
|
|
73
|
+
await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
|
|
74
|
+
const manifest = collectImageManifest(conv.id, "guardian");
|
|
75
|
+
|
|
76
|
+
const { blocks, resolved, missing } = await buildRetainedImageBlocks(
|
|
77
|
+
["good.png"],
|
|
78
|
+
manifest,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(resolved).toEqual(["good.png"]);
|
|
82
|
+
expect(missing).toEqual([]);
|
|
83
|
+
expect(blocks).toHaveLength(1);
|
|
84
|
+
expect(blocks[0].source.type).toBe("base64");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("bytes that do not parse as an image are dropped, not embedded", async () => {
|
|
88
|
+
const conv = createConversation();
|
|
89
|
+
await addImageMessage(conv.id, "corrupt.png", GARBAGE_BASE64);
|
|
90
|
+
await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
|
|
91
|
+
const manifest = collectImageManifest(conv.id, "guardian");
|
|
92
|
+
|
|
93
|
+
const { blocks, resolved, missing } = await buildRetainedImageBlocks(
|
|
94
|
+
["corrupt.png", "good.png"],
|
|
95
|
+
manifest,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// The corrupt payload is dropped (not listed as manifest-missing, since
|
|
99
|
+
// it resolved fine; its bytes were the problem) and the valid one
|
|
100
|
+
// survives.
|
|
101
|
+
expect(resolved).toEqual(["good.png"]);
|
|
102
|
+
expect(missing).toEqual([]);
|
|
103
|
+
expect(blocks).toHaveLength(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a truncated JPEG (valid SOI header, no EOI) is dropped", async () => {
|
|
107
|
+
// A JPEG torn mid-write keeps its SOI header, so a format sniff alone
|
|
108
|
+
// accepts it; the gate must also require an EOI marker.
|
|
109
|
+
const tornJpeg = Buffer.concat([
|
|
110
|
+
Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]),
|
|
111
|
+
Buffer.from("JFIF\0"),
|
|
112
|
+
Buffer.alloc(64),
|
|
113
|
+
]).toString("base64");
|
|
114
|
+
const conv = createConversation();
|
|
115
|
+
await addImageMessage(conv.id, "torn.jpg", tornJpeg);
|
|
116
|
+
const manifest = collectImageManifest(conv.id, "guardian");
|
|
117
|
+
|
|
118
|
+
const { blocks, resolved, missing } = await buildRetainedImageBlocks(
|
|
119
|
+
["torn.jpg"],
|
|
120
|
+
manifest,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(resolved).toEqual([]);
|
|
124
|
+
expect(missing).toEqual([]);
|
|
125
|
+
expect(blocks).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("unresolvable filenames still land in missing", async () => {
|
|
129
|
+
const conv = createConversation();
|
|
130
|
+
await addImageMessage(conv.id, "good.png", PNG_1X1_BASE64);
|
|
131
|
+
const manifest = collectImageManifest(conv.id, "guardian");
|
|
132
|
+
|
|
133
|
+
const { blocks, resolved, missing } = await buildRetainedImageBlocks(
|
|
134
|
+
["never-uploaded.png", "good.png"],
|
|
135
|
+
manifest,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(missing).toEqual(["never-uploaded.png"]);
|
|
139
|
+
expect(resolved).toEqual(["good.png"]);
|
|
140
|
+
expect(blocks).toHaveLength(1);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
__resetContainerCpuSamplerForTests,
|
|
5
|
+
__runContainerCpuSamplerTickForTests,
|
|
6
|
+
computeCpuPercent,
|
|
7
|
+
computeDurationWeightedMeanPercent,
|
|
8
|
+
getAverageContainerCpuPercentOrNull,
|
|
9
|
+
getCachedContainerCpuPercent,
|
|
10
|
+
getCachedContainerCpuPercentOrNull,
|
|
11
|
+
} from "../util/container-cpu-sampler.js";
|
|
12
|
+
|
|
13
|
+
describe("computeCpuPercent", () => {
|
|
14
|
+
test("computes percent of the full allocation from a CPU-time delta", () => {
|
|
15
|
+
// 2.5s of CPU over a 5s window on 1 core is 50% of the allocation.
|
|
16
|
+
expect(computeCpuPercent(2_500_000, 5_000, 1)).toBe(50);
|
|
17
|
+
// The same delta on 2 cores is half the utilization.
|
|
18
|
+
expect(computeCpuPercent(2_500_000, 5_000, 2)).toBe(25);
|
|
19
|
+
// Fully saturating 4 cores for the whole window is 100%.
|
|
20
|
+
expect(computeCpuPercent(20_000_000, 5_000, 4)).toBe(100);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rounds to 2 decimal places", () => {
|
|
24
|
+
expect(computeCpuPercent(123_456, 5_000, 2)).toBe(1.23);
|
|
25
|
+
expect(computeCpuPercent(123_999, 5_000, 2)).toBe(1.24);
|
|
26
|
+
expect(computeCpuPercent(0, 5_000, 1)).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("yields a non-finite result for zero cores, which callers must guard", () => {
|
|
30
|
+
expect(Number.isFinite(computeCpuPercent(1_000_000, 5_000, 0))).toBe(false);
|
|
31
|
+
expect(Number.isFinite(computeCpuPercent(0, 5_000, 0))).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("getCachedContainerCpuPercent", () => {
|
|
36
|
+
test("returns a finite number", () => {
|
|
37
|
+
expect(Number.isFinite(getCachedContainerCpuPercent())).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("getCachedContainerCpuPercentOrNull", () => {
|
|
42
|
+
test("returns null until the sampler computes its first delta", () => {
|
|
43
|
+
__resetContainerCpuSamplerForTests();
|
|
44
|
+
|
|
45
|
+
expect(getCachedContainerCpuPercentOrNull()).toBeNull();
|
|
46
|
+
// The plain accessor keeps its 0 placeholder for /v1/health.
|
|
47
|
+
expect(getCachedContainerCpuPercent()).toBe(0);
|
|
48
|
+
|
|
49
|
+
// Drive one sampler tick a full interval ahead of the reset baseline so
|
|
50
|
+
// it computes a real delta from process.cpuUsage().
|
|
51
|
+
__runContainerCpuSamplerTickForTests(Date.now() + 5_000);
|
|
52
|
+
|
|
53
|
+
const sampled = getCachedContainerCpuPercentOrNull();
|
|
54
|
+
expect(sampled).not.toBeNull();
|
|
55
|
+
expect(Number.isFinite(sampled!)).toBe(true);
|
|
56
|
+
expect(sampled).toBe(getCachedContainerCpuPercent());
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("computeDurationWeightedMeanPercent", () => {
|
|
61
|
+
test("weights each percent by the wall time its delta spanned", () => {
|
|
62
|
+
// 25s at 80% then 5s at 100% is 83.33% of the half minute, not the 90%
|
|
63
|
+
// an equal-weight mean would report.
|
|
64
|
+
expect(
|
|
65
|
+
computeDurationWeightedMeanPercent(
|
|
66
|
+
[
|
|
67
|
+
{ at: 25_000, percent: 80, elapsedMs: 25_000 },
|
|
68
|
+
{ at: 30_000, percent: 100, elapsedMs: 5_000 },
|
|
69
|
+
],
|
|
70
|
+
0,
|
|
71
|
+
),
|
|
72
|
+
).toBe(83.33);
|
|
73
|
+
// Equal durations degrade to the plain mean.
|
|
74
|
+
expect(
|
|
75
|
+
computeDurationWeightedMeanPercent(
|
|
76
|
+
[
|
|
77
|
+
{ at: 5_000, percent: 40, elapsedMs: 5_000 },
|
|
78
|
+
{ at: 10_000, percent: 60, elapsedMs: 5_000 },
|
|
79
|
+
],
|
|
80
|
+
0,
|
|
81
|
+
),
|
|
82
|
+
).toBe(50);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("clips a delayed tick's contribution to its overlap with the window", () => {
|
|
86
|
+
// A 25s CPU-bound stall at 100% ends 1s into the window; only that 1s
|
|
87
|
+
// may count. With 29s of idle at 10% after it, the window reads 13%,
|
|
88
|
+
// not the 52.8% an unclipped duration weighting would report.
|
|
89
|
+
expect(
|
|
90
|
+
computeDurationWeightedMeanPercent(
|
|
91
|
+
[
|
|
92
|
+
{ at: 1_000, percent: 100, elapsedMs: 25_000 },
|
|
93
|
+
{ at: 30_000, percent: 10, elapsedMs: 29_000 },
|
|
94
|
+
],
|
|
95
|
+
0,
|
|
96
|
+
),
|
|
97
|
+
).toBe(13);
|
|
98
|
+
// A tick entirely before the window contributes nothing.
|
|
99
|
+
expect(
|
|
100
|
+
computeDurationWeightedMeanPercent(
|
|
101
|
+
[
|
|
102
|
+
{ at: -1_000, percent: 100, elapsedMs: 5_000 },
|
|
103
|
+
{ at: 10_000, percent: 20, elapsedMs: 10_000 },
|
|
104
|
+
],
|
|
105
|
+
0,
|
|
106
|
+
),
|
|
107
|
+
).toBe(20);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns null without any overlapping samples", () => {
|
|
111
|
+
expect(computeDurationWeightedMeanPercent([], 0)).toBeNull();
|
|
112
|
+
expect(
|
|
113
|
+
computeDurationWeightedMeanPercent(
|
|
114
|
+
[{ at: 5_000, percent: 50, elapsedMs: 0 }],
|
|
115
|
+
0,
|
|
116
|
+
),
|
|
117
|
+
).toBeNull();
|
|
118
|
+
expect(
|
|
119
|
+
computeDurationWeightedMeanPercent(
|
|
120
|
+
[{ at: -1_000, percent: 50, elapsedMs: 5_000 }],
|
|
121
|
+
0,
|
|
122
|
+
),
|
|
123
|
+
).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("getAverageContainerCpuPercentOrNull", () => {
|
|
128
|
+
test("returns null until a tick lands inside the window", () => {
|
|
129
|
+
__resetContainerCpuSamplerForTests();
|
|
130
|
+
|
|
131
|
+
expect(getAverageContainerCpuPercentOrNull(60_000)).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("averages the ticks recorded within the trailing window", () => {
|
|
135
|
+
__resetContainerCpuSamplerForTests();
|
|
136
|
+
|
|
137
|
+
// A single recorded tick makes the average equal the last cached value.
|
|
138
|
+
__runContainerCpuSamplerTickForTests(Date.now() + 5_000);
|
|
139
|
+
const single = getAverageContainerCpuPercentOrNull(60_000);
|
|
140
|
+
expect(single).not.toBeNull();
|
|
141
|
+
expect(single).toBe(getCachedContainerCpuPercentOrNull());
|
|
142
|
+
|
|
143
|
+
// More ticks keep the average finite and within the recorded range; the
|
|
144
|
+
// exact value depends on real process CPU deltas, so only the invariant
|
|
145
|
+
// is asserted.
|
|
146
|
+
__runContainerCpuSamplerTickForTests(Date.now() + 10_000);
|
|
147
|
+
__runContainerCpuSamplerTickForTests(Date.now() + 15_000);
|
|
148
|
+
const averaged = getAverageContainerCpuPercentOrNull(60_000);
|
|
149
|
+
expect(averaged).not.toBeNull();
|
|
150
|
+
expect(Number.isFinite(averaged!)).toBe(true);
|
|
151
|
+
expect(averaged!).toBeGreaterThanOrEqual(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -71,7 +71,6 @@ mock.module("../plugins/defaults/memory/substrate/edge-index.js", () => ({
|
|
|
71
71
|
getEdgeIndex: async (): Promise<EdgeIndex> => edgeIndex,
|
|
72
72
|
invalidateEdgeIndex: () => {},
|
|
73
73
|
getReachable: () => new Set<string>(),
|
|
74
|
-
validateEdgeTargets: () => ({ ok: true, missing: [] }),
|
|
75
74
|
totalEdgeCount: () => 0,
|
|
76
75
|
}));
|
|
77
76
|
|
|
@@ -15,7 +15,6 @@ mock.module("../permissions/checker.js", () => ({
|
|
|
15
15
|
isDynamicSkillLoadInvocation: () => false,
|
|
16
16
|
check: checkSpy,
|
|
17
17
|
classifyRisk: () => Promise.resolve({ level: "low" }),
|
|
18
|
-
generateAllowlistOptions: () => Promise.resolve([]),
|
|
19
18
|
generateScopeOptions: () => [],
|
|
20
19
|
}));
|
|
21
20
|
|
|
@@ -521,6 +521,23 @@ describe("classifyConversationError", () => {
|
|
|
521
521
|
expect(result.userMessage.toLowerCase()).toContain("image");
|
|
522
522
|
});
|
|
523
523
|
|
|
524
|
+
it("classifies an OpenAI-compatible 400 for an unreadable image as image_unsupported_format (non-retryable)", () => {
|
|
525
|
+
// The rejection an OpenAI-compatible endpoint returns when any image in
|
|
526
|
+
// the request is not one of the formats it decodes (a HEIC photo renamed
|
|
527
|
+
// to .png, say). It names no index, so the whole turn dies unless it is
|
|
528
|
+
// classified for the image-recovery path with actionable copy.
|
|
529
|
+
const err = new ProviderError(
|
|
530
|
+
"The image data you provided does not represent a valid image. Please check your input and try again with one of the supported image formats: [image/jpeg, image/png, image/gif, image/webp]",
|
|
531
|
+
"openai",
|
|
532
|
+
400,
|
|
533
|
+
);
|
|
534
|
+
const result = classifyConversationError(err, baseCtx);
|
|
535
|
+
expect(result.code).toBe("IMAGE_TOO_LARGE");
|
|
536
|
+
expect(result.errorCategory).toBe("image_unsupported_format");
|
|
537
|
+
expect(result.retryable).toBe(false);
|
|
538
|
+
expect(result.userMessage.toLowerCase()).toContain("image");
|
|
539
|
+
});
|
|
540
|
+
|
|
524
541
|
it("does not steal generic 400s that happen to mention 'image'", () => {
|
|
525
542
|
const err = new ProviderError(
|
|
526
543
|
"invalid request: image source is missing",
|
|
@@ -652,14 +652,12 @@ describe("loadFromDb metadata injection rehydration", () => {
|
|
|
652
652
|
expect(messages[2].content).toEqual([{ type: "text", text: "Tail" }]);
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
-
test("internal-channel trusted_contact view
|
|
656
|
-
// Rehydration keys on
|
|
657
|
-
// `
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
// injection so a daemon-restart reload of the same conversation produces
|
|
662
|
-
// an identical prefix.
|
|
655
|
+
test("internal-channel trusted_contact view does not rehydrate memoryV2StaticBlock", async () => {
|
|
656
|
+
// Rehydration keys on the actor, not the surface: a trusted_contact is
|
|
657
|
+
// denied personal memory on the internal `"vellum"` channel exactly as it
|
|
658
|
+
// is on a remote one. Injection and rehydration share
|
|
659
|
+
// `canSeePersonalMemory`, so a daemon-restart reload of the same
|
|
660
|
+
// conversation produces an identical prefix.
|
|
663
661
|
mockConversation = defaultConv();
|
|
664
662
|
mockDbMessages = [
|
|
665
663
|
{
|
|
@@ -697,13 +695,8 @@ describe("loadFromDb metadata injection rehydration", () => {
|
|
|
697
695
|
const messages = conversation.getMessages();
|
|
698
696
|
|
|
699
697
|
expect(messages).toHaveLength(3);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
type: "text",
|
|
703
|
-
text: "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
|
|
704
|
-
},
|
|
705
|
-
{ type: "text", text: "First" },
|
|
706
|
-
]);
|
|
698
|
+
// The persisted block stays out: the console is a surface, not an actor.
|
|
699
|
+
expect(messages[0].content).toEqual([{ type: "text", text: "First" }]);
|
|
707
700
|
});
|
|
708
701
|
|
|
709
702
|
test("rehydration order matches injection-time order for the full personal-memory set", async () => {
|
|
@@ -1047,12 +1040,14 @@ describe("loadFromDb metadata injection rehydration", () => {
|
|
|
1047
1040
|
expect(messages[2].content).toEqual([{ type: "text", text: "Tail" }]);
|
|
1048
1041
|
});
|
|
1049
1042
|
|
|
1050
|
-
test("ensureActorScopedHistory reloads when
|
|
1051
|
-
//
|
|
1052
|
-
// `loadFromDb
|
|
1053
|
-
//
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1043
|
+
test("ensureActorScopedHistory reloads when the actor's memory visibility changes", async () => {
|
|
1044
|
+
// Reuse of a resident conversation by a different actor must re-run
|
|
1045
|
+
// `loadFromDb`, or the incoming actor inherits the previous actor's
|
|
1046
|
+
// transcript with its personal-memory blocks still spliced in.
|
|
1047
|
+
//
|
|
1048
|
+
// Driven by a trust-class change rather than a channel change: personal
|
|
1049
|
+
// memory is keyed on the actor, so two channels at the same class are the
|
|
1050
|
+
// same view and reloading between them would prove nothing.
|
|
1056
1051
|
mockConversation = defaultConv();
|
|
1057
1052
|
mockDbMessages = [
|
|
1058
1053
|
{
|
|
@@ -1080,10 +1075,9 @@ describe("loadFromDb metadata injection rehydration", () => {
|
|
|
1080
1075
|
];
|
|
1081
1076
|
|
|
1082
1077
|
const conversation = makeConversation();
|
|
1083
|
-
// First load:
|
|
1084
|
-
// exposed via `shouldExposePersonalMemory({sourceChannel: "vellum", ...})`.
|
|
1078
|
+
// First load: guardian actor → personal memory exposed.
|
|
1085
1079
|
conversation.setTrustContext({
|
|
1086
|
-
trustClass: "
|
|
1080
|
+
trustClass: "guardian",
|
|
1087
1081
|
sourceChannel: "vellum",
|
|
1088
1082
|
});
|
|
1089
1083
|
await conversation.ensureActorScopedHistory();
|
|
@@ -1095,8 +1089,8 @@ describe("loadFromDb metadata injection rehydration", () => {
|
|
|
1095
1089
|
{ type: "text", text: "First" },
|
|
1096
1090
|
]);
|
|
1097
1091
|
|
|
1098
|
-
// Reuse
|
|
1099
|
-
//
|
|
1092
|
+
// Reuse by a contact. The cache must invalidate and trigger a reload that
|
|
1093
|
+
// strips the personal-memory block.
|
|
1100
1094
|
conversation.setTrustContext({
|
|
1101
1095
|
trustClass: "trusted_contact",
|
|
1102
1096
|
sourceChannel: "telegram",
|
|
@@ -47,6 +47,14 @@ const getConversationMock = mock((id: string) => {
|
|
|
47
47
|
return mockExistingConversations[id] ?? null;
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
const messagesInvalidated: string[] = [];
|
|
51
|
+
|
|
52
|
+
mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
53
|
+
publishConversationMessagesChanged: (conversationId: string) => {
|
|
54
|
+
messagesInvalidated.push(conversationId);
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
|
|
50
58
|
mock.module("../persistence/conversation-crud.js", () => ({
|
|
51
59
|
setConversationProcessingStartedAt: () => {},
|
|
52
60
|
isConversationProcessing: () => false,
|
|
@@ -144,6 +152,7 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
144
152
|
beforeEach(() => {
|
|
145
153
|
createConversationMock.mockClear();
|
|
146
154
|
addMessageMock.mockClear();
|
|
155
|
+
messagesInvalidated.length = 0;
|
|
147
156
|
getConversationMock.mockClear();
|
|
148
157
|
getBindingByChannelChatMock.mockClear();
|
|
149
158
|
upsertOutboundBindingMock.mockClear();
|
|
@@ -870,6 +879,183 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
870
879
|
expect(addMessageMock).not.toHaveBeenCalled();
|
|
871
880
|
});
|
|
872
881
|
|
|
882
|
+
// ── Passive default: body appended to the producing conversation ───
|
|
883
|
+
//
|
|
884
|
+
// The home feed points "Go to Conversation" at `sourceContextId`, so the
|
|
885
|
+
// body has to land there for that button to open something that contains
|
|
886
|
+
// the notification the user tapped.
|
|
887
|
+
|
|
888
|
+
test("passive vellum signal appends the body to the producing conversation", async () => {
|
|
889
|
+
mockExistingConversations["conv-producer"] = {
|
|
890
|
+
id: "conv-producer",
|
|
891
|
+
source: "schedule",
|
|
892
|
+
title: "Daily Briefing Delivery",
|
|
893
|
+
};
|
|
894
|
+
const signal = makeSignal({
|
|
895
|
+
requiresConversation: undefined,
|
|
896
|
+
sourceContextId: "conv-producer",
|
|
897
|
+
});
|
|
898
|
+
const copy = makeCopy({ body: "Daily Briefing for Tuesday." });
|
|
899
|
+
|
|
900
|
+
const result = await pairDeliveryWithConversation(
|
|
901
|
+
signal,
|
|
902
|
+
"vellum" as NotificationChannel,
|
|
903
|
+
copy,
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
expect(result.conversationId).toBe("conv-producer");
|
|
907
|
+
expect(result.messageId).toBe("msg-001");
|
|
908
|
+
expect(result.createdNewConversation).toBe(false);
|
|
909
|
+
expect(result.conversationFallbackUsed).toBe(false);
|
|
910
|
+
// Appending must never mint a sidebar entry.
|
|
911
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
912
|
+
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
913
|
+
const [conversationId, role] = addMessageMock.mock.calls[0]!;
|
|
914
|
+
expect(conversationId).toBe("conv-producer");
|
|
915
|
+
expect(role).toBe("assistant");
|
|
916
|
+
// `addMessage` publishes the metadata tag alone, so a client holding this
|
|
917
|
+
// conversation open needs the messages tag to refetch the transcript.
|
|
918
|
+
expect(messagesInvalidated).toContain("conv-producer");
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test("appends for a foreground producer the home feed will not mirror", async () => {
|
|
922
|
+
// `resolveHomeFeedMirror` declines a signal that is neither
|
|
923
|
+
// `assistant_tool`, nor flagged async-background, nor produced by a
|
|
924
|
+
// background-typed conversation, so no card is written for this one. The
|
|
925
|
+
// append is deliberately not gated on that: the vellum banner still
|
|
926
|
+
// deep-links to this conversation, and the client suppresses the banner
|
|
927
|
+
// entirely when the user is already viewing it, leaving the transcript as
|
|
928
|
+
// the only place the notification can land.
|
|
929
|
+
mockExistingConversations["conv-foreground"] = {
|
|
930
|
+
id: "conv-foreground",
|
|
931
|
+
source: "user",
|
|
932
|
+
title: "An ordinary chat",
|
|
933
|
+
};
|
|
934
|
+
const signal = makeSignal({
|
|
935
|
+
requiresConversation: undefined,
|
|
936
|
+
sourceChannel: "plugin",
|
|
937
|
+
sourceContextId: "conv-foreground",
|
|
938
|
+
attentionHints: {
|
|
939
|
+
requiresAction: false,
|
|
940
|
+
urgency: "medium",
|
|
941
|
+
isAsyncBackground: false,
|
|
942
|
+
visibleInSourceNow: false,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
const copy = makeCopy({ body: "Something you asked to watch changed." });
|
|
946
|
+
|
|
947
|
+
const result = await pairDeliveryWithConversation(
|
|
948
|
+
signal,
|
|
949
|
+
"vellum" as NotificationChannel,
|
|
950
|
+
copy,
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
expect(result.conversationId).toBe("conv-foreground");
|
|
954
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
955
|
+
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
956
|
+
expect(messagesInvalidated).toContain("conv-foreground");
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test("appended notification body skips indexing", async () => {
|
|
960
|
+
mockExistingConversations["conv-producer"] = {
|
|
961
|
+
id: "conv-producer",
|
|
962
|
+
source: "schedule",
|
|
963
|
+
title: null,
|
|
964
|
+
};
|
|
965
|
+
const signal = makeSignal({
|
|
966
|
+
requiresConversation: undefined,
|
|
967
|
+
sourceContextId: "conv-producer",
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
await pairDeliveryWithConversation(
|
|
971
|
+
signal,
|
|
972
|
+
"vellum" as NotificationChannel,
|
|
973
|
+
makeCopy(),
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
const options = addMessageMock.mock.calls[0]![3] as {
|
|
977
|
+
skipIndexing?: boolean;
|
|
978
|
+
};
|
|
979
|
+
expect(options.skipIndexing).toBe(true);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test("passive vellum signal appends regardless of the producing conversation's source", async () => {
|
|
983
|
+
// The reuse_existing branch requires a source match; appending to the
|
|
984
|
+
// producing conversation does not, since the row already exists and the
|
|
985
|
+
// feed links to it either way.
|
|
986
|
+
mockExistingConversations["conv-producer"] = {
|
|
987
|
+
id: "conv-producer",
|
|
988
|
+
source: "user",
|
|
989
|
+
title: null,
|
|
990
|
+
};
|
|
991
|
+
const signal = makeSignal({
|
|
992
|
+
requiresConversation: undefined,
|
|
993
|
+
sourceContextId: "conv-producer",
|
|
994
|
+
conversationMetadata: { source: "notification" },
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const result = await pairDeliveryWithConversation(
|
|
998
|
+
signal,
|
|
999
|
+
"vellum" as NotificationChannel,
|
|
1000
|
+
makeCopy(),
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
expect(result.conversationId).toBe("conv-producer");
|
|
1004
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test("passive vellum signal with a sentinel sourceContextId appends nothing", async () => {
|
|
1008
|
+
// Producers pass job ids, call session ids and `access-req-*` strings
|
|
1009
|
+
// here. Those resolve to no row, which matches the feed hiding the button.
|
|
1010
|
+
const signal = makeSignal({
|
|
1011
|
+
requiresConversation: undefined,
|
|
1012
|
+
sourceContextId: "job-1234",
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
const result = await pairDeliveryWithConversation(
|
|
1016
|
+
signal,
|
|
1017
|
+
"vellum" as NotificationChannel,
|
|
1018
|
+
makeCopy(),
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
expect(result.conversationId).toBeNull();
|
|
1022
|
+
expect(result.messageId).toBeNull();
|
|
1023
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1024
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
test("passive vellum signal appends to the producer, not the reuse_existing target", async () => {
|
|
1028
|
+
mockExistingConversations["conv-producer"] = {
|
|
1029
|
+
id: "conv-producer",
|
|
1030
|
+
source: "schedule",
|
|
1031
|
+
title: null,
|
|
1032
|
+
};
|
|
1033
|
+
mockExistingConversations["conv-hinted"] = {
|
|
1034
|
+
id: "conv-hinted",
|
|
1035
|
+
source: "notification",
|
|
1036
|
+
title: null,
|
|
1037
|
+
};
|
|
1038
|
+
const signal = makeSignal({
|
|
1039
|
+
requiresConversation: undefined,
|
|
1040
|
+
sourceContextId: "conv-producer",
|
|
1041
|
+
});
|
|
1042
|
+
const conversationAction: ConversationAction = {
|
|
1043
|
+
action: "reuse_existing",
|
|
1044
|
+
conversationId: "conv-hinted",
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
const result = await pairDeliveryWithConversation(
|
|
1048
|
+
signal,
|
|
1049
|
+
"vellum" as NotificationChannel,
|
|
1050
|
+
makeCopy(),
|
|
1051
|
+
{ conversationAction },
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
expect(result.conversationId).toBe("conv-producer");
|
|
1055
|
+
expect(addMessageMock).toHaveBeenCalledTimes(1);
|
|
1056
|
+
expect(addMessageMock.mock.calls[0]![0]).toBe("conv-producer");
|
|
1057
|
+
});
|
|
1058
|
+
|
|
873
1059
|
// ── conversationMetadata.conversationType override ─────────────────
|
|
874
1060
|
|
|
875
1061
|
test("uses conversationMetadata.conversationType when set, overriding channel strategy", async () => {
|