@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
package/src/runtime/AGENTS.md
CHANGED
|
@@ -168,6 +168,41 @@ Identifiers and plumbing notes:
|
|
|
168
168
|
- Guardian request records live in the gateway-owned `guardian_requests` table (and their `guardian_request_deliveries`); the daemon reads/writes them through the `channels/gateway-guardian-requests.ts` client.
|
|
169
169
|
- Legacy in-turn interception (`routes/guardian-approval-interception.ts` + the approval prompt watcher) still serves a guardian's own tool-approval prompts mid-turn, resolving via `conversation.handleConfirmationResponse(requestId, decision)`. It is a legacy rail — the reply router runs first; converge new work on the pipeline.
|
|
170
170
|
|
|
171
|
+
### Message metadata vocabulary
|
|
172
|
+
|
|
173
|
+
Several differently-scoped things are called metadata on the channel path, and
|
|
174
|
+
they nest, so a name that fits two of them reads as one concept. Counterpart to
|
|
175
|
+
the gateway's Channel Identity Vocabulary, which covers the wire side.
|
|
176
|
+
|
|
177
|
+
- **`sourceMetadata`** describes an inbound event in flight on the gateway to
|
|
178
|
+
daemon wire (`SourceMetadataSchema`, `packages/gateway-client`): provider
|
|
179
|
+
ids, trust verdict, admission policy. It is also persisted verbatim on the
|
|
180
|
+
stored inbound payload, because the retry sweep replays that payload and has
|
|
181
|
+
to reconstruct the same turn (`channel-retry-sweep.ts`).
|
|
182
|
+
- **`messages.metadata`** is the stored envelope on a row. Everything below is
|
|
183
|
+
a key inside it. `messageMetadataSchema` (`persistence/conversation-crud.ts`)
|
|
184
|
+
describes its shared keys; it is parsed where a reader needs them, not
|
|
185
|
+
enforced on every write.
|
|
186
|
+
- **`buildChannelMetadata`** (`routes/channel-metadata.ts`) builds the turn's
|
|
187
|
+
portion of that envelope: provenance, `userMessageChannel`, interfaces,
|
|
188
|
+
attachments. Callers spread it and add channel-specific keys alongside, so it
|
|
189
|
+
is a producer of the envelope, never a key within it.
|
|
190
|
+
- **`slackMeta`** is the per-row key describing what a row is in its Slack
|
|
191
|
+
conversation: `channelTs` for the row's own id, `threadTs` for its thread,
|
|
192
|
+
`reaction.targetChannelTs` for the message a reaction was attached to, plus
|
|
193
|
+
Slack's own file markers and timezone labels. Every channel-path reader goes
|
|
194
|
+
through it today.
|
|
195
|
+
- **`providerMeta`** is the channel-neutral counterpart of that key
|
|
196
|
+
(`messaging/provider-message-metadata.ts`): `conversationExternalId`,
|
|
197
|
+
`messageId`, `threadId`, `actorExternalId`, `eventKind` and
|
|
198
|
+
`reaction.targetMessageId`, deliberately the same vocabulary the inbound
|
|
199
|
+
wire uses in `InboundEventBase`, so a channel that can describe itself at
|
|
200
|
+
ingress can describe its stored rows without a translation of its own.
|
|
201
|
+
Writable by any channel, including one this repo has no code for. `readProviderMetadata` reads it and maps `slackMeta` onto it.
|
|
202
|
+
**Nothing writes or reads it yet**; it is the home a new channel should use
|
|
203
|
+
rather than inventing a sixth key, and existing readers move onto it as
|
|
204
|
+
channels adopt it.
|
|
205
|
+
|
|
171
206
|
### Channel verification: gateway-owned
|
|
172
207
|
|
|
173
208
|
Verification SESSION state (sessions, secrets, rate limits, validate+consume) AND the channel-verified OUTCOME (status / verifiedAt / verifiedVia) are both gateway-owned. The gateway holds the `channel_verification_sessions` + `channel_guardian_rate_limits` tables (`gateway/src/db/session-store.ts`) and mints all secrets in `gateway/src/verification/session-service.ts`; the daemon holds no session or rate-limit state (its legacy tables were dropped by gateway data migration m0014).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { emitDaemonError } from "../../daemon/startup-error.js";
|
|
4
|
+
import {
|
|
5
|
+
startRuntimeHttpServer,
|
|
6
|
+
stopRuntimeHttpServer,
|
|
7
|
+
} from "../http-server.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The daemon binds two client-facing transports. An occupied runtime HTTP port
|
|
11
|
+
* must abort startup the same way an occupied IPC socket does, so the daemon
|
|
12
|
+
* can never answer IPC (reading healthy to `vellum ps` and platform status)
|
|
13
|
+
* while the gateway proxies /v1/* to a foreign listener.
|
|
14
|
+
*/
|
|
15
|
+
describe("runtime HTTP port collision", () => {
|
|
16
|
+
const originalPort = process.env.RUNTIME_HTTP_PORT;
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await stopRuntimeHttpServer();
|
|
20
|
+
if (originalPort === undefined) {
|
|
21
|
+
delete process.env.RUNTIME_HTTP_PORT;
|
|
22
|
+
} else {
|
|
23
|
+
process.env.RUNTIME_HTTP_PORT = originalPort;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("aborts startup when a foreign process holds the runtime HTTP port", async () => {
|
|
28
|
+
/**
|
|
29
|
+
* Tests that a runtime HTTP bind collision fails daemon startup instead of
|
|
30
|
+
* silently degrading to IPC-only operation.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// GIVEN a foreign process holding a port
|
|
34
|
+
const foreign = Bun.serve({
|
|
35
|
+
port: 0,
|
|
36
|
+
hostname: "127.0.0.1",
|
|
37
|
+
fetch: () => new Response("foreign"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// AND the daemon configured to serve HTTP on that same port
|
|
41
|
+
process.env.RUNTIME_HTTP_PORT = String(foreign.port);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// WHEN the daemon starts its runtime HTTP server
|
|
45
|
+
const err = await startRuntimeHttpServer().then(
|
|
46
|
+
() => null,
|
|
47
|
+
(e: unknown) => e,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// THEN startup fails rather than continuing without HTTP
|
|
51
|
+
expect(err).toBeInstanceOf(Error);
|
|
52
|
+
|
|
53
|
+
// AND the failure carries the address-collision code the daemon's
|
|
54
|
+
// startup-error categorization branches on
|
|
55
|
+
expect((err as NodeJS.ErrnoException).code).toBe("EADDRINUSE");
|
|
56
|
+
|
|
57
|
+
// AND the message names the occupied port and the env var that moves it
|
|
58
|
+
expect((err as Error).message).toContain(String(foreign.port));
|
|
59
|
+
expect((err as Error).message).toContain("RUNTIME_HTTP_PORT");
|
|
60
|
+
} finally {
|
|
61
|
+
foreign.stop(true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("reports a runtime HTTP port collision as PORT_IN_USE on stderr", async () => {
|
|
66
|
+
/**
|
|
67
|
+
* Tests that the bind failure reaches consumers that parse the daemon's
|
|
68
|
+
* structured startup-error line (e.g. the macOS app) as PORT_IN_USE.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
// GIVEN a foreign process holding the daemon's runtime HTTP port
|
|
72
|
+
const foreign = Bun.serve({
|
|
73
|
+
port: 0,
|
|
74
|
+
hostname: "127.0.0.1",
|
|
75
|
+
fetch: () => new Response("foreign"),
|
|
76
|
+
});
|
|
77
|
+
process.env.RUNTIME_HTTP_PORT = String(foreign.port);
|
|
78
|
+
|
|
79
|
+
// AND stderr captured so the structured startup-error line is observable
|
|
80
|
+
const written: string[] = [];
|
|
81
|
+
const originalWrite = process.stderr.write.bind(process.stderr);
|
|
82
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
83
|
+
written.push(chunk.toString());
|
|
84
|
+
return true;
|
|
85
|
+
}) as typeof process.stderr.write;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// WHEN the daemon's startup error handler reports the bind failure
|
|
89
|
+
const err = await startRuntimeHttpServer().then(
|
|
90
|
+
() => null,
|
|
91
|
+
(e: unknown) => e,
|
|
92
|
+
);
|
|
93
|
+
emitDaemonError(err);
|
|
94
|
+
|
|
95
|
+
// THEN the structured line categorizes the failure as PORT_IN_USE
|
|
96
|
+
const line = written.find((entry) => entry.includes("DAEMON_ERROR:"));
|
|
97
|
+
expect(line).toBeDefined();
|
|
98
|
+
const structured = JSON.parse(
|
|
99
|
+
(line as string).replace("DAEMON_ERROR:", "").trim(),
|
|
100
|
+
) as Record<string, unknown>;
|
|
101
|
+
expect(structured.error).toBe("PORT_IN_USE");
|
|
102
|
+
} finally {
|
|
103
|
+
process.stderr.write = originalWrite;
|
|
104
|
+
foreign.stop(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("binds and leaves the singleton serving when the port is free", async () => {
|
|
109
|
+
/**
|
|
110
|
+
* Tests that the collision guard does not turn ordinary startup into a
|
|
111
|
+
* failure: a free port still yields a listening HTTP server.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
// GIVEN a free port for the runtime HTTP server
|
|
115
|
+
const probe = Bun.serve({
|
|
116
|
+
port: 0,
|
|
117
|
+
hostname: "127.0.0.1",
|
|
118
|
+
fetch: () => new Response("probe"),
|
|
119
|
+
});
|
|
120
|
+
const freePort = probe.port;
|
|
121
|
+
probe.stop(true);
|
|
122
|
+
process.env.RUNTIME_HTTP_PORT = String(freePort);
|
|
123
|
+
|
|
124
|
+
// WHEN the daemon starts its runtime HTTP server
|
|
125
|
+
await startRuntimeHttpServer();
|
|
126
|
+
|
|
127
|
+
// THEN the server answers its liveness probe
|
|
128
|
+
const response = await fetch(`http://127.0.0.1:${freePort}/healthz`);
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the web presence policy.
|
|
3
|
+
*
|
|
4
|
+
* The invariant under test is fail-open: only a fresh report from a `web`
|
|
5
|
+
* client with `visible: true` and a matching `focusedConversationId` answers
|
|
6
|
+
* `true`. Every other shape (stale, hidden, mismatched conversation, no
|
|
7
|
+
* report, wrong interface, no clients, hub throw) answers `false` so the push
|
|
8
|
+
* is sent. Scoping the read to an actor principal narrows what counts as
|
|
9
|
+
* focus, so it can only ever turn a `true` into a `false`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
clearHubClients,
|
|
16
|
+
registerHubClient,
|
|
17
|
+
type RegisterHubClientArgs,
|
|
18
|
+
} from "../../__tests__/helpers/hub-clients.js";
|
|
19
|
+
import { assistantEventHub } from "../assistant-event-hub.js";
|
|
20
|
+
import {
|
|
21
|
+
isWebConversationFocused,
|
|
22
|
+
WEB_PRESENCE_STALE_AFTER_MS,
|
|
23
|
+
} from "../web-presence.js";
|
|
24
|
+
|
|
25
|
+
const CONVERSATION_ID = "conv-1";
|
|
26
|
+
|
|
27
|
+
// ── Test helpers ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** A `now` far enough past every report to push all of them out of the window. */
|
|
30
|
+
function afterStaleness(): Date {
|
|
31
|
+
return new Date(Date.now() + WEB_PRESENCE_STALE_AFTER_MS + 1_000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerClient(
|
|
35
|
+
args: Omit<RegisterHubClientArgs, "hub" | "presence">,
|
|
36
|
+
): void {
|
|
37
|
+
registerHubClient({ ...args, hub: assistantEventHub, interfaceId: "web" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function reportWebPresence(
|
|
41
|
+
clientId: string,
|
|
42
|
+
report: { visible: boolean; focusedConversationId: string | null },
|
|
43
|
+
): void {
|
|
44
|
+
assistantEventHub.setClientWebPresence(clientId, report);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clientEntry(clientId: string) {
|
|
48
|
+
const client = assistantEventHub
|
|
49
|
+
.listClientsByInterface("web")
|
|
50
|
+
.find((entry) => entry.clientId === clientId);
|
|
51
|
+
if (!client) {
|
|
52
|
+
throw new Error(`missing test client ${clientId}`);
|
|
53
|
+
}
|
|
54
|
+
return client;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
clearHubClients(assistantEventHub);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe("isWebConversationFocused", () => {
|
|
64
|
+
test("fresh visible report focused on the conversation is focused", () => {
|
|
65
|
+
registerClient({ clientId: "web-1" });
|
|
66
|
+
reportWebPresence("web-1", {
|
|
67
|
+
visible: true,
|
|
68
|
+
focusedConversationId: CONVERSATION_ID,
|
|
69
|
+
});
|
|
70
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("report older than the staleness bound is not focused", () => {
|
|
74
|
+
registerClient({ clientId: "web-1" });
|
|
75
|
+
reportWebPresence("web-1", {
|
|
76
|
+
visible: true,
|
|
77
|
+
focusedConversationId: CONVERSATION_ID,
|
|
78
|
+
});
|
|
79
|
+
expect(
|
|
80
|
+
isWebConversationFocused(CONVERSATION_ID, { now: afterStaleness() }),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("semantic report expires even while SSE transport stays alive", () => {
|
|
85
|
+
registerClient({ clientId: "web-1" });
|
|
86
|
+
reportWebPresence("web-1", {
|
|
87
|
+
visible: true,
|
|
88
|
+
focusedConversationId: CONVERSATION_ID,
|
|
89
|
+
});
|
|
90
|
+
const now = new Date(Date.now() + WEB_PRESENCE_STALE_AFTER_MS + 1_000);
|
|
91
|
+
clientEntry("web-1").lastActiveAt = now;
|
|
92
|
+
|
|
93
|
+
expect(isWebConversationFocused(CONVERSATION_ID, { now })).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("transport expiry is not focused even with a fresh semantic report", () => {
|
|
97
|
+
registerClient({ clientId: "web-1" });
|
|
98
|
+
reportWebPresence("web-1", {
|
|
99
|
+
visible: true,
|
|
100
|
+
focusedConversationId: CONVERSATION_ID,
|
|
101
|
+
});
|
|
102
|
+
const now = new Date();
|
|
103
|
+
clientEntry("web-1").lastActiveAt = new Date(
|
|
104
|
+
now.getTime() - WEB_PRESENCE_STALE_AFTER_MS - 1_000,
|
|
105
|
+
);
|
|
106
|
+
clientEntry("web-1").webPresence!.reportedAt = now;
|
|
107
|
+
|
|
108
|
+
expect(isWebConversationFocused(CONVERSATION_ID, { now })).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("hidden tab is not focused even on the matching conversation", () => {
|
|
112
|
+
registerClient({ clientId: "web-1" });
|
|
113
|
+
reportWebPresence("web-1", {
|
|
114
|
+
visible: false,
|
|
115
|
+
focusedConversationId: CONVERSATION_ID,
|
|
116
|
+
});
|
|
117
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("visible tab focused on a different conversation is not focused", () => {
|
|
121
|
+
registerClient({ clientId: "web-1" });
|
|
122
|
+
reportWebPresence("web-1", {
|
|
123
|
+
visible: true,
|
|
124
|
+
focusedConversationId: "conv-other",
|
|
125
|
+
});
|
|
126
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("visible tab with no focused conversation is not focused", () => {
|
|
130
|
+
registerClient({ clientId: "web-1" });
|
|
131
|
+
reportWebPresence("web-1", { visible: true, focusedConversationId: null });
|
|
132
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("client that never reported presence is not focused", () => {
|
|
136
|
+
registerClient({ clientId: "web-1" });
|
|
137
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("a macos client's presence does not count as web focus", () => {
|
|
141
|
+
registerHubClient({
|
|
142
|
+
hub: assistantEventHub,
|
|
143
|
+
clientId: "mac-1",
|
|
144
|
+
interfaceId: "macos",
|
|
145
|
+
});
|
|
146
|
+
assistantEventHub.setClientWebPresence("mac-1", {
|
|
147
|
+
visible: true,
|
|
148
|
+
focusedConversationId: CONVERSATION_ID,
|
|
149
|
+
});
|
|
150
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("no connected clients is not focused", () => {
|
|
154
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("a hub read that throws is not focused", () => {
|
|
158
|
+
const spy = spyOn(
|
|
159
|
+
assistantEventHub,
|
|
160
|
+
"listClientsByInterface",
|
|
161
|
+
).mockImplementation(() => {
|
|
162
|
+
throw new Error("hub exploded");
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(false);
|
|
166
|
+
} finally {
|
|
167
|
+
spy.mockRestore();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("one focused web client among several is enough", () => {
|
|
172
|
+
registerClient({ clientId: "web-1" });
|
|
173
|
+
registerClient({ clientId: "web-2" });
|
|
174
|
+
reportWebPresence("web-1", {
|
|
175
|
+
visible: true,
|
|
176
|
+
focusedConversationId: "conv-other",
|
|
177
|
+
});
|
|
178
|
+
reportWebPresence("web-2", {
|
|
179
|
+
visible: true,
|
|
180
|
+
focusedConversationId: CONVERSATION_ID,
|
|
181
|
+
});
|
|
182
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("isWebConversationFocused scoped to an actor principal", () => {
|
|
187
|
+
test("matching actor principal is focused", () => {
|
|
188
|
+
registerClient({ clientId: "web-1", actorPrincipalId: "actor-a" });
|
|
189
|
+
reportWebPresence("web-1", {
|
|
190
|
+
visible: true,
|
|
191
|
+
focusedConversationId: CONVERSATION_ID,
|
|
192
|
+
});
|
|
193
|
+
expect(
|
|
194
|
+
isWebConversationFocused(CONVERSATION_ID, {
|
|
195
|
+
actorPrincipalId: "actor-a",
|
|
196
|
+
}),
|
|
197
|
+
).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("another actor's focused tab does not count", () => {
|
|
201
|
+
registerClient({ clientId: "web-1", actorPrincipalId: "actor-a" });
|
|
202
|
+
reportWebPresence("web-1", {
|
|
203
|
+
visible: true,
|
|
204
|
+
focusedConversationId: CONVERSATION_ID,
|
|
205
|
+
});
|
|
206
|
+
expect(
|
|
207
|
+
isWebConversationFocused(CONVERSATION_ID, {
|
|
208
|
+
actorPrincipalId: "actor-b",
|
|
209
|
+
}),
|
|
210
|
+
).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("client with no principal does not match a supplied principal", () => {
|
|
214
|
+
registerClient({ clientId: "web-1" });
|
|
215
|
+
reportWebPresence("web-1", {
|
|
216
|
+
visible: true,
|
|
217
|
+
focusedConversationId: CONVERSATION_ID,
|
|
218
|
+
});
|
|
219
|
+
expect(
|
|
220
|
+
isWebConversationFocused(CONVERSATION_ID, {
|
|
221
|
+
actorPrincipalId: "actor-a",
|
|
222
|
+
}),
|
|
223
|
+
).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("omitting the principal counts any focused web client", () => {
|
|
227
|
+
registerClient({ clientId: "web-1", actorPrincipalId: "actor-a" });
|
|
228
|
+
reportWebPresence("web-1", {
|
|
229
|
+
visible: true,
|
|
230
|
+
focusedConversationId: CONVERSATION_ID,
|
|
231
|
+
});
|
|
232
|
+
expect(isWebConversationFocused(CONVERSATION_ID)).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -109,6 +109,24 @@ export interface ClientPresence {
|
|
|
109
109
|
reportedAt: Date;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Web tab visibility and conversation focus, as reported by a `web`-interface
|
|
114
|
+
* client (a browser tab or the Electron renderer). Distinct from
|
|
115
|
+
* {@link ClientPresence}: scoped to a single conversation rather than app-wide
|
|
116
|
+
* attendance, and reported by the shared web renderer rather than the macOS
|
|
117
|
+
* host-proxy bridge. See `runtime/web-presence.ts` for the read-side policy.
|
|
118
|
+
*/
|
|
119
|
+
export interface WebPresenceReport {
|
|
120
|
+
visible: boolean;
|
|
121
|
+
/** The conversation currently focused (chat composer on screen), or `null`. */
|
|
122
|
+
focusedConversationId: string | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ClientWebPresence extends WebPresenceReport {
|
|
126
|
+
/** When the daemon last heard from the client. Drives staleness. */
|
|
127
|
+
reportedAt: Date;
|
|
128
|
+
}
|
|
129
|
+
|
|
112
130
|
interface ClientEntry extends BaseSubscriberEntry {
|
|
113
131
|
type: "client";
|
|
114
132
|
clientId: string;
|
|
@@ -130,6 +148,11 @@ interface ClientEntry extends BaseSubscriberEntry {
|
|
|
130
148
|
* In-memory only, so consumers must fail open when it is absent.
|
|
131
149
|
*/
|
|
132
150
|
presence?: ClientPresence;
|
|
151
|
+
/**
|
|
152
|
+
* Last web tab visibility/focus reported by this client, for clients that
|
|
153
|
+
* report it. In-memory only, so consumers must fail open when it is absent.
|
|
154
|
+
*/
|
|
155
|
+
webPresence?: ClientWebPresence;
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
interface ProcessEntry extends BaseSubscriberEntry {
|
|
@@ -621,6 +644,21 @@ export class AssistantEventHub {
|
|
|
621
644
|
return matched;
|
|
622
645
|
}
|
|
623
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Record the web tab visibility/focused-conversation reported by a client.
|
|
649
|
+
* `reportedAt` always advances. Returns true when at least one active
|
|
650
|
+
* client entry matched.
|
|
651
|
+
*/
|
|
652
|
+
setClientWebPresence(clientId: string, report: WebPresenceReport): boolean {
|
|
653
|
+
const now = new Date();
|
|
654
|
+
let matched = false;
|
|
655
|
+
for (const entry of this.activeClientEntries(clientId)) {
|
|
656
|
+
entry.webPresence = { ...report, reportedAt: now };
|
|
657
|
+
matched = true;
|
|
658
|
+
}
|
|
659
|
+
return matched;
|
|
660
|
+
}
|
|
661
|
+
|
|
624
662
|
/**
|
|
625
663
|
* Force-disconnect a client by disposing all subscribers for the given
|
|
626
664
|
* `clientId`. Returns the number of disposed entries.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { MessageAudience } from "@vellumai/gateway-client";
|
|
2
|
+
|
|
1
3
|
import { stripVellumLinks } from "../daemon/assistant-attachments.js";
|
|
2
4
|
import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
|
|
3
5
|
import { renderHistoryContent } from "../daemon/handlers/shared.js";
|
|
6
|
+
import { editChannelMessage } from "../messaging/providers/index.js";
|
|
4
7
|
import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
|
|
5
8
|
import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
|
|
6
9
|
import {
|
|
@@ -35,13 +38,11 @@ type DeliverRenderedReplyParams = {
|
|
|
35
38
|
* 1-based count of segments delivered so far (including prior attempts). */
|
|
36
39
|
onSegmentDelivered?: (deliveredCount: number) => void;
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
+
* Restricts the reply to one reader. Absent means the whole room sees it.
|
|
42
|
+
* A restricted reply is fire-and-forget: it cannot be edited or deleted
|
|
43
|
+
* after posting.
|
|
41
44
|
*/
|
|
42
|
-
|
|
43
|
-
/** Channel-specific user ID — required when `ephemeral` is true. */
|
|
44
|
-
user?: string;
|
|
45
|
+
audience?: MessageAudience;
|
|
45
46
|
/** When provided, the first segment will update the existing message
|
|
46
47
|
* identified by this ts instead of posting a new one (Slack-specific). */
|
|
47
48
|
messageTs?: string;
|
|
@@ -140,8 +141,7 @@ export async function deliverRenderedReplyViaCallback(
|
|
|
140
141
|
interSegmentDelayMs = INTER_SEGMENT_DELAY_MS,
|
|
141
142
|
startFromSegment = 0,
|
|
142
143
|
onSegmentDelivered,
|
|
143
|
-
|
|
144
|
-
user,
|
|
144
|
+
audience,
|
|
145
145
|
messageTs,
|
|
146
146
|
onMessageTs,
|
|
147
147
|
} = params;
|
|
@@ -167,9 +167,7 @@ export async function deliverRenderedReplyViaCallback(
|
|
|
167
167
|
chatId,
|
|
168
168
|
attachments: replyAttachments,
|
|
169
169
|
assistantId,
|
|
170
|
-
|
|
171
|
-
user,
|
|
172
|
-
messageTs,
|
|
170
|
+
audience,
|
|
173
171
|
},
|
|
174
172
|
);
|
|
175
173
|
if (result.ts) {
|
|
@@ -187,9 +185,7 @@ export async function deliverRenderedReplyViaCallback(
|
|
|
187
185
|
chatId,
|
|
188
186
|
attachments: replyAttachments,
|
|
189
187
|
assistantId,
|
|
190
|
-
|
|
191
|
-
user,
|
|
192
|
-
messageTs,
|
|
188
|
+
audience,
|
|
193
189
|
},
|
|
194
190
|
);
|
|
195
191
|
const deliveredTs = result.ts ?? messageTs;
|
|
@@ -210,21 +206,51 @@ export async function deliverRenderedReplyViaCallback(
|
|
|
210
206
|
const isLastSegment = i === deliverableSegments.length - 1;
|
|
211
207
|
const isFirstSegment = i === startFromSegment;
|
|
212
208
|
const segmentText = deliverableSegments[i];
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
// Ask the channel to render richly; each channel's adapter decides how
|
|
210
|
+
// (Slack to Block Kit). Channels without rich rendering send plain text.
|
|
211
|
+
const segmentAttachments = isLastSegment ? replyAttachments : undefined;
|
|
212
|
+
const editTarget = isFirstSegment ? currentMessageTs : undefined;
|
|
213
|
+
|
|
214
|
+
let result: ChannelDeliveryResult;
|
|
215
|
+
if (editTarget) {
|
|
216
|
+
result = await editChannelMessage(callbackUrl, {
|
|
216
217
|
chatId,
|
|
218
|
+
messageId: editTarget,
|
|
217
219
|
text: segmentText,
|
|
218
|
-
// Ask the channel to render richly; each channel's adapter decides how
|
|
219
|
-
// (Slack → Block Kit). Channels without rich rendering send plain text.
|
|
220
220
|
useBlocks: true,
|
|
221
|
-
|
|
221
|
+
});
|
|
222
|
+
// An edit replaces the text of one message. Attachments are always new
|
|
223
|
+
// messages, so they still have to be posted alongside it.
|
|
224
|
+
//
|
|
225
|
+
// Failures here are not the reply failing. A transport rejects a total
|
|
226
|
+
// attachment failure only when there is no text to fall back on, and the
|
|
227
|
+
// edit above already delivered the text. Rethrowing would mark a reply
|
|
228
|
+
// undelivered that the reader can see, and have the sweep repost it.
|
|
229
|
+
if (segmentAttachments) {
|
|
230
|
+
try {
|
|
231
|
+
await deliverChannelReply(callbackUrl, {
|
|
232
|
+
chatId,
|
|
233
|
+
attachments: segmentAttachments,
|
|
234
|
+
assistantId,
|
|
235
|
+
audience,
|
|
236
|
+
});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
log.warn(
|
|
239
|
+
{ err, chatId },
|
|
240
|
+
"Attachments failed after an in-place edit; the edited text stands",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
result = await deliverChannelReply(callbackUrl, {
|
|
246
|
+
chatId,
|
|
247
|
+
text: segmentText,
|
|
248
|
+
useBlocks: true,
|
|
249
|
+
attachments: segmentAttachments,
|
|
222
250
|
assistantId,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
},
|
|
227
|
-
);
|
|
251
|
+
audience,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
228
254
|
|
|
229
255
|
if (result.ts) {
|
|
230
256
|
currentMessageTs = result.ts;
|
|
@@ -251,10 +277,8 @@ export type DeliverReplyOptions = {
|
|
|
251
277
|
sinceMessageId?: string;
|
|
252
278
|
startFromSegment?: number;
|
|
253
279
|
onSegmentDelivered?: (deliveredCount: number) => void;
|
|
254
|
-
/**
|
|
255
|
-
|
|
256
|
-
/** Channel-specific user ID — required when `ephemeral` is true. */
|
|
257
|
-
user?: string;
|
|
280
|
+
/** Restricts the reply to one reader. Absent means the whole room. */
|
|
281
|
+
audience?: MessageAudience;
|
|
258
282
|
/** Update an existing message instead of posting a new one. */
|
|
259
283
|
messageTs?: string;
|
|
260
284
|
/** Called with the ts of the delivered/updated message. */
|
|
@@ -384,8 +408,7 @@ async function deliverPersistedAssistantMessageViaCallback(
|
|
|
384
408
|
assistantId,
|
|
385
409
|
startFromSegment: options?.startFromSegment,
|
|
386
410
|
onSegmentDelivered: options?.onSegmentDelivered,
|
|
387
|
-
|
|
388
|
-
user: options?.user,
|
|
411
|
+
audience: options?.audience,
|
|
389
412
|
messageTs: options?.messageTs,
|
|
390
413
|
onMessageTs: composedOnMessageTs,
|
|
391
414
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { AssistantEvent } from "../api/index.js";
|
|
6
|
+
import { audienceForReader } from "../channels/message-audience.js";
|
|
6
7
|
import {
|
|
7
8
|
type ChannelId,
|
|
8
9
|
isChannelId,
|
|
@@ -333,10 +334,11 @@ export async function sweepFailedEvents(
|
|
|
333
334
|
text: DISK_PRESSURE_REMOTE_BLOCK_REPLY,
|
|
334
335
|
assistantId,
|
|
335
336
|
};
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
replyPayload.audience = audienceForReader(
|
|
338
|
+
sourceChannel,
|
|
339
|
+
externalChatId,
|
|
340
|
+
requesterExternalUserId,
|
|
341
|
+
);
|
|
340
342
|
try {
|
|
341
343
|
await deliverChannelReply(replyCallbackUrl, replyPayload);
|
|
342
344
|
} catch (err) {
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
canActOnPrivilegedDocuments,
|
|
5
|
+
canSeePersonalMemory,
|
|
5
6
|
isArchiveBySenderAuthorized,
|
|
6
7
|
} from "./effective-capabilities.js";
|
|
7
8
|
|
|
@@ -90,3 +91,21 @@ describe("isArchiveBySenderAuthorized", () => {
|
|
|
90
91
|
).toBe(false);
|
|
91
92
|
});
|
|
92
93
|
});
|
|
94
|
+
|
|
95
|
+
describe("canSeePersonalMemory", () => {
|
|
96
|
+
test("only the guardian class sees personal memory", () => {
|
|
97
|
+
expect(canSeePersonalMemory({ trustClass: "guardian" })).toBe(true);
|
|
98
|
+
for (const trustClass of [
|
|
99
|
+
"trusted_contact",
|
|
100
|
+
"unverified_contact",
|
|
101
|
+
"unknown",
|
|
102
|
+
]) {
|
|
103
|
+
expect(canSeePersonalMemory({ trustClass })).toBe(false);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("an unrecognized or absent class fails closed", () => {
|
|
108
|
+
expect(canSeePersonalMemory({ trustClass: "non_guardian" })).toBe(false);
|
|
109
|
+
expect(canSeePersonalMemory({ trustClass: undefined })).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|