@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
|
@@ -46,6 +46,7 @@ import { classifyDiskPressureTurnPolicy } from "../../daemon/disk-pressure-polic
|
|
|
46
46
|
import { processMessage } from "../../daemon/process-message.js";
|
|
47
47
|
import type { TrustContext } from "../../daemon/trust-context-types.js";
|
|
48
48
|
import { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
|
|
49
|
+
import type { ProviderMessageMetadata } from "../../messaging/provider-message-metadata.js";
|
|
49
50
|
import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
|
|
50
51
|
import { editChannelMessage } from "../../messaging/providers/index.js";
|
|
51
52
|
import {
|
|
@@ -68,7 +69,10 @@ import {
|
|
|
68
69
|
type SlackMessageMetadata,
|
|
69
70
|
writeSlackMetadata,
|
|
70
71
|
} from "../../messaging/providers/slack/message-metadata.js";
|
|
71
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
mergeProviderMessageMetadata,
|
|
74
|
+
readProviderMetadata,
|
|
75
|
+
} from "../../messaging/read-provider-metadata.js";
|
|
72
76
|
import { MESSAGE_PREVIEW_MAX_LENGTH } from "../../notifications/notification-utils.js";
|
|
73
77
|
import {
|
|
74
78
|
attachInlineAttachmentToMessage,
|
|
@@ -91,8 +95,11 @@ import {
|
|
|
91
95
|
import { applyDeterministicTitleIfReplaceable } from "../../persistence/conversation-title-service.js";
|
|
92
96
|
import {
|
|
93
97
|
clearPayload,
|
|
98
|
+
DELETE_PROVIDER_MESSAGE_ID_MAX_SCAN,
|
|
99
|
+
findMessageByProviderMessageId,
|
|
94
100
|
findMessageBySourceId,
|
|
95
101
|
hasInboundEventForSource,
|
|
102
|
+
hasUnreconciledOutboundRow,
|
|
96
103
|
recordInbound,
|
|
97
104
|
} from "../../persistence/delivery-crud.js";
|
|
98
105
|
import { markProcessed } from "../../persistence/delivery-status.js";
|
|
@@ -110,6 +117,7 @@ import {
|
|
|
110
117
|
} from "../access-request-helper.js";
|
|
111
118
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
112
119
|
import { deliverChannelReply } from "../gateway-client.js";
|
|
120
|
+
import { publishConversationMessagesChanged } from "../sync/resource-sync-events.js";
|
|
113
121
|
import { trustContextFromVerdict } from "../trust-verdict-consumer.js";
|
|
114
122
|
import { BadRequestError } from "./errors.js";
|
|
115
123
|
import { handleApprovalInterception } from "./guardian-approval-interception.js";
|
|
@@ -149,6 +157,13 @@ const DISK_PRESSURE_REMOTE_BLOCK_REPLY =
|
|
|
149
157
|
// EDIT_LOOKUP_RETRIES / EDIT_LOOKUP_DELAY_MS constants.
|
|
150
158
|
let deleteLookupRetries = 5;
|
|
151
159
|
let deleteLookupDelayMs = 2000;
|
|
160
|
+
/**
|
|
161
|
+
* Attempts during which a delete with no inbound event keeps retrying the
|
|
162
|
+
* envelope lane: an assistant post's provider id lands with the post-send
|
|
163
|
+
* reconciliation, one delivery round-trip behind the post itself, so a
|
|
164
|
+
* couple of backoff steps bridge an automated deletion that outruns it.
|
|
165
|
+
*/
|
|
166
|
+
const deleteOutboundReconcileRetries = 2;
|
|
152
167
|
|
|
153
168
|
interface SlackActorTimezoneMetadata {
|
|
154
169
|
timezone?: string;
|
|
@@ -434,13 +449,13 @@ export async function handleChannelInbound({
|
|
|
434
449
|
// pipeline (ACL, admission floor, disk-pressure, conversation binding) so a
|
|
435
450
|
// 👍 never triggers a verification handshake or an access-request
|
|
436
451
|
// notification, and a stranger's reaction creates no conversation/binding.
|
|
437
|
-
// The interceptor drops strangers
|
|
438
|
-
// transcript
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
452
|
+
// The interceptor drops strangers and records an admitted actor's reaction
|
|
453
|
+
// as a transcript signal in the conversation of the reacted message.
|
|
454
|
+
// Reactions never mint a conversation; the one that wakes a turn (a
|
|
455
|
+
// reaction on the assistant's own post) is the interceptor's call. A family member whose payload does not
|
|
456
|
+
// resolve (no emoji or no target message id) is dropped as noise here:
|
|
457
|
+
// the kind names the family, so it must never fall through and be read
|
|
458
|
+
// as a message.
|
|
444
459
|
if (isReactionEvent(body)) {
|
|
445
460
|
const reaction = resolveInboundReactionPayload(body);
|
|
446
461
|
if (!reaction) {
|
|
@@ -462,7 +477,6 @@ export async function handleChannelInbound({
|
|
|
462
477
|
actorUsername: body.actorUsername,
|
|
463
478
|
replyCallbackUrl: body.replyCallbackUrl,
|
|
464
479
|
sourceMetadata: body.sourceMetadata,
|
|
465
|
-
approvalConversationGenerator,
|
|
466
480
|
});
|
|
467
481
|
}
|
|
468
482
|
|
|
@@ -527,10 +541,11 @@ export async function handleChannelInbound({
|
|
|
527
541
|
// A delete names the original via `sourceMetadata.messageId` and
|
|
528
542
|
// short-circuits the rest of the pipeline: no agent loop, no approval
|
|
529
543
|
// routing. The stored row keeps its content for audit; rendering elides on
|
|
530
|
-
// the deletedAt marker. An attributed delete (the wire names
|
|
531
|
-
// message
|
|
532
|
-
// unattributed one bypassed it under the
|
|
533
|
-
// only to a row this daemon
|
|
544
|
+
// the deletedAt marker. An attributed delete (the wire names a human
|
|
545
|
+
// author for the deleted message, as Slack's does for a member's post)
|
|
546
|
+
// passed the ingress ACL above; an unattributed one bypassed it under the
|
|
547
|
+
// ACL's stated contract and applies only to a row this daemon already
|
|
548
|
+
// holds for the chat.
|
|
534
549
|
if (eventKind === "delete") {
|
|
535
550
|
const deletedMessageTs =
|
|
536
551
|
typeof sourceMetadata?.messageId === "string"
|
|
@@ -556,20 +571,38 @@ export async function handleChannelInbound({
|
|
|
556
571
|
// that window is silently dropped and the deletion signal is lost.
|
|
557
572
|
let original: { messageId: string; conversationId: string } | null = null;
|
|
558
573
|
for (let attempt = 0; attempt <= deleteLookupRetries; attempt++) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
574
|
+
// Two resolution lanes, each with its own race. An inbound row's
|
|
575
|
+
// event is written before its message link lands (retried until
|
|
576
|
+
// `hasInboundEventForSource` says the source was never ingested). An
|
|
577
|
+
// assistant post opens no inbound event; its provider id is written
|
|
578
|
+
// by the post-send reconciliation, which an automated deletion can
|
|
579
|
+
// outrun, so the envelope lane retries within the same bounded
|
|
580
|
+
// window rather than giving up on the first miss.
|
|
581
|
+
original =
|
|
582
|
+
findMessageBySourceId(
|
|
583
|
+
sourceChannel,
|
|
584
|
+
conversationExternalId,
|
|
585
|
+
deletedMessageTs,
|
|
586
|
+
) ??
|
|
587
|
+
findMessageByProviderMessageId(
|
|
588
|
+
sourceChannel,
|
|
589
|
+
conversationExternalId,
|
|
590
|
+
deletedMessageTs,
|
|
591
|
+
);
|
|
564
592
|
if (original) {
|
|
565
593
|
break;
|
|
566
594
|
}
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
595
|
+
// A source with no inbound row was either never ingested (nothing can
|
|
596
|
+
// appear by waiting) or is an assistant post racing its post-send id
|
|
597
|
+
// reconciliation. Only the second earns a wait, and it leaves
|
|
598
|
+
// evidence: a recent outbound row whose envelope names no id yet.
|
|
599
|
+
// Without that evidence the miss is final, so an unrelated delete in
|
|
600
|
+
// a busy room never holds the serialized lane through the window.
|
|
601
|
+
const raceStillPossible =
|
|
602
|
+
attempt < deleteOutboundReconcileRetries &&
|
|
603
|
+
hasUnreconciledOutboundRow(sourceChannel, conversationExternalId);
|
|
572
604
|
if (
|
|
605
|
+
!raceStillPossible &&
|
|
573
606
|
!hasInboundEventForSource(
|
|
574
607
|
sourceChannel,
|
|
575
608
|
conversationExternalId,
|
|
@@ -586,7 +619,7 @@ export async function handleChannelInbound({
|
|
|
586
619
|
attempt: attempt + 1,
|
|
587
620
|
maxAttempts: deleteLookupRetries,
|
|
588
621
|
},
|
|
589
|
-
"Original message not
|
|
622
|
+
"Original message not resolved yet, retrying delete lookup",
|
|
590
623
|
);
|
|
591
624
|
await new Promise((resolve) =>
|
|
592
625
|
setTimeout(resolve, deleteLookupDelayMs),
|
|
@@ -594,6 +627,17 @@ export async function handleChannelInbound({
|
|
|
594
627
|
}
|
|
595
628
|
}
|
|
596
629
|
|
|
630
|
+
// Recency-capped misses above could still be an OLD assistant post:
|
|
631
|
+
// deletion can target one arbitrarily far back, so the last resort is a
|
|
632
|
+
// single deep scan. Once, after the retries: age and race are exclusive
|
|
633
|
+
// (a post old enough to be past the recent window reconciled long ago).
|
|
634
|
+
original ??= findMessageByProviderMessageId(
|
|
635
|
+
sourceChannel,
|
|
636
|
+
conversationExternalId,
|
|
637
|
+
deletedMessageTs,
|
|
638
|
+
{ maxScan: DELETE_PROVIDER_MESSAGE_ID_MAX_SCAN },
|
|
639
|
+
);
|
|
640
|
+
|
|
597
641
|
if (!original) {
|
|
598
642
|
log.debug(
|
|
599
643
|
{ conversationExternalId, deletedMessageTs },
|
|
@@ -618,22 +662,50 @@ export async function handleChannelInbound({
|
|
|
618
662
|
: null;
|
|
619
663
|
|
|
620
664
|
if (!existingSlackMeta) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
665
|
+
// Deletion is tracked per provider post: a split reply's row names
|
|
666
|
+
// several posts, and deleting one must not read as the whole reply
|
|
667
|
+
// vanishing. The row-level `deletedAt` lands only when every post the
|
|
668
|
+
// row names is gone; a row whose envelope names no id (a legacy row
|
|
669
|
+
// synthesized from lookup facts) is single-post by construction and
|
|
670
|
+
// keeps the direct stamp.
|
|
671
|
+
const base = readProviderMetadata(row?.metadata ?? null, {
|
|
672
|
+
allowFlatLegacy: true,
|
|
673
|
+
});
|
|
674
|
+
let providerMeta: string;
|
|
675
|
+
if (base?.messageId) {
|
|
676
|
+
const rowPostIds = [
|
|
677
|
+
base.messageId,
|
|
678
|
+
...(base.additionalMessageIds ?? []),
|
|
679
|
+
];
|
|
680
|
+
const deletedIds = Array.from(
|
|
681
|
+
new Set([...(base.deletedMessageIds ?? []), deletedMessageTs]),
|
|
682
|
+
);
|
|
683
|
+
const fullyDeleted = rowPostIds.every((id) => deletedIds.includes(id));
|
|
684
|
+
providerMeta = JSON.stringify({
|
|
685
|
+
...base,
|
|
686
|
+
deletedMessageIds: deletedIds,
|
|
687
|
+
...(fullyDeleted ? { deletedAt: Date.now() } : {}),
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
providerMeta = mergeProviderMessageMetadata(
|
|
691
|
+
row?.metadata ?? null,
|
|
692
|
+
{
|
|
693
|
+
source: sourceChannel,
|
|
694
|
+
conversationExternalId,
|
|
695
|
+
messageId: deletedMessageTs,
|
|
696
|
+
...(sourceMetadata?.threadId
|
|
697
|
+
? { threadId: sourceMetadata.threadId }
|
|
698
|
+
: {}),
|
|
699
|
+
},
|
|
700
|
+
{ deletedAt: Date.now() },
|
|
701
|
+
);
|
|
702
|
+
}
|
|
633
703
|
updateMessageMetadata(original.messageId, { providerMeta });
|
|
634
|
-
// The stamp lands in the store only
|
|
635
|
-
// conversation's next turn reload and see the row as deleted
|
|
704
|
+
// The stamp lands in the store only: stale-marking makes a resident
|
|
705
|
+
// conversation's next turn reload and see the row as deleted, and the
|
|
706
|
+
// invalidation makes a client showing the conversation refetch it.
|
|
636
707
|
findConversation(original.conversationId)?.markHistoryStale();
|
|
708
|
+
publishConversationMessagesChanged(original.conversationId);
|
|
637
709
|
log.info(
|
|
638
710
|
{
|
|
639
711
|
conversationExternalId,
|
|
@@ -655,6 +727,7 @@ export async function handleChannelInbound({
|
|
|
655
727
|
// is intentionally not updated.
|
|
656
728
|
updateMessageMetadata(original.messageId, { slackMeta: updatedSlackMeta });
|
|
657
729
|
findConversation(original.conversationId)?.markHistoryStale();
|
|
730
|
+
publishConversationMessagesChanged(original.conversationId);
|
|
658
731
|
|
|
659
732
|
log.info(
|
|
660
733
|
{
|
|
@@ -1417,6 +1490,30 @@ export async function handleChannelInbound({
|
|
|
1417
1490
|
}
|
|
1418
1491
|
: undefined;
|
|
1419
1492
|
|
|
1493
|
+
// Neutral per-row envelope for every non-Slack channel, mirroring the
|
|
1494
|
+
// Slack capture above field for field: it is persisted as the row's
|
|
1495
|
+
// `providerMeta` so the row can say which external message it is, in
|
|
1496
|
+
// which thread, from whom. Slack keeps its own `slackMeta` envelope
|
|
1497
|
+
// (mapped to the neutral shape on read), so nothing is stored twice.
|
|
1498
|
+
const inboundActorDisplayName =
|
|
1499
|
+
body.actorDisplayName ?? body.actorUsername;
|
|
1500
|
+
const channelInbound: ProviderMessageMetadata | undefined =
|
|
1501
|
+
sourceChannel !== "slack"
|
|
1502
|
+
? {
|
|
1503
|
+
source: sourceChannel,
|
|
1504
|
+
conversationExternalId,
|
|
1505
|
+
messageId: sourceMessageId ?? externalMessageId,
|
|
1506
|
+
eventKind: "message",
|
|
1507
|
+
...(channelThreadId ? { threadId: channelThreadId } : {}),
|
|
1508
|
+
...(inboundActorDisplayName
|
|
1509
|
+
? { displayName: inboundActorDisplayName }
|
|
1510
|
+
: {}),
|
|
1511
|
+
...(trustCtx.requesterExternalUserId
|
|
1512
|
+
? { actorExternalId: trustCtx.requesterExternalUserId }
|
|
1513
|
+
: {}),
|
|
1514
|
+
}
|
|
1515
|
+
: undefined;
|
|
1516
|
+
|
|
1420
1517
|
// Account identifier threaded into backfill so `resolveConnection()`
|
|
1421
1518
|
// can pick the right workspace in multi-account setups. Best-effort:
|
|
1422
1519
|
// the gateway forwards `sourceMetadata.account` when it knows which
|
|
@@ -1524,6 +1621,7 @@ export async function handleChannelInbound({
|
|
|
1524
1621
|
clientTimezone: inboundClientTimezone,
|
|
1525
1622
|
slackBotMentioned,
|
|
1526
1623
|
slackInbound,
|
|
1624
|
+
channelInbound,
|
|
1527
1625
|
});
|
|
1528
1626
|
}
|
|
1529
1627
|
}
|
|
@@ -1775,6 +1873,15 @@ async function persistBackfilledSlackMessage(params: {
|
|
|
1775
1873
|
);
|
|
1776
1874
|
const slackTranscriptTimestampTimezone =
|
|
1777
1875
|
resolveSlackTranscriptTimestampTimezone();
|
|
1876
|
+
const slackTimezoneFields = buildSlackTimezoneMetadata({
|
|
1877
|
+
actorTimezone,
|
|
1878
|
+
actorTimezoneLabel,
|
|
1879
|
+
actorTimezoneOffsetSeconds: message.metadata?.actorTimezoneOffsetSeconds,
|
|
1880
|
+
timestampTimezone: slackTranscriptTimestampTimezone?.timestampTimezone,
|
|
1881
|
+
timestampTimezoneLabel:
|
|
1882
|
+
slackTranscriptTimestampTimezone?.timestampTimezoneLabel,
|
|
1883
|
+
speakerTimezoneLabel: isGuardian ? undefined : actorTimezoneLabel,
|
|
1884
|
+
});
|
|
1778
1885
|
const slackMeta: SlackMessageMetadata = {
|
|
1779
1886
|
source: "slack",
|
|
1780
1887
|
channelId: params.channelId,
|
|
@@ -1783,15 +1890,7 @@ async function persistBackfilledSlackMessage(params: {
|
|
|
1783
1890
|
...(message.threadId ? { threadTs: message.threadId } : {}),
|
|
1784
1891
|
...(message.sender?.name ? { displayName: message.sender.name } : {}),
|
|
1785
1892
|
...(actorExternalUserId ? { actorExternalUserId } : {}),
|
|
1786
|
-
...
|
|
1787
|
-
actorTimezone,
|
|
1788
|
-
actorTimezoneLabel,
|
|
1789
|
-
actorTimezoneOffsetSeconds: message.metadata?.actorTimezoneOffsetSeconds,
|
|
1790
|
-
timestampTimezone: slackTranscriptTimestampTimezone?.timestampTimezone,
|
|
1791
|
-
timestampTimezoneLabel:
|
|
1792
|
-
slackTranscriptTimestampTimezone?.timestampTimezoneLabel,
|
|
1793
|
-
speakerTimezoneLabel: isGuardian ? undefined : actorTimezoneLabel,
|
|
1794
|
-
}),
|
|
1893
|
+
...slackTimezoneFields,
|
|
1795
1894
|
...(slackFiles.length > 0 ? { slackFiles } : {}),
|
|
1796
1895
|
};
|
|
1797
1896
|
|
|
@@ -1813,9 +1912,32 @@ async function persistBackfilledSlackMessage(params: {
|
|
|
1813
1912
|
? message.timestamp
|
|
1814
1913
|
: undefined;
|
|
1815
1914
|
|
|
1915
|
+
// A row the daemon authors carries the neutral envelope, Slack's own fields
|
|
1916
|
+
// riding its passthrough; a person's row keeps Slack's envelope for now.
|
|
1917
|
+
const envelope =
|
|
1918
|
+
role === "assistant"
|
|
1919
|
+
? {
|
|
1920
|
+
providerMeta: JSON.stringify({
|
|
1921
|
+
source: "slack",
|
|
1922
|
+
conversationExternalId: params.channelId,
|
|
1923
|
+
messageId: message.id,
|
|
1924
|
+
eventKind: "message",
|
|
1925
|
+
...(message.threadId ? { threadId: message.threadId } : {}),
|
|
1926
|
+
...(message.sender?.name
|
|
1927
|
+
? { displayName: message.sender.name }
|
|
1928
|
+
: {}),
|
|
1929
|
+
...(actorExternalUserId
|
|
1930
|
+
? { actorExternalId: actorExternalUserId }
|
|
1931
|
+
: {}),
|
|
1932
|
+
...slackTimezoneFields,
|
|
1933
|
+
...(slackFiles.length > 0 ? { slackFiles } : {}),
|
|
1934
|
+
} satisfies ProviderMessageMetadata),
|
|
1935
|
+
}
|
|
1936
|
+
: { slackMeta: writeSlackMetadata(slackMeta) };
|
|
1937
|
+
|
|
1816
1938
|
const persisted = await addMessage(params.conversationId, role, rawText, {
|
|
1817
1939
|
metadata: {
|
|
1818
|
-
|
|
1940
|
+
...envelope,
|
|
1819
1941
|
...(sentAt !== undefined ? { sentAt } : {}),
|
|
1820
1942
|
provenanceTrustClass: isGuardian ? "guardian" : "unknown",
|
|
1821
1943
|
provenanceSourceChannel: "slack",
|
|
@@ -131,9 +131,10 @@ export interface AclEnforcementParams {
|
|
|
131
131
|
* The platform named no actor for this event (the id is a channel's
|
|
132
132
|
* synthetic system identity). There is no identity claim to enforce, so
|
|
133
133
|
* the ACL neither resolves a member nor denies: the family stage that
|
|
134
|
-
* consumes such an event applies it only to
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* consumes such an event applies it only to a row the daemon already
|
|
135
|
+
* holds for that chat, one ingested from an author who cleared this ACL
|
|
136
|
+
* on arrival or one the assistant posted itself. Callers set this ONLY
|
|
137
|
+
* for event kinds that cannot start an agent turn.
|
|
137
138
|
*/
|
|
138
139
|
actorUnattributed?: boolean;
|
|
139
140
|
}
|
|
@@ -44,6 +44,7 @@ mock.module("../../../persistence/delivery-channels.js", () => ({
|
|
|
44
44
|
mock.module("../../../persistence/delivery-crud.js", () => ({
|
|
45
45
|
linkMessage: () => {},
|
|
46
46
|
storeReplyMessageId: (eventId: string, replyMessageId: string) => {
|
|
47
|
+
operationOrder.push("store-reply-id");
|
|
47
48
|
storedReplyMessageIds.push({ eventId, replyMessageId });
|
|
48
49
|
},
|
|
49
50
|
storeStreamedReplyTs: (eventId: string, messageTs: string) => {
|
|
@@ -58,6 +59,7 @@ mock.module("../../../persistence/delivery-status.js", () => ({
|
|
|
58
59
|
deliveredEvents.push(eventId);
|
|
59
60
|
},
|
|
60
61
|
markProcessed: (eventId: string) => {
|
|
62
|
+
operationOrder.push("mark-processed");
|
|
61
63
|
markedProcessedEvents.push(eventId);
|
|
62
64
|
},
|
|
63
65
|
recordDeliveryFailure: (eventId: string) => {
|
|
@@ -111,12 +113,35 @@ mock.module("../../../messaging/providers/index.js", () => ({
|
|
|
111
113
|
sentActivity.push(target);
|
|
112
114
|
return setChannelActivityImpl(target);
|
|
113
115
|
},
|
|
116
|
+
// Slack declares it can stream by implementing the method; whether a given
|
|
117
|
+
// conversation can is Slack's own answer to `start`, which is why the stub
|
|
118
|
+
// below models the refusal rather than gating before the call.
|
|
119
|
+
getTransportForCallback: () => ({
|
|
120
|
+
streamReply: () => undefined,
|
|
121
|
+
streamPersists: true,
|
|
122
|
+
}),
|
|
114
123
|
sendChannelStreamOp: async (
|
|
115
|
-
|
|
124
|
+
callbackUrl: string,
|
|
116
125
|
_chatId: string,
|
|
117
126
|
op: Record<string, unknown>,
|
|
118
127
|
) => {
|
|
119
128
|
sentStreamOps.push(op);
|
|
129
|
+
// `chat.startStream` streams into a thread, and names the reader when the
|
|
130
|
+
// room has more than one, so a start lacking either is refused. Modelled
|
|
131
|
+
// here so these tests exercise the fallback the refusal produces.
|
|
132
|
+
// `chat.startStream` streams into a thread, and a room with more than one
|
|
133
|
+
// reader must name the reader, so a start lacking either is refused. Slack
|
|
134
|
+
// ids say which room this is: `D` a DM, `C` a channel. Modelled here so
|
|
135
|
+
// these tests exercise the fallback a refusal produces, rather than a gate
|
|
136
|
+
// that no longer exists above the transport.
|
|
137
|
+
if (op.action === "start") {
|
|
138
|
+
if (!callbackUrl.includes("threadTs=")) {
|
|
139
|
+
return { ok: false };
|
|
140
|
+
}
|
|
141
|
+
if (/channel=C/.test(callbackUrl) && op.audience === undefined) {
|
|
142
|
+
return { ok: false };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
120
145
|
return sendChannelStreamOpImpl(op);
|
|
121
146
|
},
|
|
122
147
|
}));
|
|
@@ -322,6 +347,37 @@ describe("processChannelMessageInBackground — reply delivery", () => {
|
|
|
322
347
|
expect(deliveredEvents).toEqual(["evt-fast-path"]);
|
|
323
348
|
});
|
|
324
349
|
|
|
350
|
+
test("stores the reply id before marking the event processed", async () => {
|
|
351
|
+
// `processed` is the point stranded-delivery recovery starts considering
|
|
352
|
+
// an event, and that step reads the reply id from the stored payload. If
|
|
353
|
+
// the event became eligible first, a crash in between would leave a row
|
|
354
|
+
// that recovery skips, so its reply would never be delivered.
|
|
355
|
+
const processMessage: MessageProcessor = async () => ({
|
|
356
|
+
messageId: "user-msg-order",
|
|
357
|
+
assistantMessageId: "assistant-msg-order",
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
processChannelMessageInBackground({
|
|
361
|
+
processMessage,
|
|
362
|
+
conversationId: "conv-order",
|
|
363
|
+
eventId: "evt-order",
|
|
364
|
+
content: "hello",
|
|
365
|
+
sourceChannel: "slack",
|
|
366
|
+
sourceInterface: "slack",
|
|
367
|
+
externalChatId: "C-ORDER",
|
|
368
|
+
trustCtx,
|
|
369
|
+
metadataHints: [],
|
|
370
|
+
replyCallbackUrl: "https://example.test/deliver/slack?channel=C-ORDER",
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await flush();
|
|
374
|
+
|
|
375
|
+
expect(operationOrder.indexOf("store-reply-id")).toBeGreaterThanOrEqual(0);
|
|
376
|
+
expect(operationOrder.indexOf("store-reply-id")).toBeLessThan(
|
|
377
|
+
operationOrder.indexOf("mark-processed"),
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
325
381
|
test("suppresses reply delivery when a deduplicated redelivery's prior attempt already delivered", async () => {
|
|
326
382
|
const conversationId = "conv-dedup-delivered";
|
|
327
383
|
const channelId = "C-DEDUP-DELIVERED";
|
|
@@ -351,12 +407,15 @@ describe("processChannelMessageInBackground — reply delivery", () => {
|
|
|
351
407
|
|
|
352
408
|
await flush();
|
|
353
409
|
|
|
354
|
-
// The redelivery is recorded as processed
|
|
355
|
-
//
|
|
410
|
+
// The redelivery is recorded as processed and nothing is re-posted. It
|
|
411
|
+
// still settles its OWN delivery state: the sibling owns delivery, so a
|
|
412
|
+
// row left `pending` would read as still owing one and the
|
|
413
|
+
// stranded-delivery recovery step would re-post the sibling's reply
|
|
414
|
+
// after a restart.
|
|
356
415
|
expect(markedProcessedEvents).toEqual(["evt-dedup-delivered"]);
|
|
357
416
|
expect(replyDeliveryCalls).toEqual([]);
|
|
358
|
-
expect(deliveredEvents).toEqual([]);
|
|
359
417
|
expect(deliveredChannelReplies).toEqual([]);
|
|
418
|
+
expect(deliveredEvents).toEqual(["evt-dedup-delivered"]);
|
|
360
419
|
});
|
|
361
420
|
|
|
362
421
|
test("skips reply delivery when a deduplicated redelivery's prior attempt failed (sweep owns recovery)", async () => {
|
|
@@ -386,10 +445,13 @@ describe("processChannelMessageInBackground — reply delivery", () => {
|
|
|
386
445
|
|
|
387
446
|
await flush();
|
|
388
447
|
|
|
448
|
+
// Same settlement as the delivered-sibling case. Marking THIS row
|
|
449
|
+
// delivered does not touch the sibling's `failed` status, so the sweep
|
|
450
|
+
// still owns and retries the real delivery.
|
|
389
451
|
expect(markedProcessedEvents).toEqual(["evt-dedup-failed"]);
|
|
390
452
|
expect(replyDeliveryCalls).toEqual([]);
|
|
391
|
-
expect(deliveredEvents).toEqual([]);
|
|
392
453
|
expect(deliveredChannelReplies).toEqual([]);
|
|
454
|
+
expect(deliveredEvents).toEqual(["evt-dedup-failed"]);
|
|
393
455
|
});
|
|
394
456
|
|
|
395
457
|
test("recovers the reply when a deduplicated redelivery's prior attempt is stuck pending (crash window)", async () => {
|
|
@@ -510,7 +572,8 @@ describe("processChannelMessageInBackground — reply delivery", () => {
|
|
|
510
572
|
|
|
511
573
|
await flush();
|
|
512
574
|
|
|
513
|
-
|
|
575
|
+
// A start is attempted and refused, so nothing advances past it.
|
|
576
|
+
expect(slackStreamOps().map((op) => op.action)).toEqual(["start"]);
|
|
514
577
|
expect(
|
|
515
578
|
deliveredChannelReplies
|
|
516
579
|
.map((entry) => entry.payload.text)
|
|
@@ -566,7 +629,6 @@ describe("processChannelMessageInBackground — reply delivery", () => {
|
|
|
566
629
|
expect(slackStreamOps()).toEqual([
|
|
567
630
|
{
|
|
568
631
|
action: "start",
|
|
569
|
-
anchorMessageId: threadTs,
|
|
570
632
|
text: "Streamed DM reply.",
|
|
571
633
|
appended: "Streamed DM reply.",
|
|
572
634
|
},
|
|
@@ -648,7 +710,8 @@ describe("processChannelMessageInBackground — reply delivery", () => {
|
|
|
648
710
|
.map((entry) => entry.payload.text)
|
|
649
711
|
.filter(Boolean),
|
|
650
712
|
).toEqual([]);
|
|
651
|
-
|
|
713
|
+
// A start is attempted and refused, so nothing advances past it.
|
|
714
|
+
expect(slackStreamOps().map((op) => op.action)).toEqual(["start"]);
|
|
652
715
|
expect(replyDeliveryCalls).toEqual([
|
|
653
716
|
{ messageId: "assistant-msg-channel-final", startFromSegment: 0 },
|
|
654
717
|
]);
|
|
@@ -889,6 +952,104 @@ describe("processChannelMessageInBackground — admission (queue if busy)", () =
|
|
|
889
952
|
expect(markedProcessedEvents).toEqual([]);
|
|
890
953
|
expect(deliveredEvents).toEqual([]);
|
|
891
954
|
});
|
|
955
|
+
|
|
956
|
+
test("a busy loss with a caller fallback degrades there, never to the sweep", async () => {
|
|
957
|
+
const processMessage: MessageProcessor = async () => {
|
|
958
|
+
throw new Error(CONVERSATION_BUSY_MESSAGE);
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
let fellBack = 0;
|
|
962
|
+
processChannelMessageInBackground({
|
|
963
|
+
processMessage,
|
|
964
|
+
conversationId: "conv-wake-busy",
|
|
965
|
+
eventId: "evt-wake-busy",
|
|
966
|
+
content: "Alice reacted with :tada: to your message",
|
|
967
|
+
sourceChannel: "discord",
|
|
968
|
+
sourceInterface: "discord",
|
|
969
|
+
externalChatId: "999000111222333444",
|
|
970
|
+
trustCtx,
|
|
971
|
+
metadataHints: [],
|
|
972
|
+
replyCallbackUrl: "https://example.test/deliver/discord",
|
|
973
|
+
onTurnLostToBusy: async () => {
|
|
974
|
+
fellBack++;
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
await flush();
|
|
979
|
+
|
|
980
|
+
// The caller's event stores nothing the sweep could rebuild a turn
|
|
981
|
+
// from, so its processing lane must never see it: the degradation is
|
|
982
|
+
// the fallback alone.
|
|
983
|
+
expect(fellBack).toBe(1);
|
|
984
|
+
expect(deferredRetryEvents).toEqual([]);
|
|
985
|
+
expect(retryableFailureEvents).toEqual([]);
|
|
986
|
+
expect(processingFailureEvents).toEqual([]);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
test("a non-busy failure with a caller fallback records nothing for the sweep", async () => {
|
|
990
|
+
const processMessage: MessageProcessor = async () => {
|
|
991
|
+
throw new Error("provider exploded");
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
let fellBack = 0;
|
|
995
|
+
processChannelMessageInBackground({
|
|
996
|
+
processMessage,
|
|
997
|
+
conversationId: "conv-wake-error",
|
|
998
|
+
eventId: "evt-wake-error",
|
|
999
|
+
content: "Alice reacted with :tada: to your message",
|
|
1000
|
+
sourceChannel: "discord",
|
|
1001
|
+
sourceInterface: "discord",
|
|
1002
|
+
externalChatId: "999000111222333444",
|
|
1003
|
+
trustCtx,
|
|
1004
|
+
metadataHints: [],
|
|
1005
|
+
replyCallbackUrl: "https://example.test/deliver/discord",
|
|
1006
|
+
onTurnLostToBusy: async () => {
|
|
1007
|
+
fellBack++;
|
|
1008
|
+
},
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
await flush();
|
|
1012
|
+
|
|
1013
|
+
// Best-effort turn lost; a failure record would only feed the sweep an
|
|
1014
|
+
// event it cannot rebuild. The fallback is busy-specific and stays uncalled.
|
|
1015
|
+
expect(fellBack).toBe(0);
|
|
1016
|
+
expect(processingFailureEvents).toEqual([]);
|
|
1017
|
+
expect(deferredRetryEvents).toEqual([]);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test("ingress row extensions reach processMessage", async () => {
|
|
1021
|
+
let seenOptions: Record<string, unknown> | undefined;
|
|
1022
|
+
const processMessage: MessageProcessor = async (
|
|
1023
|
+
_conversationId,
|
|
1024
|
+
_content,
|
|
1025
|
+
options,
|
|
1026
|
+
) => {
|
|
1027
|
+
seenOptions = options as unknown as Record<string, unknown>;
|
|
1028
|
+
return { messageId: "user-row-1" };
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
processChannelMessageInBackground({
|
|
1032
|
+
processMessage,
|
|
1033
|
+
conversationId: "conv-wake-meta",
|
|
1034
|
+
eventId: "evt-wake-meta",
|
|
1035
|
+
content: "Alice reacted with :tada: to your message",
|
|
1036
|
+
sourceChannel: "discord",
|
|
1037
|
+
sourceInterface: "discord",
|
|
1038
|
+
externalChatId: "999000111222333444",
|
|
1039
|
+
trustCtx,
|
|
1040
|
+
metadataHints: [],
|
|
1041
|
+
replyCallbackUrl: "https://example.test/deliver/discord",
|
|
1042
|
+
slackReactionRowMeta: '{"eventKind":"reaction"}',
|
|
1043
|
+
clientMessageId: "reaction:evt-wake-meta",
|
|
1044
|
+
skipUserMessageIndexing: true,
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
await flush();
|
|
1048
|
+
|
|
1049
|
+
expect(seenOptions?.slackReactionRowMeta).toBe('{"eventKind":"reaction"}');
|
|
1050
|
+
expect(seenOptions?.clientMessageId).toBe("reaction:evt-wake-meta");
|
|
1051
|
+
expect(seenOptions?.skipUserMessageIndexing).toBe(true);
|
|
1052
|
+
});
|
|
892
1053
|
});
|
|
893
1054
|
|
|
894
1055
|
describe("channel activity timing", () => {
|