@vellumai/assistant 0.11.7 → 0.11.8-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/ARCHITECTURE.md +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- 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/channels.ts +7 -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/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -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/channels.ts +7 -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/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
|
@@ -8,6 +8,8 @@ const LIVE_VOICE_CLIENT_FRAME_TYPES = [
|
|
|
8
8
|
"end",
|
|
9
9
|
"update_config",
|
|
10
10
|
"attach_image",
|
|
11
|
+
"attach_frame",
|
|
12
|
+
"sight_frame",
|
|
11
13
|
"text",
|
|
12
14
|
] as const;
|
|
13
15
|
|
|
@@ -194,18 +196,89 @@ export interface LiveVoiceClientUpdateConfigFrame {
|
|
|
194
196
|
* caps size, and stores the blob, and routing an image through this socket
|
|
195
197
|
* would duplicate all of it on a transport tuned for 50 ms audio frames.
|
|
196
198
|
*
|
|
197
|
-
* The id is
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
199
|
+
* The id is persisted the moment it arrives, as a standalone user message that
|
|
200
|
+
* runs no turn (see `live-voice-photo.ts`). A snap the user watched themselves
|
|
201
|
+
* take has to show up in the conversation whether or not they ever speak about
|
|
202
|
+
* it, and landing it in history immediately is what makes shutter-then-speak
|
|
203
|
+
* and speak-then-shutter answer the same way.
|
|
204
|
+
*
|
|
205
|
+
* Deliberately not {@link LiveVoiceClientAttachFrameFrame}, which parks its id
|
|
206
|
+
* for the next spoken turn instead. The two frames look alike on the wire and
|
|
207
|
+
* answer opposite questions: a deliberate snap must appear now; an ambient
|
|
208
|
+
* camera frame is worthless without the words that follow it.
|
|
203
209
|
*/
|
|
204
210
|
export interface LiveVoiceClientAttachImageFrame {
|
|
205
211
|
readonly type: "attach_image";
|
|
206
212
|
readonly attachmentId: string;
|
|
207
213
|
}
|
|
208
214
|
|
|
215
|
+
/**
|
|
216
|
+
* An ambient camera frame the client picked for the assistant to see, already
|
|
217
|
+
* uploaded over the normal attachment route, and sent here as an id alone for
|
|
218
|
+
* the same reason {@link LiveVoiceClientAttachImageFrame} sends one.
|
|
219
|
+
*
|
|
220
|
+
* The id is *parked* on the session rather than persisted. It rides the next
|
|
221
|
+
* spoken turn's own user message, and the slot is cleared as that turn starts,
|
|
222
|
+
* so a frame that arrives mid-turn belongs to the turn after it. Nothing the
|
|
223
|
+
* user asked for happened here: a frame nobody ever speaks over must leave no
|
|
224
|
+
* trace in the conversation, and a frame that does get words must be part of
|
|
225
|
+
* the same message as those words. Riding the newest user message is also what
|
|
226
|
+
* carries it through a context-overflow retry, the only message media survives
|
|
227
|
+
* on (`conversation-media-retry.ts`).
|
|
228
|
+
*
|
|
229
|
+
* Latest wins. A second frame overwrites the first, because what matters is
|
|
230
|
+
* what the camera is pointed at when the user speaks.
|
|
231
|
+
*
|
|
232
|
+
* A `null` id is the other direction: unpark, meaning the client's camera is
|
|
233
|
+
* no longer on and the slot must not keep answering for it. It clears the slot
|
|
234
|
+
* and gives the frame up, so a viewfinder the user closed leaves nothing
|
|
235
|
+
* staged behind it. Sending it needs no id of its own because the slot only
|
|
236
|
+
* ever holds one frame and only the client that parked it can be closing.
|
|
237
|
+
*
|
|
238
|
+
* `null` and a missing field are different things. An absent `attachmentId` is
|
|
239
|
+
* still a malformed frame, because a client that meant to unpark says so.
|
|
240
|
+
*
|
|
241
|
+
* Version skew: a build carrying the park half but not this one refuses the
|
|
242
|
+
* null with the attributable recoverable error, which the web client logs and
|
|
243
|
+
* drops. Only dev builds cut between the two changes are affected, and there
|
|
244
|
+
* the cost is a frame the session gives up at close instead of at unpark.
|
|
245
|
+
*/
|
|
246
|
+
export interface LiveVoiceClientAttachFrameFrame {
|
|
247
|
+
readonly type: "attach_frame";
|
|
248
|
+
readonly attachmentId: string | null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* An ambient camera frame the client's gate kept, already uploaded over the
|
|
253
|
+
* normal attachment route and sent here as an id alone for the same reason
|
|
254
|
+
* {@link LiveVoiceClientAttachImageFrame} sends one.
|
|
255
|
+
*
|
|
256
|
+
* Three client frames carry an image and they mean three different things:
|
|
257
|
+
*
|
|
258
|
+
* - `attach_image` is a photo the user deliberately snapped. It persists
|
|
259
|
+
* standalone the moment it arrives and drives the client's receipt strip.
|
|
260
|
+
* - `attach_frame` parks one frame on the session for the next spoken turn's
|
|
261
|
+
* own user message to carry, and persists nothing by itself.
|
|
262
|
+
* - `sight_frame` persists an ambient keep immediately as its own user
|
|
263
|
+
* message, tagged as a camera frame, and runs no turn.
|
|
264
|
+
*
|
|
265
|
+
* A keep is tagged so retention can age it out of the model's context (newest
|
|
266
|
+
* few stay images, older ones become timestamped stubs) while the transcript
|
|
267
|
+
* keeps every one of them. The transcript is therefore the record of what the
|
|
268
|
+
* assistant saw, and the model correlates a frame with speech by adjacency
|
|
269
|
+
* rather than by any attachment to a turn.
|
|
270
|
+
*
|
|
271
|
+
* A frame the attachment store does not know is refused with `frameType:
|
|
272
|
+
* "sight_frame"` and `recoverable: true`: the session is fine and only this
|
|
273
|
+
* frame failed. Attributing it is what lets the client retract the preview it
|
|
274
|
+
* already showed instead of filing the error with the transient transcriber
|
|
275
|
+
* and TTS blips that share `recoverable`.
|
|
276
|
+
*/
|
|
277
|
+
export interface LiveVoiceClientSightFrameFrame {
|
|
278
|
+
readonly type: "sight_frame";
|
|
279
|
+
readonly attachmentId: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
209
282
|
/**
|
|
210
283
|
* A user turn the client already has as text, taken without the microphone.
|
|
211
284
|
*
|
|
@@ -253,6 +326,8 @@ export type LiveVoiceClientFrame =
|
|
|
253
326
|
| LiveVoiceClientEndFrame
|
|
254
327
|
| LiveVoiceClientUpdateConfigFrame
|
|
255
328
|
| LiveVoiceClientAttachImageFrame
|
|
329
|
+
| LiveVoiceClientAttachFrameFrame
|
|
330
|
+
| LiveVoiceClientSightFrameFrame
|
|
256
331
|
| LiveVoiceClientTextTurnFrame;
|
|
257
332
|
|
|
258
333
|
interface LiveVoiceBinaryAudioFrame {
|
|
@@ -513,11 +588,12 @@ export interface LiveVoiceErrorServerFrame extends LiveVoiceServerFrameBase {
|
|
|
513
588
|
* daemons predating the field.
|
|
514
589
|
*
|
|
515
590
|
* It exists so an `unknown_type` is attributable. A client that sends more
|
|
516
|
-
* than one optional frame (today: `update_config
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* dropped with
|
|
591
|
+
* than one optional frame (today: `update_config`, `attach_image`,
|
|
592
|
+
* `attach_frame`, `sight_frame`, and `text`) gets the same code for any of
|
|
593
|
+
* them, and without this has to assume which one was refused. The wrong
|
|
594
|
+
* assumption is silent in both directions: settings stop applying for a
|
|
595
|
+
* session, or a photo the user watched themselves take is dropped with
|
|
596
|
+
* nothing said.
|
|
521
597
|
*/
|
|
522
598
|
readonly frameType?: string;
|
|
523
599
|
/**
|
|
@@ -654,6 +730,10 @@ export function validateLiveVoiceClientFrame(
|
|
|
654
730
|
return validateUpdateConfigFrame(value);
|
|
655
731
|
case "attach_image":
|
|
656
732
|
return validateAttachImageFrame(value);
|
|
733
|
+
case "attach_frame":
|
|
734
|
+
return validateAttachFrameFrame(value);
|
|
735
|
+
case "sight_frame":
|
|
736
|
+
return validateSightFrameFrame(value);
|
|
657
737
|
case "text":
|
|
658
738
|
return validateTextTurnFrame(value);
|
|
659
739
|
}
|
|
@@ -750,6 +830,62 @@ function validateAttachImageFrame(
|
|
|
750
830
|
};
|
|
751
831
|
}
|
|
752
832
|
|
|
833
|
+
function validateAttachFrameFrame(
|
|
834
|
+
value: Record<string, unknown>,
|
|
835
|
+
): LiveVoiceParseResult<LiveVoiceClientAttachFrameFrame> {
|
|
836
|
+
if (!("attachmentId" in value)) {
|
|
837
|
+
return protocolError(
|
|
838
|
+
"missing_required_field",
|
|
839
|
+
"attach_frame frame is missing required field attachmentId",
|
|
840
|
+
"attachmentId",
|
|
841
|
+
"attach_frame",
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (value.attachmentId !== null && !isNonEmptyString(value.attachmentId)) {
|
|
846
|
+
return protocolError(
|
|
847
|
+
"invalid_field",
|
|
848
|
+
"attach_frame frame field attachmentId must be a non-empty string or null",
|
|
849
|
+
"attachmentId",
|
|
850
|
+
"attach_frame",
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
ok: true,
|
|
856
|
+
frame: { type: "attach_frame", attachmentId: value.attachmentId },
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function validateSightFrameFrame(
|
|
861
|
+
value: Record<string, unknown>,
|
|
862
|
+
): LiveVoiceParseResult<LiveVoiceClientSightFrameFrame> {
|
|
863
|
+
if (!("attachmentId" in value)) {
|
|
864
|
+
return protocolError(
|
|
865
|
+
"missing_required_field",
|
|
866
|
+
"sight_frame frame is missing required field attachmentId",
|
|
867
|
+
"attachmentId",
|
|
868
|
+
"sight_frame",
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// No null form, unlike `attach_frame`. Nothing is staged for a keep to give
|
|
873
|
+
// up: it persisted on arrival, so a closing viewfinder has nothing to clear.
|
|
874
|
+
if (!isNonEmptyString(value.attachmentId)) {
|
|
875
|
+
return protocolError(
|
|
876
|
+
"invalid_field",
|
|
877
|
+
"sight_frame frame field attachmentId must be a non-empty string",
|
|
878
|
+
"attachmentId",
|
|
879
|
+
"sight_frame",
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return {
|
|
884
|
+
ok: true,
|
|
885
|
+
frame: { type: "sight_frame", attachmentId: value.attachmentId },
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
753
889
|
function validateUpdateConfigFrame(
|
|
754
890
|
value: Record<string, unknown>,
|
|
755
891
|
): LiveVoiceParseResult<LiveVoiceClientUpdateConfigFrame> {
|
|
@@ -61,6 +61,13 @@ export const providerMessageMetadataSchema = z
|
|
|
61
61
|
* reaction. The message it acts on is `reaction.targetMessageId`.
|
|
62
62
|
*/
|
|
63
63
|
messageId: z.string().optional(),
|
|
64
|
+
/**
|
|
65
|
+
* Provider ids of the further posts this row's delivery produced beyond
|
|
66
|
+
* `messageId`: one stored reply split at tool boundaries or length
|
|
67
|
+
* limits posts several provider messages, and they all belong to this
|
|
68
|
+
* one row. A reaction naming any of them resolves here.
|
|
69
|
+
*/
|
|
70
|
+
additionalMessageIds: z.array(z.string()).optional(),
|
|
64
71
|
/**
|
|
65
72
|
* Provider id of the thread this row sits in, absent when it is not in one.
|
|
66
73
|
* Never synthesized from `messageId`: a value here asserts that a thread
|
|
@@ -8,7 +8,9 @@ const slack = {
|
|
|
8
8
|
sendSlackReply: mock((..._args: unknown[]) =>
|
|
9
9
|
Promise.resolve({ ts: "slack-ts" }),
|
|
10
10
|
),
|
|
11
|
-
sendSlackReaction: mock((..._args: unknown[]) =>
|
|
11
|
+
sendSlackReaction: mock((..._args: unknown[]) =>
|
|
12
|
+
Promise.resolve({ ok: true }),
|
|
13
|
+
),
|
|
12
14
|
sendSlackAgentSessionStatus: mock((..._args: unknown[]) => Promise.resolve()),
|
|
13
15
|
sendSlackAttachments: mock((..._args: unknown[]) =>
|
|
14
16
|
Promise.resolve({ allFailed: false, failureCount: 0 }),
|
|
@@ -22,6 +24,9 @@ const slack = {
|
|
|
22
24
|
};
|
|
23
25
|
const telegram = {
|
|
24
26
|
editTelegramMessage: mock((..._args: unknown[]) => Promise.resolve()),
|
|
27
|
+
sendTelegramReaction: mock((..._args: unknown[]) =>
|
|
28
|
+
Promise.resolve({ ok: true }),
|
|
29
|
+
),
|
|
25
30
|
sendTelegramReply: mock((..._args: unknown[]) => Promise.resolve()),
|
|
26
31
|
sendTelegramRichReply: mock((..._args: unknown[]) => Promise.resolve()),
|
|
27
32
|
sendTelegramTypingIndicator: mock((..._args: unknown[]) => Promise.resolve()),
|
|
@@ -46,6 +51,9 @@ const discord = {
|
|
|
46
51
|
Promise.resolve(true),
|
|
47
52
|
),
|
|
48
53
|
editDiscordMessage: mock((..._args: unknown[]) => Promise.resolve()),
|
|
54
|
+
sendDiscordReaction: mock((..._args: unknown[]) =>
|
|
55
|
+
Promise.resolve({ ok: true }),
|
|
56
|
+
),
|
|
49
57
|
sendDiscordAttachments: mock((..._args: unknown[]) =>
|
|
50
58
|
Promise.resolve({ allFailed: false, failureCount: 0, totalCount: 0 }),
|
|
51
59
|
),
|
|
@@ -63,9 +71,11 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
63
71
|
const {
|
|
64
72
|
deliverDirect,
|
|
65
73
|
editChannelMessage,
|
|
74
|
+
sendChannelReaction,
|
|
66
75
|
sendChannelStreamOp,
|
|
67
76
|
setChannelActivity,
|
|
68
77
|
supportsChannelActivity,
|
|
78
|
+
supportsChannelReaction,
|
|
69
79
|
isDirectDelivery,
|
|
70
80
|
getTransportForCallback,
|
|
71
81
|
} = await import("../index.js");
|
|
@@ -203,6 +213,108 @@ describe("Slack sub-operation selection", () => {
|
|
|
203
213
|
});
|
|
204
214
|
});
|
|
205
215
|
|
|
216
|
+
describe("react dispatch", () => {
|
|
217
|
+
test("sendChannelReaction routes to the Slack reaction sender", async () => {
|
|
218
|
+
const result = await sendChannelReaction("slack", {
|
|
219
|
+
chatId: "C1",
|
|
220
|
+
messageId: "123.456",
|
|
221
|
+
emoji: "thumbsup",
|
|
222
|
+
action: "add",
|
|
223
|
+
});
|
|
224
|
+
expect(result.ok).toBe(true);
|
|
225
|
+
expect(slack.sendSlackReaction).toHaveBeenCalledWith(
|
|
226
|
+
"C1",
|
|
227
|
+
"thumbsup",
|
|
228
|
+
"123.456",
|
|
229
|
+
"add",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("supportsChannelReaction follows the transport declaration", () => {
|
|
234
|
+
expect(supportsChannelReaction("slack")).toBe(true);
|
|
235
|
+
expect(supportsChannelReaction("telegram")).toBe(true);
|
|
236
|
+
expect(supportsChannelReaction("discord")).toBe(true);
|
|
237
|
+
expect(supportsChannelReaction("whatsapp")).toBe(false);
|
|
238
|
+
expect(supportsChannelReaction("some-plugin-channel")).toBe(false);
|
|
239
|
+
expect(supportsChannelReaction(undefined)).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("sendChannelReaction routes to the Telegram reaction sender", async () => {
|
|
243
|
+
const result = await sendChannelReaction("telegram", {
|
|
244
|
+
chatId: "12345",
|
|
245
|
+
messageId: "678",
|
|
246
|
+
emoji: "👍",
|
|
247
|
+
action: "add",
|
|
248
|
+
});
|
|
249
|
+
expect(result.ok).toBe(true);
|
|
250
|
+
expect(telegram.sendTelegramReaction).toHaveBeenCalledWith(
|
|
251
|
+
"12345",
|
|
252
|
+
"👍",
|
|
253
|
+
"678",
|
|
254
|
+
"add",
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("a Discord thread reaction addresses the thread channel, not the parent", async () => {
|
|
259
|
+
await sendChannelReaction("discord", {
|
|
260
|
+
chatId: "PARENT",
|
|
261
|
+
threadId: "THREAD",
|
|
262
|
+
messageId: "M1",
|
|
263
|
+
emoji: "👍",
|
|
264
|
+
action: "add",
|
|
265
|
+
});
|
|
266
|
+
expect(discord.sendDiscordReaction).toHaveBeenCalledWith(
|
|
267
|
+
"THREAD",
|
|
268
|
+
"👍",
|
|
269
|
+
"M1",
|
|
270
|
+
"add",
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("Slack ignores the thread coordinate: chatId plus ts addresses the message", async () => {
|
|
275
|
+
await sendChannelReaction("slack", {
|
|
276
|
+
chatId: "C1",
|
|
277
|
+
threadId: "1716000000.000001",
|
|
278
|
+
messageId: "1716000000.000002",
|
|
279
|
+
emoji: "thumbsup",
|
|
280
|
+
action: "add",
|
|
281
|
+
});
|
|
282
|
+
expect(slack.sendSlackReaction).toHaveBeenCalledWith(
|
|
283
|
+
"C1",
|
|
284
|
+
"thumbsup",
|
|
285
|
+
"1716000000.000002",
|
|
286
|
+
"add",
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("sendChannelReaction routes to the Discord reaction sender", async () => {
|
|
291
|
+
const result = await sendChannelReaction("discord", {
|
|
292
|
+
chatId: "C9",
|
|
293
|
+
messageId: "M9",
|
|
294
|
+
emoji: "<:vex:12345>",
|
|
295
|
+
action: "remove",
|
|
296
|
+
});
|
|
297
|
+
expect(result.ok).toBe(true);
|
|
298
|
+
expect(discord.sendDiscordReaction).toHaveBeenCalledWith(
|
|
299
|
+
"C9",
|
|
300
|
+
"<:vex:12345>",
|
|
301
|
+
"M9",
|
|
302
|
+
"remove",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("a channel without react resolves to nothing and sends nothing", async () => {
|
|
307
|
+
const result = await sendChannelReaction("whatsapp", {
|
|
308
|
+
chatId: "W1",
|
|
309
|
+
messageId: "m1",
|
|
310
|
+
emoji: "thumbsup",
|
|
311
|
+
action: "add",
|
|
312
|
+
});
|
|
313
|
+
expect(result).toEqual({ ok: true });
|
|
314
|
+
expect(whatsapp.sendWhatsAppReply).not.toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
206
318
|
describe("capability gating across channels", () => {
|
|
207
319
|
test("Slack renders a muted edit as its own context block", async () => {
|
|
208
320
|
await editChannelMessage(`${BASE}/deliver/slack`, {
|
|
@@ -71,6 +71,31 @@ export interface EditTarget {
|
|
|
71
71
|
readonly emphasis?: "muted";
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The message an emoji reaction lands on, and what to do there.
|
|
76
|
+
*
|
|
77
|
+
* `emoji` is spelled in the channel's own vocabulary (a Slack emoji name, a
|
|
78
|
+
* unicode emoji elsewhere): Vellum names the operation and the channel
|
|
79
|
+
* decides what its argument means, the same way `EditTarget.text` renders
|
|
80
|
+
* per channel.
|
|
81
|
+
*/
|
|
82
|
+
export interface ReactionTarget {
|
|
83
|
+
readonly chatId: string;
|
|
84
|
+
/** The target message, in the channel's own id space. */
|
|
85
|
+
readonly messageId: string;
|
|
86
|
+
/**
|
|
87
|
+
* Provider id of the thread the target message sits in, absent when it is
|
|
88
|
+
* not in one. Discord needs it: `chatId` is the delivery address (a thread
|
|
89
|
+
* message's parent channel), but a thread is its own channel and the
|
|
90
|
+
* reaction route must name the channel the message actually lives in.
|
|
91
|
+
* Slack and Telegram address reactions by `chatId` + `messageId` alone
|
|
92
|
+
* and ignore it.
|
|
93
|
+
*/
|
|
94
|
+
readonly threadId?: string;
|
|
95
|
+
readonly emoji: string;
|
|
96
|
+
readonly action: "add" | "remove";
|
|
97
|
+
}
|
|
98
|
+
|
|
74
99
|
/**
|
|
75
100
|
* Direct outbound delivery for one channel, wrapping the channel's provider-API
|
|
76
101
|
* send functions behind a uniform surface. Transports are registered statically
|
|
@@ -107,6 +132,18 @@ export interface ChannelTransport {
|
|
|
107
132
|
target: EditTarget,
|
|
108
133
|
): Promise<ChannelDeliveryResult>;
|
|
109
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Add or remove one of the assistant's own emoji reactions on a message.
|
|
137
|
+
*
|
|
138
|
+
* Takes no callback context: a reaction addresses its message absolutely
|
|
139
|
+
* (`chatId` + `messageId`) and carries no per-callback thread state, which
|
|
140
|
+
* is also what lets a producer with no callback URL, a tool running inside
|
|
141
|
+
* a turn, reach it. A channel whose platform has no reaction affordance
|
|
142
|
+
* omits the method, and the tool surface reads that omission as the
|
|
143
|
+
* capability's absence.
|
|
144
|
+
*/
|
|
145
|
+
react?(target: ReactionTarget): Promise<ChannelDeliveryResult>;
|
|
146
|
+
|
|
110
147
|
/**
|
|
111
148
|
* Show how busy the assistant is, in whatever form the channel has.
|
|
112
149
|
*
|
|
@@ -267,9 +267,9 @@ export function resetDiscordDmChannelCache(): void {
|
|
|
267
267
|
|
|
268
268
|
/** Call a Discord REST route with a JSON body. */
|
|
269
269
|
export async function callDiscordApi<T>(
|
|
270
|
-
method: "POST" | "PATCH",
|
|
270
|
+
method: "POST" | "PATCH" | "PUT" | "DELETE",
|
|
271
271
|
route: string,
|
|
272
|
-
body
|
|
272
|
+
body?: Record<string, unknown>,
|
|
273
273
|
): Promise<T | undefined> {
|
|
274
274
|
const botToken = await resolveBotToken();
|
|
275
275
|
return discordCall<T>(route, () =>
|
|
@@ -277,10 +277,10 @@ export async function callDiscordApi<T>(
|
|
|
277
277
|
method,
|
|
278
278
|
headers: {
|
|
279
279
|
Authorization: `Bot ${botToken}`,
|
|
280
|
-
"Content-Type": "application/json",
|
|
281
280
|
"User-Agent": DISCORD_USER_AGENT,
|
|
281
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
282
282
|
},
|
|
283
|
-
body: JSON.stringify(body),
|
|
283
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
284
284
|
signal: AbortSignal.timeout(DISCORD_DEFAULT_TIMEOUT_MS),
|
|
285
285
|
}),
|
|
286
286
|
);
|
|
@@ -30,8 +30,13 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
30
30
|
getLogger: () => ({ debug() {}, info() {}, warn() {}, error() {} }),
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
|
-
const {
|
|
34
|
-
|
|
33
|
+
const {
|
|
34
|
+
sendDiscordReply,
|
|
35
|
+
sendDiscordAttachments,
|
|
36
|
+
sendDiscordReaction,
|
|
37
|
+
editDiscordMessage,
|
|
38
|
+
DiscordPartialSendError,
|
|
39
|
+
} = await import("./send.js");
|
|
35
40
|
|
|
36
41
|
const originalFetch = globalThis.fetch;
|
|
37
42
|
|
|
@@ -71,6 +76,40 @@ afterEach(() => {
|
|
|
71
76
|
globalThis.fetch = originalFetch;
|
|
72
77
|
});
|
|
73
78
|
|
|
79
|
+
describe("sendDiscordReaction", () => {
|
|
80
|
+
test("adds via PUT on the @me reaction route, emoji URL-encoded", async () => {
|
|
81
|
+
stubFetch(204, null);
|
|
82
|
+
const result = await sendDiscordReaction("C1", "👍", "M1", "add");
|
|
83
|
+
expect(result).toEqual({ ok: true });
|
|
84
|
+
expect(calls[0].method).toBe("PUT");
|
|
85
|
+
expect(calls[0].url).toBe(
|
|
86
|
+
`https://discord.com/api/v10/channels/C1/messages/M1/reactions/${encodeURIComponent("👍")}/@me`,
|
|
87
|
+
);
|
|
88
|
+
expect(calls[0].body).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a custom emoji mention form travels as bare name:id", async () => {
|
|
92
|
+
stubFetch(204, null);
|
|
93
|
+
await sendDiscordReaction("C1", "<:vex:12345>", "M1", "add");
|
|
94
|
+
expect(calls[0].url).toContain(
|
|
95
|
+
`/reactions/${encodeURIComponent("vex:12345")}/@me`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("remove routes as DELETE", async () => {
|
|
100
|
+
stubFetch(204, null);
|
|
101
|
+
const result = await sendDiscordReaction("C1", "👍", "M1", "remove");
|
|
102
|
+
expect(result).toEqual({ ok: true });
|
|
103
|
+
expect(calls[0].method).toBe("DELETE");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a rejection reports ok false without throwing", async () => {
|
|
107
|
+
stubFetch(403, { message: "Missing Permissions", code: 50013 });
|
|
108
|
+
const result = await sendDiscordReaction("C1", "👍", "M1", "add");
|
|
109
|
+
expect(result).toEqual({ ok: false });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
74
113
|
describe("sendDiscordReply", () => {
|
|
75
114
|
test("posts to the channel's messages route with bot auth", async () => {
|
|
76
115
|
await sendDiscordReply({ channelId: "C1" }, "hello");
|
|
@@ -123,6 +162,136 @@ describe("sendDiscordReply", () => {
|
|
|
123
162
|
await sendDiscordReply({ channelId: "C1" }, " ");
|
|
124
163
|
expect(calls).toHaveLength(0);
|
|
125
164
|
});
|
|
165
|
+
|
|
166
|
+
test("an approval card carries its buttons on the shared apr: convention", async () => {
|
|
167
|
+
await sendDiscordReply({ channelId: "C1" }, "Approve?", {
|
|
168
|
+
requestId: "req-1",
|
|
169
|
+
actions: [
|
|
170
|
+
{ id: "approve_once", label: "Approve once" },
|
|
171
|
+
{ id: "reject", label: "Reject" },
|
|
172
|
+
],
|
|
173
|
+
plainTextFallback: "reply approve or reject",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const body = JSON.parse(calls[0].body as string);
|
|
177
|
+
expect(body.components).toEqual([
|
|
178
|
+
{
|
|
179
|
+
type: 1,
|
|
180
|
+
components: [
|
|
181
|
+
{
|
|
182
|
+
type: 2,
|
|
183
|
+
style: 1,
|
|
184
|
+
label: "Approve once",
|
|
185
|
+
custom_id: "apr:req-1:approve_once",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 2,
|
|
189
|
+
style: 4,
|
|
190
|
+
label: "Reject",
|
|
191
|
+
custom_id: "apr:req-1:reject",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("buttons ride only the final chunk of a long card", async () => {
|
|
199
|
+
const long = Array.from({ length: 400 }, (_, i) => `line ${i}`).join("\n");
|
|
200
|
+
await sendDiscordReply({ channelId: "C1" }, long, {
|
|
201
|
+
requestId: "req-1",
|
|
202
|
+
actions: [{ id: "approve_once", label: "Approve once" }],
|
|
203
|
+
plainTextFallback: "reply approve",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
207
|
+
const withComponents = calls.filter(
|
|
208
|
+
(c) => JSON.parse(c.body as string).components !== undefined,
|
|
209
|
+
);
|
|
210
|
+
// The final chunk's id is the one the delivery row records, so the
|
|
211
|
+
// message a press arrives on is the message the row can find.
|
|
212
|
+
expect(withComponents).toHaveLength(1);
|
|
213
|
+
expect(calls.indexOf(withComponents[0])).toBe(calls.length - 1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("an action's own emphasis outranks positional styling", async () => {
|
|
217
|
+
await sendDiscordReply({ channelId: "C1" }, "Meet?", {
|
|
218
|
+
requestId: "req-1",
|
|
219
|
+
actions: [
|
|
220
|
+
{ id: "accept", label: "Accept", emphasis: "secondary" },
|
|
221
|
+
{ id: "later", label: "Later" },
|
|
222
|
+
],
|
|
223
|
+
plainTextFallback: "reply accept or later",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const row = JSON.parse(calls[0].body as string).components[0];
|
|
227
|
+
expect(row.components[0].style).toBe(2);
|
|
228
|
+
expect(row.components[1].style).toBe(2);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("a failure after the first chunk names the undelivered remainder", async () => {
|
|
232
|
+
const long = Array.from({ length: 400 }, (_, i) => `line ${i}`).join("\n");
|
|
233
|
+
let posts = 0;
|
|
234
|
+
globalThis.fetch = (async (url: string | URL, init?: RequestInit) => {
|
|
235
|
+
posts += 1;
|
|
236
|
+
calls.push({
|
|
237
|
+
url: String(url),
|
|
238
|
+
method: init?.method ?? "GET",
|
|
239
|
+
headers: {},
|
|
240
|
+
body: init?.body,
|
|
241
|
+
});
|
|
242
|
+
if (posts > 1) {
|
|
243
|
+
return new Response(JSON.stringify({ message: "rejected" }), {
|
|
244
|
+
status: 400,
|
|
245
|
+
headers: { "content-type": "application/json" },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return new Response(JSON.stringify({ id: "msg-1" }), {
|
|
249
|
+
status: 200,
|
|
250
|
+
headers: { "content-type": "application/json" },
|
|
251
|
+
});
|
|
252
|
+
}) as unknown as typeof fetch;
|
|
253
|
+
|
|
254
|
+
let thrown: unknown;
|
|
255
|
+
try {
|
|
256
|
+
await sendDiscordReply({ channelId: "C1" }, long);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
thrown = err;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
expect(thrown).toBeInstanceOf(DiscordPartialSendError);
|
|
262
|
+
const partial = thrown as InstanceType<typeof DiscordPartialSendError>;
|
|
263
|
+
expect(partial.chunksSent).toBe(1);
|
|
264
|
+
expect(partial.lastMessageId).toBe("msg-1");
|
|
265
|
+
// The delivered prefix plus the named remainder reassemble the text, so
|
|
266
|
+
// a caller completing the card sends no line twice and drops none.
|
|
267
|
+
const delivered = JSON.parse(calls[0].body as string).content;
|
|
268
|
+
expect(`${delivered}\n${partial.remainingText}`).toBe(long);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("a failure on the first chunk propagates plainly, so full fallback is safe", async () => {
|
|
272
|
+
stubFetch(400, { message: "rejected" });
|
|
273
|
+
|
|
274
|
+
let thrown: unknown;
|
|
275
|
+
try {
|
|
276
|
+
await sendDiscordReply({ channelId: "C1" }, "short card");
|
|
277
|
+
} catch (err) {
|
|
278
|
+
thrown = err;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
expect(thrown).toBeInstanceOf(Error);
|
|
282
|
+
expect(thrown).not.toBeInstanceOf(DiscordPartialSendError);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a custom_id past Discord's cap throws instead of sending a dead button", async () => {
|
|
286
|
+
await expect(
|
|
287
|
+
sendDiscordReply({ channelId: "C1" }, "Approve?", {
|
|
288
|
+
requestId: "r".repeat(120),
|
|
289
|
+
actions: [{ id: "approve_once", label: "Approve once" }],
|
|
290
|
+
plainTextFallback: "reply approve",
|
|
291
|
+
}),
|
|
292
|
+
).rejects.toThrow("100-character limit");
|
|
293
|
+
expect(calls).toHaveLength(0);
|
|
294
|
+
});
|
|
126
295
|
});
|
|
127
296
|
|
|
128
297
|
describe("sendDiscordAttachments", () => {
|
|
@@ -183,6 +352,12 @@ describe("editDiscordMessage", () => {
|
|
|
183
352
|
const bodyOf = (index = 0): Record<string, unknown> =>
|
|
184
353
|
JSON.parse(String(calls[index]?.body)) as Record<string, unknown>;
|
|
185
354
|
|
|
355
|
+
test("an edit always strips components, so a settled card keeps no live buttons", async () => {
|
|
356
|
+
await editDiscordMessage({ channelId: "C1" }, "m1", "Approved");
|
|
357
|
+
const body = JSON.parse(calls[0].body as string);
|
|
358
|
+
expect(body.components).toEqual([]);
|
|
359
|
+
});
|
|
360
|
+
|
|
186
361
|
test("patches the message rather than posting a new one", async () => {
|
|
187
362
|
await editDiscordMessage({ channelId: "C1" }, "M9", "revised");
|
|
188
363
|
|