@vellumai/assistant 0.11.8 → 0.11.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -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/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -112,8 +112,8 @@ import {
|
|
|
112
112
|
type VoiceEndpointSource,
|
|
113
113
|
} from "./live-voice-metrics.js";
|
|
114
114
|
import {
|
|
115
|
+
persistAmbientSightFrame,
|
|
115
116
|
persistLiveVoicePhoto,
|
|
116
|
-
persistLiveVoiceSightFrame,
|
|
117
117
|
} from "./live-voice-photo.js";
|
|
118
118
|
import {
|
|
119
119
|
type LiveVoiceSession as LiveVoiceSessionContract,
|
|
@@ -1694,9 +1694,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1694
1694
|
* same fact.
|
|
1695
1695
|
*/
|
|
1696
1696
|
private persistSightFrame(frame: LiveVoiceClientSightFrameFrame): void {
|
|
1697
|
-
void
|
|
1697
|
+
void persistAmbientSightFrame(
|
|
1698
1698
|
this.conversationId,
|
|
1699
1699
|
frame.attachmentId,
|
|
1700
|
+
"voice",
|
|
1700
1701
|
).then((result) => {
|
|
1701
1702
|
if (!result.ok && !this.isClosed) {
|
|
1702
1703
|
void this.sendFrame({
|
|
@@ -1707,6 +1708,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1707
1708
|
// preview it already showed, rather than filing this with the
|
|
1708
1709
|
// transient transcriber and TTS blips that share `recoverable`.
|
|
1709
1710
|
frameType: "sight_frame",
|
|
1711
|
+
// Which keep, not just which stream: several can be outstanding
|
|
1712
|
+
// while a persist waits out a turn.
|
|
1713
|
+
attachmentId: frame.attachmentId,
|
|
1710
1714
|
// The session is fine; only this frame failed.
|
|
1711
1715
|
recoverable: true,
|
|
1712
1716
|
});
|
|
@@ -272,7 +272,9 @@ export interface LiveVoiceClientAttachFrameFrame {
|
|
|
272
272
|
* "sight_frame"` and `recoverable: true`: the session is fine and only this
|
|
273
273
|
* frame failed. Attributing it is what lets the client retract the preview it
|
|
274
274
|
* already showed instead of filing the error with the transient transcriber
|
|
275
|
-
* and TTS blips that share `recoverable`.
|
|
275
|
+
* and TTS blips that share `recoverable`. The refusal echoes the frame's
|
|
276
|
+
* `attachmentId` too, because keeps overlap: see that field on
|
|
277
|
+
* {@link LiveVoiceErrorServerFrame}.
|
|
276
278
|
*/
|
|
277
279
|
export interface LiveVoiceClientSightFrameFrame {
|
|
278
280
|
readonly type: "sight_frame";
|
|
@@ -602,6 +604,23 @@ export interface LiveVoiceErrorServerFrame extends LiveVoiceServerFrameBase {
|
|
|
602
604
|
* from older daemons) means the error is terminal for the session.
|
|
603
605
|
*/
|
|
604
606
|
readonly recoverable?: boolean;
|
|
607
|
+
/**
|
|
608
|
+
* The attachment the refused frame named, present on `sight_frame`
|
|
609
|
+
* rejections so the client can retire the exact keep that failed.
|
|
610
|
+
*
|
|
611
|
+
* `frameType` alone only narrows a rejection to the keep stream, and that
|
|
612
|
+
* stream is the one place several sends are routinely outstanding at once:
|
|
613
|
+
* the camera keeps shooting while a persist waits out a running turn, so a
|
|
614
|
+
* client holding three unacknowledged keeps cannot tell which of them this
|
|
615
|
+
* error is about. Naming the id is what turns "a frame failed" into "this
|
|
616
|
+
* preview comes down".
|
|
617
|
+
*
|
|
618
|
+
* Optional, and no other error path populates it: a parse failure has no
|
|
619
|
+
* id to name, and the other frames carrying an attachment have at most one
|
|
620
|
+
* in flight, which `frameType` already identifies. Clients must tolerate
|
|
621
|
+
* an absent id and must not gate any behavior on its presence.
|
|
622
|
+
*/
|
|
623
|
+
readonly attachmentId?: string;
|
|
605
624
|
}
|
|
606
625
|
|
|
607
626
|
export type LiveVoiceServerFrame =
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReactionEmojiFieldsSchema } from "@vellumai/service-contracts/reactions";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
import { CHANNEL_IDS } from "../channels/types.js";
|
|
@@ -40,7 +41,21 @@ const providerReactionMetadataSchema = z.object({
|
|
|
40
41
|
* namespace as `messageId`. Resolution is keyed on it, so it is required.
|
|
41
42
|
*/
|
|
42
43
|
targetMessageId: z.string(),
|
|
44
|
+
/**
|
|
45
|
+
* The emoji in the channel's own spelling. Kept because the channel's
|
|
46
|
+
* write path consumes it and the model hands it back verbatim; the typed
|
|
47
|
+
* fields beside it are what a reader consults.
|
|
48
|
+
*/
|
|
43
49
|
emoji: z.string(),
|
|
50
|
+
/**
|
|
51
|
+
* Which namespace the emoji was drawn from, as the channel said it. Spread
|
|
52
|
+
* from the shared group rather than restated, and declared rather than
|
|
53
|
+
* left to passthrough: the outer envelope passes unknown keys through, but
|
|
54
|
+
* this nested object strips them, so an undeclared field writes fine and
|
|
55
|
+
* reads back as nothing with no error at either end. A row carrying only
|
|
56
|
+
* the spelling has its kind recovered by `resolveInboundReactionPayload`.
|
|
57
|
+
*/
|
|
58
|
+
...ReactionEmojiFieldsSchema.shape,
|
|
44
59
|
op: z.enum(["added", "removed"]),
|
|
45
60
|
actorDisplayName: z.string().optional(),
|
|
46
61
|
});
|
|
@@ -65,7 +80,7 @@ export const providerMessageMetadataSchema = z
|
|
|
65
80
|
* Provider ids of the further posts this row's delivery produced beyond
|
|
66
81
|
* `messageId`: one stored reply split at tool boundaries or length
|
|
67
82
|
* limits posts several provider messages, and they all belong to this
|
|
68
|
-
* one row. A reaction naming any of them resolves here.
|
|
83
|
+
* one row. A reaction or delete naming any of them resolves here.
|
|
69
84
|
*/
|
|
70
85
|
additionalMessageIds: z.array(z.string()).optional(),
|
|
71
86
|
/**
|
|
@@ -89,7 +104,19 @@ export const providerMessageMetadataSchema = z
|
|
|
89
104
|
eventKind: z.enum(["message", "reaction"]),
|
|
90
105
|
reaction: providerReactionMetadataSchema.optional(),
|
|
91
106
|
editedAt: z.number().optional(),
|
|
107
|
+
/**
|
|
108
|
+
* The row is no longer visible on the channel: every provider post it
|
|
109
|
+
* produced has been deleted. Readers treat this as the whole row's
|
|
110
|
+
* deletion mark, as before rows could name several posts.
|
|
111
|
+
*/
|
|
92
112
|
deletedAt: z.number().optional(),
|
|
113
|
+
/**
|
|
114
|
+
* Provider ids among `messageId`/`additionalMessageIds` whose posts have
|
|
115
|
+
* been deleted on the channel. A split reply can lose one post while the
|
|
116
|
+
* others stay visible, so deletion is tracked per id; `deletedAt` is
|
|
117
|
+
* stamped only once every id is here.
|
|
118
|
+
*/
|
|
119
|
+
deletedMessageIds: z.array(z.string()).optional(),
|
|
93
120
|
})
|
|
94
121
|
.passthrough();
|
|
95
122
|
|
|
@@ -100,6 +127,18 @@ export type ProviderMessageMetadata = z.infer<
|
|
|
100
127
|
typeof providerMessageMetadataSchema
|
|
101
128
|
>;
|
|
102
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Whether every post a row names is among its deleted ones, which is exactly
|
|
132
|
+
* when the row-level `deletedAt` is warranted. A row naming no post is a
|
|
133
|
+
* single post by construction, so an empty list reads as fully deleted.
|
|
134
|
+
*/
|
|
135
|
+
export function everyPostDeleted(
|
|
136
|
+
postIds: readonly string[],
|
|
137
|
+
deletedIds: readonly string[] | undefined,
|
|
138
|
+
): boolean {
|
|
139
|
+
return postIds.every((id) => deletedIds?.includes(id) === true);
|
|
140
|
+
}
|
|
141
|
+
|
|
103
142
|
/**
|
|
104
143
|
* Parse and validate a serialized `ProviderMessageMetadata`, the counterpart of
|
|
105
144
|
* `readSlackMetadata` for the neutral shape. Anything that does not parse or
|
|
@@ -23,6 +23,10 @@ const slack = {
|
|
|
23
23
|
),
|
|
24
24
|
};
|
|
25
25
|
const telegram = {
|
|
26
|
+
// Spread the real module so a stub listing only what today's tests touch
|
|
27
|
+
// cannot break the next import a transport adds; the mocks below then
|
|
28
|
+
// override exactly the calls this suite asserts on.
|
|
29
|
+
...(await import("../telegram-bot/send.js")),
|
|
26
30
|
editTelegramMessage: mock((..._args: unknown[]) => Promise.resolve()),
|
|
27
31
|
sendTelegramReaction: mock((..._args: unknown[]) =>
|
|
28
32
|
Promise.resolve({ ok: true }),
|
|
@@ -90,8 +94,12 @@ function payload(
|
|
|
90
94
|
|
|
91
95
|
beforeEach(() => {
|
|
92
96
|
for (const group of [slack, telegram, whatsapp, a2a, discord]) {
|
|
93
|
-
for (const
|
|
94
|
-
|
|
97
|
+
for (const value of Object.values(group)) {
|
|
98
|
+
// A group spreads its real module so a stub cannot fall behind the
|
|
99
|
+
// exports a transport imports, which means it also carries constants
|
|
100
|
+
// and untouched functions. Only the spies have anything to clear.
|
|
101
|
+
const spy = value as { mockClear?: () => void };
|
|
102
|
+
spy.mockClear?.();
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
});
|
|
@@ -189,4 +189,32 @@ export interface ChannelTransport {
|
|
|
189
189
|
chatId: string,
|
|
190
190
|
op: StreamOp,
|
|
191
191
|
): Promise<ChannelDeliveryResult>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The most text one stream operation may carry, for a channel that caps it.
|
|
195
|
+
*
|
|
196
|
+
* Declared rather than applied here because the caller is what knows how
|
|
197
|
+
* much of the reply a channel has actually accepted: it must advance that
|
|
198
|
+
* mark once per operation the channel confirms. A transport that split a
|
|
199
|
+
* wide delta into several calls of its own would leave the caller unable to
|
|
200
|
+
* tell a partial delivery from a whole one, and a retry would then re-send
|
|
201
|
+
* the part that already landed. Omitted by a channel with no cap.
|
|
202
|
+
*/
|
|
203
|
+
readonly maxStreamTextChars?: number;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether what `streamReply` leaves behind is the reply itself.
|
|
207
|
+
*
|
|
208
|
+
* True for a channel that finalizes the streamed message in place, so the
|
|
209
|
+
* reply is already delivered once the stream ends and durable delivery must
|
|
210
|
+
* not send it again. Omitted by a channel whose stream is only a preview:
|
|
211
|
+
* the draft evaporates and the reply is still owed, so durable delivery
|
|
212
|
+
* sends it as it would for a channel that never streamed.
|
|
213
|
+
*
|
|
214
|
+
* Omission is the safe default on purpose. A channel that forgets to
|
|
215
|
+
* declare it posts the reply through the ordinary path, which at worst
|
|
216
|
+
* repeats what a persisting stream already showed; the opposite mistake
|
|
217
|
+
* loses the reply entirely.
|
|
218
|
+
*/
|
|
219
|
+
readonly streamPersists?: boolean;
|
|
192
220
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ApprovalUIMetadata,
|
|
10
10
|
ChannelDeliveryResult,
|
|
11
11
|
} from "@vellumai/gateway-client";
|
|
12
|
+
import { parseDiscordEmojiMention } from "@vellumai/gateway-client";
|
|
12
13
|
|
|
13
14
|
import { getAttachmentContent } from "../../../persistence/attachments-store.js";
|
|
14
15
|
import type { RuntimeAttachmentMetadata } from "../../../runtime/http-types.js";
|
|
@@ -424,8 +425,8 @@ export async function sendDiscordAttachments(
|
|
|
424
425
|
* as itself.
|
|
425
426
|
*/
|
|
426
427
|
function discordReactionPathEmoji(emoji: string): string {
|
|
427
|
-
const custom =
|
|
428
|
-
return encodeURIComponent(custom ? `${custom
|
|
428
|
+
const custom = parseDiscordEmojiMention(emoji);
|
|
429
|
+
return encodeURIComponent(custom ? `${custom.name}:${custom.id}` : emoji);
|
|
429
430
|
}
|
|
430
431
|
|
|
431
432
|
/**
|
|
@@ -30,7 +30,17 @@ import {
|
|
|
30
30
|
type SlackRequestOptions,
|
|
31
31
|
} from "./web-api-transport.js";
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Envelope fields the outbound surfaces read off successful responses.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately no `message_ts`: that is what `chat.postEphemeral` returns
|
|
37
|
+
* instead of `ts`, and Slack documents it as not addressing a persisted
|
|
38
|
+
* message (`chat.update` rejects it). So an ephemeral reply has no id worth
|
|
39
|
+
* recording, and the absent `ts` correctly leaves its row unstamped rather
|
|
40
|
+
* than carrying a `channelTs` whose permalink resolves to nothing.
|
|
41
|
+
*
|
|
42
|
+
* @see https://docs.slack.dev/reference/methods/chat.postEphemeral/
|
|
43
|
+
*/
|
|
34
44
|
interface SlackOutboundApiResponse extends SlackApiResponse {
|
|
35
45
|
ts?: string;
|
|
36
46
|
upload_url?: string;
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
buildSlackTimezoneMetadata,
|
|
5
5
|
formatSlackTimezoneLabel,
|
|
6
6
|
mergeSlackMetadata,
|
|
7
|
+
providerMetadataOfPreSendSlackEnvelope,
|
|
7
8
|
readSlackMetadata,
|
|
8
9
|
readSlackMetadataFromMessageMetadata,
|
|
9
10
|
type SlackMessageMetadata,
|
|
11
|
+
slackViewOfProviderMetadata,
|
|
10
12
|
writeSlackMetadata,
|
|
11
13
|
} from "./message-metadata.js";
|
|
12
14
|
|
|
@@ -394,3 +396,134 @@ describe("mergeSlackMetadata", () => {
|
|
|
394
396
|
expect(parsed?.displayName).toBe("Bob");
|
|
395
397
|
});
|
|
396
398
|
});
|
|
399
|
+
|
|
400
|
+
describe("providerMetadataOfPreSendSlackEnvelope", () => {
|
|
401
|
+
test("reads a pre-send Slack envelope as the neutral shape, Slack's own fields riding along", () => {
|
|
402
|
+
expect(
|
|
403
|
+
providerMetadataOfPreSendSlackEnvelope({
|
|
404
|
+
slackMeta: JSON.stringify({
|
|
405
|
+
source: "slack",
|
|
406
|
+
eventKind: "message",
|
|
407
|
+
channelId: "C1",
|
|
408
|
+
threadTs: "1700000000.000001",
|
|
409
|
+
timestampTimezone: "America/New_York",
|
|
410
|
+
}),
|
|
411
|
+
}),
|
|
412
|
+
).toEqual({
|
|
413
|
+
source: "slack",
|
|
414
|
+
conversationExternalId: "C1",
|
|
415
|
+
eventKind: "message",
|
|
416
|
+
threadId: "1700000000.000001",
|
|
417
|
+
timestampTimezone: "America/New_York",
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("reads as null for a reconciled row, a non-Slack envelope, and a row without slackMeta", () => {
|
|
422
|
+
expect(
|
|
423
|
+
providerMetadataOfPreSendSlackEnvelope({
|
|
424
|
+
slackMeta: JSON.stringify({
|
|
425
|
+
source: "slack",
|
|
426
|
+
eventKind: "message",
|
|
427
|
+
channelId: "C1",
|
|
428
|
+
channelTs: "1700000000.000002",
|
|
429
|
+
}),
|
|
430
|
+
}),
|
|
431
|
+
).toBeNull();
|
|
432
|
+
expect(
|
|
433
|
+
providerMetadataOfPreSendSlackEnvelope({
|
|
434
|
+
slackMeta: JSON.stringify({ source: "telegram", channelId: "C1" }),
|
|
435
|
+
}),
|
|
436
|
+
).toBeNull();
|
|
437
|
+
expect(providerMetadataOfPreSendSlackEnvelope({})).toBeNull();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("slackViewOfProviderMetadata", () => {
|
|
442
|
+
test("maps a Slack reply row's neutral envelope onto the Slack view, extras included", () => {
|
|
443
|
+
const view = slackViewOfProviderMetadata({
|
|
444
|
+
source: "slack",
|
|
445
|
+
conversationExternalId: "C123",
|
|
446
|
+
messageId: "1700000000.000100",
|
|
447
|
+
threadId: "1700000000.000001",
|
|
448
|
+
eventKind: "message",
|
|
449
|
+
timestampTimezone: "America/New_York",
|
|
450
|
+
timestampTimezoneLabel: "ET",
|
|
451
|
+
slackFiles: [{ name: "report.pdf", mimetype: "application/pdf" }],
|
|
452
|
+
deletedAt: 1700000001000,
|
|
453
|
+
});
|
|
454
|
+
expect(view).toEqual({
|
|
455
|
+
source: "slack",
|
|
456
|
+
channelId: "C123",
|
|
457
|
+
channelTs: "1700000000.000100",
|
|
458
|
+
threadTs: "1700000000.000001",
|
|
459
|
+
eventKind: "message",
|
|
460
|
+
timestampTimezone: "America/New_York",
|
|
461
|
+
timestampTimezoneLabel: "ET",
|
|
462
|
+
slackFiles: [{ name: "report.pdf", mimetype: "application/pdf" }],
|
|
463
|
+
deletedAt: 1700000001000,
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("a reaction row's view stores the reacted message's ts as channelTs", () => {
|
|
468
|
+
const view = slackViewOfProviderMetadata({
|
|
469
|
+
source: "slack",
|
|
470
|
+
conversationExternalId: "C123",
|
|
471
|
+
eventKind: "reaction",
|
|
472
|
+
actorExternalId: "U1",
|
|
473
|
+
displayName: "Alice",
|
|
474
|
+
reaction: {
|
|
475
|
+
targetMessageId: "1700000000.000100",
|
|
476
|
+
emoji: "tada",
|
|
477
|
+
op: "added",
|
|
478
|
+
actorDisplayName: "Alice",
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
expect(view).toEqual({
|
|
482
|
+
source: "slack",
|
|
483
|
+
channelId: "C123",
|
|
484
|
+
channelTs: "1700000000.000100",
|
|
485
|
+
actorExternalUserId: "U1",
|
|
486
|
+
displayName: "Alice",
|
|
487
|
+
eventKind: "reaction",
|
|
488
|
+
reaction: {
|
|
489
|
+
emoji: "tada",
|
|
490
|
+
op: "added",
|
|
491
|
+
targetChannelTs: "1700000000.000100",
|
|
492
|
+
actorDisplayName: "Alice",
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("a row naming no post yet, or another channel's row, has no Slack view", () => {
|
|
498
|
+
expect(
|
|
499
|
+
slackViewOfProviderMetadata({
|
|
500
|
+
source: "slack",
|
|
501
|
+
conversationExternalId: "C123",
|
|
502
|
+
eventKind: "message",
|
|
503
|
+
}),
|
|
504
|
+
).toBeNull();
|
|
505
|
+
expect(
|
|
506
|
+
slackViewOfProviderMetadata({
|
|
507
|
+
source: "discord",
|
|
508
|
+
conversationExternalId: "999",
|
|
509
|
+
messageId: "1",
|
|
510
|
+
eventKind: "message",
|
|
511
|
+
}),
|
|
512
|
+
).toBeNull();
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("readSlackMetadataFromMessageMetadata serves the neutral envelope before the nested one", () => {
|
|
516
|
+
const metadata = JSON.stringify({
|
|
517
|
+
assistantMessageChannel: "slack",
|
|
518
|
+
providerMeta: JSON.stringify({
|
|
519
|
+
source: "slack",
|
|
520
|
+
conversationExternalId: "C123",
|
|
521
|
+
messageId: "1700000000.000100",
|
|
522
|
+
eventKind: "message",
|
|
523
|
+
}),
|
|
524
|
+
});
|
|
525
|
+
expect(readSlackMetadataFromMessageMetadata(metadata)?.channelTs).toBe(
|
|
526
|
+
"1700000000.000100",
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pickReactionEmojiFields,
|
|
3
|
+
ReactionEmojiFieldsSchema,
|
|
4
|
+
} from "@vellumai/service-contracts/reactions";
|
|
1
5
|
import { z } from "zod";
|
|
2
6
|
|
|
3
|
-
import
|
|
7
|
+
import { safeParseRecord } from "../../../util/json.js";
|
|
8
|
+
import {
|
|
9
|
+
type ProviderMessageMetadata,
|
|
10
|
+
readProviderMessageMetadata,
|
|
11
|
+
} from "../../provider-message-metadata.js";
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* Typed Slack message metadata stored flat in the `messages.metadata` column
|
|
@@ -39,6 +47,7 @@ export function isSlackDmConversation(conversationExternalId: string): boolean {
|
|
|
39
47
|
|
|
40
48
|
const slackReactionMetadataSchema = z.object({
|
|
41
49
|
emoji: z.string(),
|
|
50
|
+
...ReactionEmojiFieldsSchema.shape,
|
|
42
51
|
actorDisplayName: z.string().optional(),
|
|
43
52
|
targetChannelTs: z.string(),
|
|
44
53
|
op: z.enum(["added", "removed"]),
|
|
@@ -258,6 +267,13 @@ export function readSlackMetadata(
|
|
|
258
267
|
return result.success ? result.data : null;
|
|
259
268
|
}
|
|
260
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Read a row's Slack view from its stored metadata, whichever envelope the
|
|
272
|
+
* row carries: the neutral `providerMeta` (every row the daemon authors, and
|
|
273
|
+
* the end state for every Slack row) or the nested `slackMeta` that inbound
|
|
274
|
+
* Slack rows still write. `allowFlatLegacy` also accepts the pre-envelope
|
|
275
|
+
* flat form.
|
|
276
|
+
*/
|
|
261
277
|
export function readSlackMetadataFromMessageMetadata(
|
|
262
278
|
metadata: string | null | undefined,
|
|
263
279
|
opts?: { allowFlatLegacy?: boolean },
|
|
@@ -279,6 +295,18 @@ export function readSlackMetadataFromMessageMetadata(
|
|
|
279
295
|
return null;
|
|
280
296
|
}
|
|
281
297
|
|
|
298
|
+
// A row the daemon authors (an assistant reply, its own reaction, a
|
|
299
|
+
// bot-authored backfill row) describes itself in the neutral envelope;
|
|
300
|
+
// its Slack view is derived from that, so every Slack reader serves both
|
|
301
|
+
// envelopes without a branch of its own.
|
|
302
|
+
const neutral = readProviderMessageMetadata(parent.providerMeta);
|
|
303
|
+
if (neutral !== null) {
|
|
304
|
+
const view = slackViewOfProviderMetadata(neutral);
|
|
305
|
+
if (view !== null) {
|
|
306
|
+
return view;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
282
310
|
const nested = parent.slackMeta;
|
|
283
311
|
if (typeof nested === "string") {
|
|
284
312
|
const parsedNested = readSlackMetadata(nested);
|
|
@@ -290,6 +318,80 @@ export function readSlackMetadataFromMessageMetadata(
|
|
|
290
318
|
return opts?.allowFlatLegacy ? readSlackMetadata(metadata) : null;
|
|
291
319
|
}
|
|
292
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Slack's own fields, carried on the neutral envelope's passthrough by rows
|
|
323
|
+
* that write it. They have no neutral counterpart: the Slack transcript
|
|
324
|
+
* renderer is a provider renderer by design and reads them through the
|
|
325
|
+
* Slack view.
|
|
326
|
+
*/
|
|
327
|
+
const SLACK_ONLY_FIELDS = [
|
|
328
|
+
"channelName",
|
|
329
|
+
"timestampTimezone",
|
|
330
|
+
"timestampTimezoneLabel",
|
|
331
|
+
"speakerTimezoneLabel",
|
|
332
|
+
"actorTimezone",
|
|
333
|
+
"actorTimezoneLabel",
|
|
334
|
+
"actorTimezoneOffsetSeconds",
|
|
335
|
+
"slackFiles",
|
|
336
|
+
] as const;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The Slack view of a neutral envelope: the inverse of
|
|
340
|
+
* `slackMetadataAsProviderMetadata`, for the Slack renderers and the backfill
|
|
341
|
+
* readers. A reaction row's `channelTs` is the reacted message's ts, as the
|
|
342
|
+
* Slack envelope stores it. Null for a non-Slack envelope, and for one that
|
|
343
|
+
* names no post yet (a reply before its post-send reconciliation), which is
|
|
344
|
+
* also what the strict reader says of a pre-send Slack envelope.
|
|
345
|
+
*/
|
|
346
|
+
export function slackViewOfProviderMetadata(
|
|
347
|
+
meta: ProviderMessageMetadata,
|
|
348
|
+
): SlackMessageMetadata | null {
|
|
349
|
+
if (meta.source !== "slack") {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const channelTs =
|
|
353
|
+
meta.eventKind === "reaction"
|
|
354
|
+
? meta.reaction?.targetMessageId
|
|
355
|
+
: meta.messageId;
|
|
356
|
+
if (channelTs === undefined) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const candidate: Record<string, unknown> = {
|
|
360
|
+
source: "slack",
|
|
361
|
+
channelId: meta.conversationExternalId,
|
|
362
|
+
channelTs,
|
|
363
|
+
...(meta.threadId !== undefined ? { threadTs: meta.threadId } : {}),
|
|
364
|
+
...(meta.displayName !== undefined
|
|
365
|
+
? { displayName: meta.displayName }
|
|
366
|
+
: {}),
|
|
367
|
+
...(meta.actorExternalId !== undefined
|
|
368
|
+
? { actorExternalUserId: meta.actorExternalId }
|
|
369
|
+
: {}),
|
|
370
|
+
eventKind: meta.eventKind,
|
|
371
|
+
...(meta.reaction
|
|
372
|
+
? {
|
|
373
|
+
reaction: {
|
|
374
|
+
emoji: meta.reaction.emoji,
|
|
375
|
+
op: meta.reaction.op,
|
|
376
|
+
targetChannelTs: meta.reaction.targetMessageId,
|
|
377
|
+
...(meta.reaction.actorDisplayName
|
|
378
|
+
? { actorDisplayName: meta.reaction.actorDisplayName }
|
|
379
|
+
: {}),
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
: {}),
|
|
383
|
+
...(meta.editedAt !== undefined ? { editedAt: meta.editedAt } : {}),
|
|
384
|
+
...(meta.deletedAt !== undefined ? { deletedAt: meta.deletedAt } : {}),
|
|
385
|
+
};
|
|
386
|
+
for (const field of SLACK_ONLY_FIELDS) {
|
|
387
|
+
if (meta[field] !== undefined) {
|
|
388
|
+
candidate[field] = meta[field];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const result = slackMessageMetadataSchema.safeParse(candidate);
|
|
392
|
+
return result.success ? result.data : null;
|
|
393
|
+
}
|
|
394
|
+
|
|
293
395
|
/**
|
|
294
396
|
* Present a Slack row's stored metadata as the channel-neutral shape.
|
|
295
397
|
*
|
|
@@ -321,6 +423,7 @@ export function slackMetadataAsProviderMetadata(
|
|
|
321
423
|
reaction: {
|
|
322
424
|
targetMessageId: meta.reaction.targetChannelTs,
|
|
323
425
|
emoji: meta.reaction.emoji,
|
|
426
|
+
...pickReactionEmojiFields(meta.reaction),
|
|
324
427
|
op: meta.reaction.op,
|
|
325
428
|
...(meta.reaction.actorDisplayName
|
|
326
429
|
? { actorDisplayName: meta.reaction.actorDisplayName }
|
|
@@ -333,6 +436,43 @@ export function slackMetadataAsProviderMetadata(
|
|
|
333
436
|
};
|
|
334
437
|
}
|
|
335
438
|
|
|
439
|
+
/**
|
|
440
|
+
* The neutral envelope of a reply row reserved with Slack's own pre-send
|
|
441
|
+
* envelope: a `slackMeta` naming the channel and thread but no `channelTs`,
|
|
442
|
+
* the id the post-send reconciliation fills in.
|
|
443
|
+
*
|
|
444
|
+
* Transitional: a reply the daemon reserves carries the neutral envelope from
|
|
445
|
+
* the start, so this serves only a reply still pending for the retry sweep
|
|
446
|
+
* that a daemon reserving Slack replies under `slackMeta` left there; the
|
|
447
|
+
* reconciliation stamps it and converges the row onto the neutral envelope,
|
|
448
|
+
* Slack's own fields riding the schema's passthrough. Delete once no such
|
|
449
|
+
* row can be pending. A `slackMeta` that already names its post is a
|
|
450
|
+
* reconciled row and reads as null here; `readSlackMetadata` serves it.
|
|
451
|
+
*/
|
|
452
|
+
export function providerMetadataOfPreSendSlackEnvelope(
|
|
453
|
+
envelope: Record<string, unknown>,
|
|
454
|
+
): ProviderMessageMetadata | null {
|
|
455
|
+
if (typeof envelope.slackMeta !== "string") {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const { source, channelId, channelTs, threadTs, ...slackFields } =
|
|
459
|
+
safeParseRecord(envelope.slackMeta);
|
|
460
|
+
if (
|
|
461
|
+
source !== "slack" ||
|
|
462
|
+
typeof channelId !== "string" ||
|
|
463
|
+
channelTs !== undefined
|
|
464
|
+
) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
...slackFields,
|
|
469
|
+
source: "slack",
|
|
470
|
+
conversationExternalId: channelId,
|
|
471
|
+
eventKind: "message",
|
|
472
|
+
...(typeof threadTs === "string" ? { threadId: threadTs } : {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
336
476
|
/**
|
|
337
477
|
* Serialize `SlackMessageMetadata` to a JSON string suitable for a fresh
|
|
338
478
|
* write to the `messages.metadata` column. Use `mergeSlackMetadata` when an
|
|
@@ -10,7 +10,13 @@ type CallSlackApi = typeof import("./api.js").callSlackApi;
|
|
|
10
10
|
|
|
11
11
|
const callSlackApiMock = mock<CallSlackApi>(async () => ({ ok: true }));
|
|
12
12
|
|
|
13
|
+
// Spread the real module so a factory listing only what today's tests touch
|
|
14
|
+
// cannot break the next import send.ts adds; the stubs below then override
|
|
15
|
+
// exactly the calls this suite asserts on.
|
|
16
|
+
const actualSlackApi = await import("./api.js");
|
|
17
|
+
|
|
13
18
|
mock.module("./api.js", () => ({
|
|
19
|
+
...actualSlackApi,
|
|
14
20
|
callSlackApi: (method: string, body: Record<string, unknown>) =>
|
|
15
21
|
callSlackApiMock(method, body),
|
|
16
22
|
callSlackApiForm: async () => ({}),
|
|
@@ -32,8 +38,10 @@ const {
|
|
|
32
38
|
sendSlackAgentSessionStatus,
|
|
33
39
|
sendSlackReaction,
|
|
34
40
|
sendSlackReply,
|
|
41
|
+
sendSlackStreamOp,
|
|
35
42
|
updateSlackMessage,
|
|
36
43
|
} = await import("./send.js");
|
|
44
|
+
const { SLACK_STREAM_MARKDOWN_LIMIT } = await import("./api.js");
|
|
37
45
|
|
|
38
46
|
describe("sendSlackAgentSessionStatus", () => {
|
|
39
47
|
const threadTs = "1700000000.000100";
|
|
@@ -442,3 +450,53 @@ describe("sendSlackReply approval fallback", () => {
|
|
|
442
450
|
expect(callSlackApiMock).toHaveBeenCalledTimes(1);
|
|
443
451
|
});
|
|
444
452
|
});
|
|
453
|
+
|
|
454
|
+
describe("sendSlackStreamOp", () => {
|
|
455
|
+
const streamTs = "1700000000.000900";
|
|
456
|
+
|
|
457
|
+
beforeEach(() => {
|
|
458
|
+
callSlackApiMock.mockReset();
|
|
459
|
+
callSlackApiMock.mockImplementation(async () => ({ ok: true }));
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("sends one call per operation, whatever the caller hands over", async () => {
|
|
463
|
+
// Splitting to fit the cap belongs to the caller, which is what tracks how
|
|
464
|
+
// much of the reply Slack has taken. This layer performs the operation it
|
|
465
|
+
// is given, once.
|
|
466
|
+
const appended = "x".repeat(SLACK_STREAM_MARKDOWN_LIMIT);
|
|
467
|
+
await sendSlackStreamOp("C-STREAM", {
|
|
468
|
+
action: "append",
|
|
469
|
+
streamId: streamTs,
|
|
470
|
+
text: appended,
|
|
471
|
+
appended,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const calls = callSlackApiMock.mock.calls.filter(
|
|
475
|
+
(call) => call[0] === "chat.appendStream",
|
|
476
|
+
);
|
|
477
|
+
expect(calls).toHaveLength(1);
|
|
478
|
+
expect((calls[0]![1] as { markdownText?: string }).markdownText).toBe(
|
|
479
|
+
appended,
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("a plan that moved with no new words still reaches the message", async () => {
|
|
484
|
+
// `chat.appendStream` documents "One of markdown_text or chunks is
|
|
485
|
+
// required", so a plan-only call is legal and is what ticks the plan block
|
|
486
|
+
// during silent work.
|
|
487
|
+
await sendSlackStreamOp("C-STREAM", {
|
|
488
|
+
action: "append",
|
|
489
|
+
streamId: streamTs,
|
|
490
|
+
text: "unchanged",
|
|
491
|
+
plan: { steps: [{ label: "Step", status: "completed" }] },
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const calls = callSlackApiMock.mock.calls.filter(
|
|
495
|
+
(call) => call[0] === "chat.appendStream",
|
|
496
|
+
);
|
|
497
|
+
expect(calls).toHaveLength(1);
|
|
498
|
+
const body = calls[0]![1] as { markdownText?: string; tasks?: unknown };
|
|
499
|
+
expect(body.markdownText).toBeUndefined();
|
|
500
|
+
expect(body.tasks).toBeDefined();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
@@ -331,10 +331,10 @@ export async function sendSlackStreamOp(
|
|
|
331
331
|
channel,
|
|
332
332
|
threadTs: op.anchorMessageId,
|
|
333
333
|
markdownText: op.appended ?? op.text,
|
|
334
|
-
// Fixed for the stream's lifetime at start,
|
|
335
|
-
//
|
|
336
|
-
// how task chunks render, so a
|
|
337
|
-
// reads as a plain message.
|
|
334
|
+
// Fixed for the stream's lifetime at start, so it is set
|
|
335
|
+
// unconditionally: a plan that first appears on a later append still
|
|
336
|
+
// renders as a plan. It only affects how task chunks render, so a
|
|
337
|
+
// stream that never carries one still reads as a plain message.
|
|
338
338
|
taskDisplayMode: "plan",
|
|
339
339
|
planTitle,
|
|
340
340
|
tasks,
|