@vellumai/assistant 0.11.8 → 0.11.9-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +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__/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__/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 +12 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +15 -1
- 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/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/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/injector.ts +14 -15
- 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 +12 -7
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +31 -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
|
@@ -5,17 +5,14 @@ import type {
|
|
|
5
5
|
ToolResultEvent,
|
|
6
6
|
ToolUseStartEvent,
|
|
7
7
|
} from "../api/index.js";
|
|
8
|
-
import {
|
|
9
|
-
extractThreadTsFromCallbackUrl,
|
|
10
|
-
isSlackDeliveryCallbackUrl,
|
|
11
|
-
} from "../channels/slack-callback-url.js";
|
|
12
|
-
import type { ChannelId } from "../channels/types.js";
|
|
13
8
|
import {
|
|
14
9
|
incompleteVellumLinkSuffixLength,
|
|
15
10
|
stripVellumLinks,
|
|
16
11
|
} from "../daemon/assistant-attachments.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
12
|
+
import {
|
|
13
|
+
getTransportForCallback,
|
|
14
|
+
sendChannelStreamOp,
|
|
15
|
+
} from "../messaging/providers/index.js";
|
|
19
16
|
import { getLogger } from "../util/logger.js";
|
|
20
17
|
import { needsBoundarySpace } from "../util/text-spacing.js";
|
|
21
18
|
import {
|
|
@@ -27,7 +24,7 @@ import {
|
|
|
27
24
|
mergeTaskProgressData,
|
|
28
25
|
} from "./task-progress.js";
|
|
29
26
|
|
|
30
|
-
const log = getLogger("
|
|
27
|
+
const log = getLogger("channel-reply-session");
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
30
|
* Minimum gap between coalesced `appendStream` calls. `chat.appendStream` is a
|
|
@@ -44,101 +41,101 @@ const STREAM_COALESCE_MS = 400;
|
|
|
44
41
|
* - `streamed`: the reply was delivered live into a single streamed message;
|
|
45
42
|
* finalize skips re-posting text, reconciles `slackMeta.channelTs` to the
|
|
46
43
|
* stream `ts`, and posts only attachments.
|
|
47
|
-
* - `fallback`: no stream was opened (ineligible turn,
|
|
48
|
-
* a failed `startStream`); finalize posts the full
|
|
44
|
+
* - `fallback`: no stream was opened (ineligible turn, neither deliverable
|
|
45
|
+
* text nor a plan, or a failed `startStream`); finalize posts the full
|
|
46
|
+
* reply normally.
|
|
49
47
|
*/
|
|
50
|
-
export type
|
|
48
|
+
export type StreamReconciliation =
|
|
51
49
|
| { mode: "streamed"; messageTs: string; deliveredSegmentCount: number }
|
|
52
50
|
| { mode: "fallback" };
|
|
53
51
|
|
|
54
|
-
export type
|
|
52
|
+
export type ChannelReplySession = {
|
|
55
53
|
observeEvent: (msg: AssistantEvent) => void;
|
|
56
54
|
/**
|
|
57
55
|
* Settle in-flight stream operations, finalize the stream, and report how
|
|
58
56
|
* durable delivery should reconcile. Call once after processing completes.
|
|
59
57
|
*/
|
|
60
|
-
finish: () => Promise<
|
|
58
|
+
finish: () => Promise<StreamReconciliation>;
|
|
61
59
|
};
|
|
62
60
|
|
|
63
61
|
/**
|
|
64
|
-
* Whether a
|
|
65
|
-
* turn resolves to a threaded reply, since `chat.startStream` requires a
|
|
66
|
-
* `thread_ts`. Assistant-container DMs carry that thread implicitly; channel
|
|
67
|
-
* turns (including app-mention threads) thread under the inbound message.
|
|
62
|
+
* Whether a channel can carry a reply that grows while the turn runs.
|
|
68
63
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
64
|
+
* Only the channel-level half is answered here, and only by asking whether
|
|
65
|
+
* the transport implements the operation: implementing it is the whole of
|
|
66
|
+
* declaring it. Whether THIS conversation can carry one is the platform's
|
|
67
|
+
* own rule (Slack streams into a thread, Telegram only into a private chat),
|
|
68
|
+
* so it is answered by the transport's own reply to `start` and reaches this
|
|
69
|
+
* module as an ordinary not-ok, which falls back exactly like a failure.
|
|
75
70
|
*/
|
|
76
|
-
export function
|
|
77
|
-
|
|
78
|
-
chatType?: string;
|
|
79
|
-
replyCallbackUrl?: string;
|
|
80
|
-
recipientUserId?: string;
|
|
81
|
-
recipientTeamId?: string;
|
|
82
|
-
}): boolean {
|
|
83
|
-
if (params.sourceChannel !== "slack") {
|
|
71
|
+
export function channelCanStreamReply(replyCallbackUrl?: string): boolean {
|
|
72
|
+
if (!replyCallbackUrl) {
|
|
84
73
|
return false;
|
|
85
74
|
}
|
|
86
|
-
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
if (extractThreadTsFromCallbackUrl(params.replyCallbackUrl) === null) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
if (params.chatType === "im") {
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
return Boolean(params.recipientUserId && params.recipientTeamId);
|
|
75
|
+
return getTransportForCallback(replyCallbackUrl)?.streamReply !== undefined;
|
|
96
76
|
}
|
|
97
77
|
|
|
98
78
|
type StreamState = "idle" | "streaming" | "fallback";
|
|
99
79
|
|
|
100
80
|
/**
|
|
101
|
-
* Owns the live-content lifecycle of one
|
|
81
|
+
* Owns the live-content lifecycle of one growing reply: it consumes the
|
|
102
82
|
* assistant token stream once, opens a streamed message (in plan display
|
|
103
|
-
* mode) on first deliverable text
|
|
104
|
-
* calls, advances a native plan
|
|
105
|
-
* finalizes with `stopStream`.
|
|
83
|
+
* mode) on the first deliverable text or progress plan, whichever comes
|
|
84
|
+
* first, coalesces deltas into `appendStream` calls, advances a native plan
|
|
85
|
+
* block from `task_progress` surfaces, and finalizes with `stopStream`.
|
|
106
86
|
*
|
|
107
87
|
* Any stream-call failure degrades gracefully: the session abandons streaming
|
|
108
88
|
* (state `fallback`) and reports back so durable finalize posts the full reply
|
|
109
89
|
* via the normal path. Returns `undefined` for turns that are not eligible to
|
|
110
90
|
* stream.
|
|
111
91
|
*/
|
|
112
|
-
export function
|
|
113
|
-
sourceChannel: ChannelId;
|
|
114
|
-
chatType?: string;
|
|
92
|
+
export function createChannelReplySession(params: {
|
|
115
93
|
replyCallbackUrl?: string;
|
|
116
94
|
chatId: string;
|
|
117
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* The one reader a restricted stream is shown to, when the turn names one.
|
|
97
|
+
* Travels as the operation's `audience`; a channel that cannot restrict a
|
|
98
|
+
* growing reply ignores it, and one that requires a named reader in a room
|
|
99
|
+
* refuses the start without it.
|
|
100
|
+
*/
|
|
118
101
|
recipientUserId?: string;
|
|
119
|
-
/**
|
|
102
|
+
/** The reader's org, alongside {@link recipientUserId}. */
|
|
120
103
|
recipientTeamId?: string;
|
|
121
104
|
/** Gap between coalesced `appendStream` calls. Defaults to {@link STREAM_COALESCE_MS}. */
|
|
122
105
|
coalesceMs?: number;
|
|
123
106
|
/**
|
|
124
|
-
* Invoked once with the streamed message
|
|
125
|
-
* opens
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
107
|
+
* Invoked once with the streamed message's id the moment a stream that
|
|
108
|
+
* BECOMES the reply opens, so the caller can durably record it before
|
|
109
|
+
* delivery finalizes: a crash mid-turn then leaves a breadcrumb, and a
|
|
110
|
+
* retry reconciles against the already-visible message instead of posting
|
|
111
|
+
* a duplicate reply.
|
|
112
|
+
*
|
|
113
|
+
* Never invoked for a preview stream. A preview leaves no message behind,
|
|
114
|
+
* so there is nothing for a retry to reconcile against, and recording its
|
|
115
|
+
* id would point recovery at a message that never existed: the retry would
|
|
116
|
+
* try to edit it and the reply would be lost rather than posted.
|
|
129
117
|
*/
|
|
130
118
|
onStreamOpen?: (streamTs: string) => void;
|
|
131
|
-
}):
|
|
132
|
-
if (
|
|
119
|
+
}): ChannelReplySession | undefined {
|
|
120
|
+
if (
|
|
121
|
+
!params.replyCallbackUrl ||
|
|
122
|
+
!channelCanStreamReply(params.replyCallbackUrl)
|
|
123
|
+
) {
|
|
133
124
|
return undefined;
|
|
134
125
|
}
|
|
135
126
|
const { chatId, recipientUserId, recipientTeamId } = params;
|
|
136
127
|
const coalesceMs = params.coalesceMs ?? STREAM_COALESCE_MS;
|
|
137
128
|
const replyCallbackUrl = params.replyCallbackUrl;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
// What the stream leaves behind is the channel's business; the session only
|
|
130
|
+
// needs to know whether the reply is still owed once the stream ends.
|
|
131
|
+
const streamIsTheReply =
|
|
132
|
+
getTransportForCallback(replyCallbackUrl)?.streamPersists === true;
|
|
133
|
+
// How much text one operation may carry. The channel declares it; splitting
|
|
134
|
+
// stays here because this is what tracks how much the channel has actually
|
|
135
|
+
// taken, and that mark may only advance once per confirmed operation.
|
|
136
|
+
const maxTextChars =
|
|
137
|
+
getTransportForCallback(replyCallbackUrl)?.maxStreamTextChars ??
|
|
138
|
+
Number.POSITIVE_INFINITY;
|
|
142
139
|
|
|
143
140
|
let state: StreamState = "idle";
|
|
144
141
|
let started = false;
|
|
@@ -157,11 +154,11 @@ export function createSlackReplySession(params: {
|
|
|
157
154
|
let pendingSegmentBoundary = false;
|
|
158
155
|
|
|
159
156
|
let activeProgress: StreamPlan | undefined;
|
|
160
|
-
// Fingerprint of the plan state last delivered
|
|
157
|
+
// Fingerprint of the plan state last delivered, so progress that
|
|
161
158
|
// advances without new body text still flushes as a task-only append.
|
|
162
159
|
let deliveredProgressKey: string | undefined;
|
|
163
|
-
// Set once a
|
|
164
|
-
//
|
|
160
|
+
// Set once a plan-only append is rejected (a channel that requires words
|
|
161
|
+
// on every operation): the session stops attempting
|
|
165
162
|
// them and progress rides the next text append or `stopStream` instead,
|
|
166
163
|
// so a rejecting workspace pays one failed call per turn, not one per
|
|
167
164
|
// progress update.
|
|
@@ -173,7 +170,7 @@ export function createSlackReplySession(params: {
|
|
|
173
170
|
const cleanedText = (): string =>
|
|
174
171
|
stripVellumLinks(rawText).replace(NO_RESPONSE_INLINE_RE, "");
|
|
175
172
|
|
|
176
|
-
// Text safe to
|
|
173
|
+
// Text safe to hand to an append-only stream: while more deltas may
|
|
177
174
|
// arrive, a trailing `[label](vellum://…)` link that is still being assembled
|
|
178
175
|
// is withheld so its internal path is never emitted before the link closes
|
|
179
176
|
// (and `stripVellumLinks` can remove it). Once `finished`, no delta can
|
|
@@ -197,15 +194,18 @@ export function createSlackReplySession(params: {
|
|
|
197
194
|
const enqueueStart = (): void => {
|
|
198
195
|
enqueue(async () => {
|
|
199
196
|
const clean = streamableText();
|
|
200
|
-
|
|
197
|
+
const plan = activeProgress;
|
|
198
|
+
// A plan alone opens the stream; text alone does too. Neither means
|
|
199
|
+
// there is nothing to show yet, so the start is left for a later flush.
|
|
200
|
+
if (clean.trim().length === 0 && !plan) {
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
// Open on what one operation may carry; the rest drains through the
|
|
204
|
+
// appends that follow, each advancing the delivered mark on its own.
|
|
205
|
+
const firstChunk = clean.slice(0, maxTextChars);
|
|
205
206
|
try {
|
|
206
207
|
const result = await sendChannelStreamOp(replyCallbackUrl, chatId, {
|
|
207
208
|
action: "start",
|
|
208
|
-
anchorMessageId: threadTs,
|
|
209
209
|
text: clean,
|
|
210
210
|
appended: firstChunk,
|
|
211
211
|
...(plan ? { plan } : {}),
|
|
@@ -224,23 +224,25 @@ export function createSlackReplySession(params: {
|
|
|
224
224
|
confirmedLength = firstChunk.length;
|
|
225
225
|
deliveredProgressKey = progressKey(plan);
|
|
226
226
|
state = "streaming";
|
|
227
|
-
// The stream is already open on
|
|
227
|
+
// The stream is already open on the channel's side, so an `onStreamOpen`
|
|
228
228
|
// failure must not downgrade to fallback and repost the visible
|
|
229
229
|
// reply. Losing the breadcrumb only forfeits crash-window dedup —
|
|
230
230
|
// strictly better than a guaranteed duplicate post.
|
|
231
231
|
try {
|
|
232
|
-
|
|
232
|
+
if (streamIsTheReply) {
|
|
233
|
+
params.onStreamOpen?.(result.ts);
|
|
234
|
+
}
|
|
233
235
|
} catch (err) {
|
|
234
236
|
log.warn(
|
|
235
237
|
{ err, chatId },
|
|
236
|
-
"
|
|
238
|
+
"onStreamOpen callback failed; keeping streamed state",
|
|
237
239
|
);
|
|
238
240
|
}
|
|
239
241
|
} else {
|
|
240
242
|
state = "fallback";
|
|
241
243
|
}
|
|
242
244
|
} catch (err) {
|
|
243
|
-
log.warn({ err, chatId }, "
|
|
245
|
+
log.warn({ err, chatId }, "Stream start failed; falling back");
|
|
244
246
|
state = "fallback";
|
|
245
247
|
}
|
|
246
248
|
});
|
|
@@ -255,13 +257,15 @@ export function createSlackReplySession(params: {
|
|
|
255
257
|
const clean = streamableText();
|
|
256
258
|
const plan = activeProgress;
|
|
257
259
|
const key = progressKey(plan);
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
260
|
+
// One operation per chunk the channel will accept, and the delivered
|
|
261
|
+
// mark advances only for a chunk the channel confirmed. Advancing it for
|
|
262
|
+
// a whole delta the channel took in pieces would let a failure part-way
|
|
263
|
+
// through re-send the part that already landed, and the reader would see
|
|
264
|
+
// it twice. The plan rides along, so it advances with the text.
|
|
261
265
|
while (confirmedLength < clean.length) {
|
|
262
266
|
const chunk = clean.slice(
|
|
263
267
|
confirmedLength,
|
|
264
|
-
confirmedLength +
|
|
268
|
+
confirmedLength + maxTextChars,
|
|
265
269
|
);
|
|
266
270
|
try {
|
|
267
271
|
await sendChannelStreamOp(replyCallbackUrl, chatId, {
|
|
@@ -274,10 +278,7 @@ export function createSlackReplySession(params: {
|
|
|
274
278
|
confirmedLength += chunk.length;
|
|
275
279
|
deliveredProgressKey = key ?? deliveredProgressKey;
|
|
276
280
|
} catch (err) {
|
|
277
|
-
log.warn(
|
|
278
|
-
{ err, chatId },
|
|
279
|
-
"Slack appendStream failed; deferring delta",
|
|
280
|
-
);
|
|
281
|
+
log.warn({ err, chatId }, "Stream append failed; deferring delta");
|
|
281
282
|
return;
|
|
282
283
|
}
|
|
283
284
|
}
|
|
@@ -300,13 +301,28 @@ export function createSlackReplySession(params: {
|
|
|
300
301
|
taskOnlyAppendsDisabled = true;
|
|
301
302
|
log.warn(
|
|
302
303
|
{ err, chatId },
|
|
303
|
-
"
|
|
304
|
+
"Plan-only append failed; deferring progress to text appends",
|
|
304
305
|
);
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
});
|
|
308
309
|
};
|
|
309
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Whether there is anything worth opening the stream for.
|
|
313
|
+
*
|
|
314
|
+
* A plan counts on its own. `chat.startStream` takes `markdown_text` as
|
|
315
|
+
* optional and fixes `task_display_mode` at start, so a plan-only start is
|
|
316
|
+
* both legal and the only way task cards can tick while the turn works: the
|
|
317
|
+
* model is told to show its progress card early on a multi-step turn, which
|
|
318
|
+
* is precisely the moment it has produced no prose. Requiring text first
|
|
319
|
+
* would hold every such plan back until the final answer, by which point
|
|
320
|
+
* the steps it was meant to narrate are already done.
|
|
321
|
+
*/
|
|
322
|
+
const hasOpenableContent = (): boolean =>
|
|
323
|
+
hasDeliverableAssistantText(streamableText()) ||
|
|
324
|
+
activeProgress !== undefined;
|
|
325
|
+
|
|
310
326
|
const flush = (): void => {
|
|
311
327
|
if (coalesceTimer) {
|
|
312
328
|
clearTimeout(coalesceTimer);
|
|
@@ -316,7 +332,7 @@ export function createSlackReplySession(params: {
|
|
|
316
332
|
return;
|
|
317
333
|
}
|
|
318
334
|
if (!started) {
|
|
319
|
-
if (!
|
|
335
|
+
if (!hasOpenableContent()) {
|
|
320
336
|
return;
|
|
321
337
|
}
|
|
322
338
|
started = true;
|
|
@@ -453,7 +469,10 @@ export function createSlackReplySession(params: {
|
|
|
453
469
|
|
|
454
470
|
// A reply that completed before the first coalesced flush still streams
|
|
455
471
|
// as a single start→stop so the transcript holds one streamed message.
|
|
456
|
-
|
|
472
|
+
// A turn whose only output was a plan opens here too, so work the model
|
|
473
|
+
// narrated only as steps still leaves the user something to read.
|
|
474
|
+
// `finished` is already set, so the shared check reads the full text.
|
|
475
|
+
if (!started && hasOpenableContent()) {
|
|
457
476
|
started = true;
|
|
458
477
|
enqueueStart();
|
|
459
478
|
}
|
|
@@ -481,7 +500,7 @@ export function createSlackReplySession(params: {
|
|
|
481
500
|
} catch (err) {
|
|
482
501
|
log.warn(
|
|
483
502
|
{ err, chatId },
|
|
484
|
-
"
|
|
503
|
+
"Stream stop failed; falling back to durable delivery",
|
|
485
504
|
);
|
|
486
505
|
state = "fallback";
|
|
487
506
|
}
|
|
@@ -489,7 +508,9 @@ export function createSlackReplySession(params: {
|
|
|
489
508
|
|
|
490
509
|
await opChain;
|
|
491
510
|
|
|
492
|
-
|
|
511
|
+
// A preview stream reports `fallback`: the draft evaporates, so the
|
|
512
|
+
// reply is still owed and durable delivery sends it as it always would.
|
|
513
|
+
if (state === "streaming" && streamTs && streamIsTheReply) {
|
|
493
514
|
return { mode: "streamed", messageTs: streamTs, deliveredSegmentCount };
|
|
494
515
|
}
|
|
495
516
|
return { mode: "fallback" };
|
|
@@ -15,6 +15,10 @@ import { findConversation } from "../daemon/conversation-registry.js";
|
|
|
15
15
|
import { getDiskPressureStatus } from "../daemon/disk-pressure-guard.js";
|
|
16
16
|
import { classifyDiskPressureTurnPolicy } from "../daemon/disk-pressure-policy.js";
|
|
17
17
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
18
|
+
import {
|
|
19
|
+
type ProviderMessageMetadata,
|
|
20
|
+
providerMessageMetadataSchema,
|
|
21
|
+
} from "../messaging/provider-message-metadata.js";
|
|
18
22
|
import { updateDeliveredSegmentCount } from "../persistence/delivery-channels.js";
|
|
19
23
|
import {
|
|
20
24
|
clearPayload,
|
|
@@ -107,6 +111,13 @@ function parseTrustRuntimeContext(value: unknown): TrustContext | undefined {
|
|
|
107
111
|
: undefined,
|
|
108
112
|
requesterChatId:
|
|
109
113
|
typeof raw.requesterChatId === "string" ? raw.requesterChatId : undefined,
|
|
114
|
+
// The triggering message's own id and thread, stamped at ingress. A
|
|
115
|
+
// replayed turn keeps them so what reads them (the turn's chat and thread
|
|
116
|
+
// lines, approval-card source links) sees the same turn the live path ran.
|
|
117
|
+
sourceMessageId:
|
|
118
|
+
typeof raw.sourceMessageId === "string" ? raw.sourceMessageId : undefined,
|
|
119
|
+
sourceThreadId:
|
|
120
|
+
typeof raw.sourceThreadId === "string" ? raw.sourceThreadId : undefined,
|
|
110
121
|
requesterContactId:
|
|
111
122
|
typeof raw.requesterContactId === "string"
|
|
112
123
|
? raw.requesterContactId
|
|
@@ -200,6 +211,68 @@ function buildReplaySlackInbound(params: {
|
|
|
200
211
|
};
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Read the neutral `channelInbound` envelope the live path captured onto the
|
|
216
|
+
* payload (via `storeInboundChannelMetadata`), the non-Slack counterpart of
|
|
217
|
+
* {@link parseStoredSlackInbound}: the EXACT envelope the live turn used, so
|
|
218
|
+
* the replayed row carries an identical `providerMeta`.
|
|
219
|
+
*/
|
|
220
|
+
function parseStoredChannelInbound(
|
|
221
|
+
value: unknown,
|
|
222
|
+
): ProviderMessageMetadata | undefined {
|
|
223
|
+
const parsed = providerMessageMetadataSchema.safeParse(value);
|
|
224
|
+
return parsed.success ? parsed.data : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Fallback when the payload carries no captured `channelInbound`, the
|
|
229
|
+
* non-Slack counterpart of {@link buildReplaySlackInbound}: reconstruct the
|
|
230
|
+
* envelope from what the payload has always carried, so a crash between
|
|
231
|
+
* `storePayload` and `storeInboundChannelMetadata` recovers the same row a
|
|
232
|
+
* live turn would have persisted. `messageId` mirrors the live
|
|
233
|
+
* `sourceMessageId ?? externalMessageId` derivation; `displayName` mirrors
|
|
234
|
+
* the live `actorDisplayName ?? actorUsername` (stored as the payload's
|
|
235
|
+
* `senderName` / `senderUsername`); `actorExternalId` comes from the same
|
|
236
|
+
* trust context the live envelope read.
|
|
237
|
+
*/
|
|
238
|
+
function buildReplayChannelInbound(params: {
|
|
239
|
+
sourceChannel: ChannelId;
|
|
240
|
+
externalChatId: string | undefined;
|
|
241
|
+
sourceMetadata: import("@vellumai/gateway-client").SourceMetadata | undefined;
|
|
242
|
+
externalMessageId: string | undefined;
|
|
243
|
+
senderDisplayName: string | undefined;
|
|
244
|
+
actorExternalId: string | undefined;
|
|
245
|
+
}): ProviderMessageMetadata | undefined {
|
|
246
|
+
if (params.sourceChannel === "slack" || !params.externalChatId) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const messageId =
|
|
250
|
+
(typeof params.sourceMetadata?.messageId === "string"
|
|
251
|
+
? params.sourceMetadata.messageId
|
|
252
|
+
: undefined) ?? params.externalMessageId;
|
|
253
|
+
if (!messageId) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const threadId =
|
|
257
|
+
typeof params.sourceMetadata?.threadId === "string" &&
|
|
258
|
+
params.sourceMetadata.threadId.trim().length > 0
|
|
259
|
+
? params.sourceMetadata.threadId.trim()
|
|
260
|
+
: undefined;
|
|
261
|
+
return {
|
|
262
|
+
source: params.sourceChannel,
|
|
263
|
+
conversationExternalId: params.externalChatId,
|
|
264
|
+
messageId,
|
|
265
|
+
eventKind: "message",
|
|
266
|
+
...(threadId ? { threadId } : {}),
|
|
267
|
+
...(params.senderDisplayName
|
|
268
|
+
? { displayName: params.senderDisplayName }
|
|
269
|
+
: {}),
|
|
270
|
+
...(params.actorExternalId
|
|
271
|
+
? { actorExternalId: params.actorExternalId }
|
|
272
|
+
: {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
203
276
|
/**
|
|
204
277
|
* Periodically retry failed channel inbound events that have passed
|
|
205
278
|
* their exponential backoff delay.
|
|
@@ -428,6 +501,26 @@ export async function sweepFailedEvents(
|
|
|
428
501
|
sourceMetadata,
|
|
429
502
|
externalMessageId,
|
|
430
503
|
});
|
|
504
|
+
// Empty strings fall through to the username, mirroring the live
|
|
505
|
+
// `actorDisplayName ?? actorUsername` derivation (`"" ?? x` would not).
|
|
506
|
+
const storedSenderName =
|
|
507
|
+
typeof payload.senderName === "string" && payload.senderName
|
|
508
|
+
? payload.senderName
|
|
509
|
+
: undefined;
|
|
510
|
+
const storedSenderUsername =
|
|
511
|
+
typeof payload.senderUsername === "string" && payload.senderUsername
|
|
512
|
+
? payload.senderUsername
|
|
513
|
+
: undefined;
|
|
514
|
+
const replayChannelInbound =
|
|
515
|
+
parseStoredChannelInbound(payload.channelInbound) ??
|
|
516
|
+
buildReplayChannelInbound({
|
|
517
|
+
sourceChannel,
|
|
518
|
+
externalChatId,
|
|
519
|
+
sourceMetadata,
|
|
520
|
+
externalMessageId,
|
|
521
|
+
senderDisplayName: storedSenderName ?? storedSenderUsername,
|
|
522
|
+
actorExternalId: trustContext.requesterExternalUserId,
|
|
523
|
+
});
|
|
431
524
|
// The captured `slackInbound` carries the sender's Slack `app_context`, so
|
|
432
525
|
// the replayed turn is prepared with the same context block the live turn
|
|
433
526
|
// rendered. Payloads predating the capture simply have none.
|
|
@@ -462,6 +555,10 @@ export async function sweepFailedEvents(
|
|
|
462
555
|
...(prepared.displayContent !== undefined
|
|
463
556
|
? { displayContent: prepared.displayContent }
|
|
464
557
|
: {}),
|
|
558
|
+
// Not idempotency-bearing (the derived key reads only `slackInbound`),
|
|
559
|
+
// so it is shared with the incomplete-turn re-run below: the completing
|
|
560
|
+
// row should carry the same `providerMeta` a live turn would.
|
|
561
|
+
...(replayChannelInbound ? { channelInbound: replayChannelInbound } : {}),
|
|
465
562
|
};
|
|
466
563
|
|
|
467
564
|
let userMessageId: string | undefined;
|
|
@@ -544,6 +641,10 @@ export async function sweepFailedEvents(
|
|
|
544
641
|
{ eventId: event.id, conversationId: event.conversationId },
|
|
545
642
|
"Skipping retry delivery: a sibling event owns delivery for the deduplicated turn",
|
|
546
643
|
);
|
|
644
|
+
// The sibling owns delivery, so settle this row rather than leaving it
|
|
645
|
+
// `pending`, which stranded-delivery recovery reads as delivery still
|
|
646
|
+
// owed and would act on after a restart.
|
|
647
|
+
markDeliveryDelivered(event.id);
|
|
547
648
|
} else if (replyCallbackUrl && externalChatId) {
|
|
548
649
|
try {
|
|
549
650
|
await finalizeEventDelivery({
|
|
@@ -554,7 +655,7 @@ export async function sweepFailedEvents(
|
|
|
554
655
|
assistantId,
|
|
555
656
|
replyMessageId,
|
|
556
657
|
userMessageId,
|
|
557
|
-
|
|
658
|
+
replySession: undefined,
|
|
558
659
|
priorStreamMessageTs,
|
|
559
660
|
});
|
|
560
661
|
} catch (err) {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
recordDeliveryFailure,
|
|
5
5
|
} from "../persistence/delivery-status.js";
|
|
6
6
|
import { deliverReplyViaCallback } from "./channel-reply-delivery.js";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ChannelReplySession } from "./channel-reply-session.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Owns the complete delivery-after-processing sequence for a channel
|
|
@@ -26,7 +26,7 @@ export async function finalizeEventDelivery(params: {
|
|
|
26
26
|
assistantId: string | undefined;
|
|
27
27
|
replyMessageId: string | undefined;
|
|
28
28
|
userMessageId: string | undefined;
|
|
29
|
-
|
|
29
|
+
replySession: ChannelReplySession | undefined;
|
|
30
30
|
/**
|
|
31
31
|
* `ts` of a Slack message streamed on a previous, failed attempt. A retry
|
|
32
32
|
* has no live stream of its own, so it edits this message in place rather
|
|
@@ -42,11 +42,11 @@ export async function finalizeEventDelivery(params: {
|
|
|
42
42
|
assistantId,
|
|
43
43
|
replyMessageId,
|
|
44
44
|
userMessageId,
|
|
45
|
-
|
|
45
|
+
replySession,
|
|
46
46
|
priorStreamMessageTs,
|
|
47
47
|
} = params;
|
|
48
48
|
|
|
49
|
-
const reconciliation = await
|
|
49
|
+
const reconciliation = await replySession?.finish();
|
|
50
50
|
|
|
51
51
|
// A streamed reply already delivered its text live into a single message;
|
|
52
52
|
// durable delivery skips that text, reconciles `slackMeta.channelTs` to the
|
|
@@ -54,10 +54,7 @@ export function includesRequiredKeywords(
|
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
return requiredKeywords.every((keyword) => {
|
|
57
|
-
const re = new RegExp(
|
|
58
|
-
`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
59
|
-
"i",
|
|
60
|
-
);
|
|
57
|
+
const re = new RegExp(`\\b${RegExp.escape(keyword)}\\b`, "i");
|
|
61
58
|
return re.test(text);
|
|
62
59
|
});
|
|
63
60
|
}
|