@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
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
nonEmpty,
|
|
21
21
|
sanitizeIdentityField,
|
|
22
22
|
sanitizeMessagePreview,
|
|
23
|
+
stripReplyMechanicsFromCopy,
|
|
24
|
+
stripRequestCodeDirectives,
|
|
23
25
|
} from "./notification-utils.js";
|
|
26
|
+
import type { RenderedChannelCopy } from "./types.js";
|
|
24
27
|
|
|
25
28
|
// ── Zod schema for access-request payloads ──────────────────────────────────
|
|
26
29
|
|
|
@@ -150,7 +153,7 @@ export function isHandshakeOfferedForPayload(
|
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
/** Internal typed implementation — avoids re-parsing when called from
|
|
153
|
-
*
|
|
156
|
+
* buildAccessRequestContextText which has already parsed the payload. */
|
|
154
157
|
function buildIdentityLineFromParsed(p: ParsedAccessRequestPayload): string {
|
|
155
158
|
const requester = sanitizeIdentityField(p.senderIdentifier || "Someone");
|
|
156
159
|
const callerName = nonEmpty(p.actorDisplayName);
|
|
@@ -235,112 +238,25 @@ export function buildAccessRequestInviteDirective(): string {
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/**
|
|
238
|
-
*
|
|
241
|
+
* Guardian-facing context for an access request: who is asking, what they
|
|
242
|
+
* said, any warnings, where it came from, and the invite-flow directive.
|
|
243
|
+
* The invite directive is context rather than mechanics because no surface
|
|
244
|
+
* offers a button for it: typing "open invite flow" is the only way to start
|
|
245
|
+
* it anywhere, so every surface has to say so. The request-code directive
|
|
246
|
+
* is {@link buildAccessRequestReplyMechanics}, and only the broadcaster's
|
|
247
|
+
* `plainTextFallback` holds it, so a transport appends it exactly when it
|
|
248
|
+
* sends text without buttons.
|
|
239
249
|
*
|
|
240
|
-
* -
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
250
|
+
* Channel-agnostic by design: this reads the generic `contextPayload` and
|
|
251
|
+
* renders the same on every channel. When `guardianResolutionSource` is
|
|
252
|
+
* present and not `"source-channel-contact"`, the guardian was resolved via
|
|
253
|
+
* fallback (e.g. vellum anchor) rather than a verified same-channel contact,
|
|
254
|
+
* and the copy says so.
|
|
245
255
|
*/
|
|
246
|
-
export function
|
|
247
|
-
return text
|
|
248
|
-
.replace(/[\u2018\u2019\u201B]/g, "'")
|
|
249
|
-
.replace(/\s+/g, " ")
|
|
250
|
-
.trim();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Build a negated-lookbehind directive regex for `reply ... "CODE <verb>"`. */
|
|
254
|
-
function buildCodeDirectiveRegex(escapedCode: string, verb: string): RegExp {
|
|
255
|
-
return new RegExp(
|
|
256
|
-
`(?<!not\\s)(?<!n't\\s)(?<!never\\s)reply\\b[^.!?\\n]*?"${escapedCode}\\s+${verb}"`,
|
|
257
|
-
"i",
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Check whether a text contains the required access-request instruction
|
|
263
|
-
* elements for the introduction card:
|
|
264
|
-
* 1. Trust directive: Reply "CODE trust"
|
|
265
|
-
* 2. Verify directive: Reply "CODE verify" — only when the handshake is
|
|
266
|
-
* offered for this requester (never for bots / workspace-vouched members)
|
|
267
|
-
* 3. Leave-unverified directive: Reply "CODE reject"
|
|
268
|
-
* 4. Block directive: Reply "CODE block"
|
|
269
|
-
* 5. Invite directive: Reply "open invite flow"
|
|
270
|
-
*
|
|
271
|
-
* Each directive is matched independently using negative lookbehind to reject
|
|
272
|
-
* matches preceded by negation words ("not", "n't", "never"). This prevents
|
|
273
|
-
* contradictory copy like `Do not reply "CODE reject"` from satisfying the
|
|
274
|
-
* check even when a positive directive exists nearby.
|
|
275
|
-
*
|
|
276
|
-
* The text is normalized before matching to handle smart apostrophes and
|
|
277
|
-
* multiple whitespace characters that would otherwise bypass negation detection.
|
|
278
|
-
*/
|
|
279
|
-
export function hasAccessRequestInstructions(
|
|
280
|
-
text: string | undefined,
|
|
281
|
-
requestCode: string,
|
|
282
|
-
options?: { handshakeOffered?: boolean },
|
|
283
|
-
): boolean {
|
|
284
|
-
if (typeof text !== "string") {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
const handshakeOffered = options?.handshakeOffered ?? true;
|
|
288
|
-
const normalized = normalizeForDirectiveMatching(text);
|
|
289
|
-
const escapedCode = requestCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
290
|
-
// Each directive must follow "reply" without a preceding negation word.
|
|
291
|
-
// Negative lookbehinds reject "do not reply", "don't reply", "never reply".
|
|
292
|
-
const trustRe = buildCodeDirectiveRegex(escapedCode, "trust");
|
|
293
|
-
const verifyRe = buildCodeDirectiveRegex(escapedCode, "verify");
|
|
294
|
-
const rejectRe = buildCodeDirectiveRegex(escapedCode, "reject");
|
|
295
|
-
const blockRe = buildCodeDirectiveRegex(escapedCode, "block");
|
|
296
|
-
const inviteRe =
|
|
297
|
-
/(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
trustRe.test(normalized) &&
|
|
301
|
-
(!handshakeOffered || verifyRe.test(normalized)) &&
|
|
302
|
-
rejectRe.test(normalized) &&
|
|
303
|
-
blockRe.test(normalized) &&
|
|
304
|
-
inviteRe.test(normalized)
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Check whether text contains the invite-flow directive ("open invite flow")
|
|
310
|
-
* using the same normalized negative-lookbehind pattern as the full check.
|
|
311
|
-
* This is used for enforcement when requestCode is absent but the invite
|
|
312
|
-
* directive should still be present.
|
|
313
|
-
*/
|
|
314
|
-
export function hasInviteFlowDirective(text: string | undefined): boolean {
|
|
315
|
-
if (typeof text !== "string") {
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
const normalized = normalizeForDirectiveMatching(text);
|
|
319
|
-
const inviteRe =
|
|
320
|
-
/(?<!not\s)(?<!n't\s)(?<!never\s)reply\b[^.!?\n]*?"open invite flow"/i;
|
|
321
|
-
return inviteRe.test(normalized);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ── Contract text builder ───────────────────────────────────────────────────
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Build the deterministic access-request contract text from payload fields.
|
|
328
|
-
* This is the canonical baseline that enforcement can append when generated
|
|
329
|
-
* copy is missing required elements.
|
|
330
|
-
*
|
|
331
|
-
* Channel-agnostic by design: this function reads from the generic
|
|
332
|
-
* `contextPayload` and works identically regardless of which channel
|
|
333
|
-
* (Slack, Telegram, desktop, etc.) the notification is delivered to.
|
|
334
|
-
* When `guardianResolutionSource` is present and not `"source-channel-contact"`,
|
|
335
|
-
* the guardian was resolved via fallback (e.g. vellum anchor) rather than
|
|
336
|
-
* a verified same-channel contact — downstream copy or routing can use
|
|
337
|
-
* this to append verification CTAs like "Was this you?".
|
|
338
|
-
*/
|
|
339
|
-
export function buildAccessRequestContractText(
|
|
256
|
+
export function buildAccessRequestContextText(
|
|
340
257
|
payload: Record<string, unknown>,
|
|
341
258
|
): string {
|
|
342
259
|
const p = parseAccessRequestPayload(payload);
|
|
343
|
-
const requestCode = nonEmpty(p.requestCode);
|
|
344
260
|
|
|
345
261
|
const lines: string[] = [];
|
|
346
262
|
lines.push(buildIdentityLineFromParsed(p));
|
|
@@ -371,18 +287,6 @@ export function buildAccessRequestContractText(
|
|
|
371
287
|
lines.push(source);
|
|
372
288
|
}
|
|
373
289
|
|
|
374
|
-
if (requestCode) {
|
|
375
|
-
const code = requestCode.toUpperCase();
|
|
376
|
-
if (isHandshakeOfferedForPayload(p)) {
|
|
377
|
-
lines.push(
|
|
378
|
-
`Reply "${code} verify" to send them a verification code, "${code} trust" to trust them without one, "${code} reject" to leave them unverified, or "${code} block" to block them.`,
|
|
379
|
-
);
|
|
380
|
-
} else {
|
|
381
|
-
lines.push(
|
|
382
|
-
`Reply "${code} trust" to trust them, "${code} reject" to leave them unverified, or "${code} block" to block them.`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
290
|
lines.push(buildAccessRequestInviteDirective());
|
|
387
291
|
|
|
388
292
|
if (
|
|
@@ -397,6 +301,114 @@ export function buildAccessRequestContractText(
|
|
|
397
301
|
return lines.join("\n");
|
|
398
302
|
}
|
|
399
303
|
|
|
304
|
+
/**
|
|
305
|
+
* The typed-reply mechanics for an access request: the request-code
|
|
306
|
+
* directive (verify, trust, reject, block; or trust, reject, block when no
|
|
307
|
+
* handshake is offered). This is the broadcaster's `plainTextFallback` for
|
|
308
|
+
* the card, appended to a message only by a transport that sends text
|
|
309
|
+
* without buttons. Empty when the request carries no code, since there is
|
|
310
|
+
* then nothing to type.
|
|
311
|
+
*/
|
|
312
|
+
export function buildAccessRequestReplyMechanics(
|
|
313
|
+
payload: Record<string, unknown>,
|
|
314
|
+
): string {
|
|
315
|
+
const p = parseAccessRequestPayload(payload);
|
|
316
|
+
const requestCode = nonEmpty(p.requestCode);
|
|
317
|
+
if (!requestCode) {
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
const code = requestCode.toUpperCase();
|
|
321
|
+
return isHandshakeOfferedForPayload(p)
|
|
322
|
+
? `Reply "${code} verify" to send them a verification code, "${code} trust" to trust them without one, "${code} reject" to leave them unverified, or "${code} block" to block them.`
|
|
323
|
+
: `Reply "${code} trust" to trust them, "${code} reject" to leave them unverified, or "${code} block" to block them.`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Remove request-code reply mechanics the model wrote into composed copy
|
|
328
|
+
* ({@link stripRequestCodeDirectives}). The invite-flow directive is context
|
|
329
|
+
* and stays.
|
|
330
|
+
*/
|
|
331
|
+
export function stripAccessRequestReplyMechanics(
|
|
332
|
+
text: string,
|
|
333
|
+
payload: Record<string, unknown>,
|
|
334
|
+
): string {
|
|
335
|
+
const requestCode = nonEmpty(parseAccessRequestPayload(payload).requestCode);
|
|
336
|
+
return requestCode ? stripRequestCodeDirectives(text, requestCode) : text;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The text-only rendering of an access request: the context, then the
|
|
341
|
+
* typed directive when the request carries a code. This is the card's text
|
|
342
|
+
* sibling, what a client without buttons (the CLI, search, the model) sees
|
|
343
|
+
* in place of the card.
|
|
344
|
+
*/
|
|
345
|
+
export function buildAccessRequestTextFallback(
|
|
346
|
+
payload: Record<string, unknown>,
|
|
347
|
+
): string {
|
|
348
|
+
const mechanics = buildAccessRequestReplyMechanics(payload);
|
|
349
|
+
const context = buildAccessRequestContextText(payload);
|
|
350
|
+
return mechanics ? `${context}\n${mechanics}` : context;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Whether text carries a positive invite-flow directive: a "reply ..."
|
|
355
|
+
* sentence naming the phrase, not preceded by a negation. A bare mention
|
|
356
|
+
* ("the open invite flow is disabled") or a negated form ("do not reply
|
|
357
|
+
* ...") does not count, so the canonical directive still gets appended.
|
|
358
|
+
*/
|
|
359
|
+
export function hasInviteFlowDirective(text: string): boolean {
|
|
360
|
+
const normalized = text
|
|
361
|
+
.replace(/[\u2018\u2019\u201B]/g, "'")
|
|
362
|
+
.replace(/\s+/g, " ");
|
|
363
|
+
return /(?<!not )(?<!n't )(?<!never )reply\b[^.!?\n]*?"open invite flow"/i.test(
|
|
364
|
+
normalized,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Ensure the invite-flow directive is in every text field of a channel's
|
|
370
|
+
* access-request copy. It is context rather than mechanics (no surface has
|
|
371
|
+
* an invite button, so the sentence is the only way to start the flow), and
|
|
372
|
+
* model-composed copy can omit, negate, or merely mention it, so the
|
|
373
|
+
* canonical sentence is appended whenever no positive directive is present.
|
|
374
|
+
* A title stays a title.
|
|
375
|
+
*/
|
|
376
|
+
export function ensureAccessRequestInviteDirectiveInCopy(
|
|
377
|
+
copy: RenderedChannelCopy,
|
|
378
|
+
): RenderedChannelCopy {
|
|
379
|
+
const ensure = (text: string): string =>
|
|
380
|
+
hasInviteFlowDirective(text)
|
|
381
|
+
? text
|
|
382
|
+
: `${text.trim()}\n${buildAccessRequestInviteDirective()}`;
|
|
383
|
+
return {
|
|
384
|
+
...copy,
|
|
385
|
+
body: ensure(copy.body),
|
|
386
|
+
deliveryText: copy.deliveryText
|
|
387
|
+
? ensure(copy.deliveryText)
|
|
388
|
+
: copy.deliveryText,
|
|
389
|
+
conversationSeedMessage: copy.conversationSeedMessage
|
|
390
|
+
? ensure(copy.conversationSeedMessage)
|
|
391
|
+
: copy.conversationSeedMessage,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* {@link stripAccessRequestReplyMechanics} over every text field of a
|
|
397
|
+
* channel's copy; a field left empty becomes the requester context.
|
|
398
|
+
*/
|
|
399
|
+
export function stripAccessRequestReplyMechanicsFromCopy(
|
|
400
|
+
copy: RenderedChannelCopy,
|
|
401
|
+
payload: Record<string, unknown>,
|
|
402
|
+
): RenderedChannelCopy {
|
|
403
|
+
const requestCode = nonEmpty(parseAccessRequestPayload(payload).requestCode);
|
|
404
|
+
return stripReplyMechanicsFromCopy(
|
|
405
|
+
copy,
|
|
406
|
+
(text) =>
|
|
407
|
+
requestCode ? stripRequestCodeDirectives(text, requestCode) : text,
|
|
408
|
+
buildAccessRequestContextText(payload),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
400
412
|
// ── Card view model ─────────────────────────────────────────────────────────
|
|
401
413
|
|
|
402
414
|
/**
|
|
@@ -31,7 +31,11 @@ import type {
|
|
|
31
31
|
DeliveryResult,
|
|
32
32
|
NotificationChannel,
|
|
33
33
|
} from "../types.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
appendPlainTextFallback,
|
|
36
|
+
rendersActions,
|
|
37
|
+
resolveMessageText,
|
|
38
|
+
} from "./shared.js";
|
|
35
39
|
|
|
36
40
|
const log = getLogger("notif-adapter-discord");
|
|
37
41
|
|
|
@@ -60,7 +64,7 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
60
64
|
try {
|
|
61
65
|
const channelId = await openDiscordDmChannel(guardianUserId);
|
|
62
66
|
|
|
63
|
-
if (approval) {
|
|
67
|
+
if (rendersActions(approval)) {
|
|
64
68
|
// Attempt rich delivery with component buttons; on failure, fall
|
|
65
69
|
// back to the plain-text card below.
|
|
66
70
|
try {
|
|
@@ -74,7 +78,7 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
74
78
|
"Discord approval notification delivered with buttons",
|
|
75
79
|
);
|
|
76
80
|
// The message id lets the delivery row address the card later
|
|
77
|
-
// (
|
|
81
|
+
// (button presses, in-place withdrawal).
|
|
78
82
|
return { success: true, messageId: sent.lastMessageId };
|
|
79
83
|
} catch (richErr) {
|
|
80
84
|
if (richErr instanceof DiscordPartialSendError) {
|
|
@@ -108,8 +112,8 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
115
|
+
// Text without buttons carries the typed-reply instructions, whether
|
|
116
|
+
// the rich delivery failed or the request never had buttons to draw.
|
|
113
117
|
const sent = await sendDiscordReply(
|
|
114
118
|
{ channelId },
|
|
115
119
|
appendPlainTextFallback(messageText, approval),
|
|
@@ -35,10 +35,23 @@ export function resolveMessageText(payload: ChannelDeliveryPayload): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* Whether an approval context has buttons to draw. A context with no actions
|
|
39
|
+
* (an option-less question) is answered by typed reply, so it takes the
|
|
40
|
+
* plain-text path with {@link appendPlainTextFallback} rather than a card
|
|
41
|
+
* with nothing on it.
|
|
42
|
+
*/
|
|
43
|
+
export function rendersActions(
|
|
44
|
+
approval: ChannelDeliveryPayload["approvalContext"],
|
|
45
|
+
): approval is NonNullable<ChannelDeliveryPayload["approvalContext"]> {
|
|
46
|
+
return approval != null && approval.actions.length > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Append an approval's typed-reply instructions to the message text, when
|
|
51
|
+
* the text does not already carry them. This is the only place reply
|
|
52
|
+
* mechanics join a message: composed copy never carries them, so an adapter
|
|
53
|
+
* sending text without live buttons (no actions to draw, or a rich delivery
|
|
54
|
+
* that failed) appends them here and the guardian still knows how to answer.
|
|
42
55
|
*/
|
|
43
56
|
export function appendPlainTextFallback(
|
|
44
57
|
text: string,
|
|
@@ -39,7 +39,11 @@ import type {
|
|
|
39
39
|
DeliveryResult,
|
|
40
40
|
NotificationChannel,
|
|
41
41
|
} from "../types.js";
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
appendPlainTextFallback,
|
|
44
|
+
rendersActions,
|
|
45
|
+
resolveMessageText,
|
|
46
|
+
} from "./shared.js";
|
|
43
47
|
|
|
44
48
|
const log = getLogger("notif-adapter-slack");
|
|
45
49
|
|
|
@@ -444,13 +448,19 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
444
448
|
// `approval` rides along with the prebuilt card blocks so the send
|
|
445
449
|
// layer treats a rejected Block Kit payload as an approval prompt:
|
|
446
450
|
// its block-free retry re-attaches `plainTextFallback` reply
|
|
447
|
-
// instructions instead of posting text with no way to respond.
|
|
448
|
-
|
|
451
|
+
// instructions instead of posting text with no way to respond. A
|
|
452
|
+
// context with no buttons to draw is answered by typed reply, so its
|
|
453
|
+
// instructions join the text up front.
|
|
454
|
+
const result = rendersActions(payload.approvalContext)
|
|
449
455
|
? await sendSlackReply(chatId, messageText, {
|
|
450
456
|
blocks: buildApprovalNotificationBlocks(payload, messageText),
|
|
451
457
|
approval: payload.approvalContext,
|
|
452
458
|
})
|
|
453
|
-
: await sendSlackReply(
|
|
459
|
+
: await sendSlackReply(
|
|
460
|
+
chatId,
|
|
461
|
+
appendPlainTextFallback(messageText, payload.approvalContext),
|
|
462
|
+
{ useBlocks: true },
|
|
463
|
+
);
|
|
454
464
|
|
|
455
465
|
log.info(
|
|
456
466
|
{ sourceEventName: payload.sourceEventName, chatId, ts: result.ts },
|
|
@@ -23,7 +23,11 @@ import type {
|
|
|
23
23
|
DeliveryResult,
|
|
24
24
|
NotificationChannel,
|
|
25
25
|
} from "../types.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
appendPlainTextFallback,
|
|
28
|
+
rendersActions,
|
|
29
|
+
resolveMessageText,
|
|
30
|
+
} from "./shared.js";
|
|
27
31
|
|
|
28
32
|
const log = getLogger("notif-adapter-telegram");
|
|
29
33
|
|
|
@@ -50,7 +54,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
50
54
|
const approval = payload.approvalContext;
|
|
51
55
|
|
|
52
56
|
try {
|
|
53
|
-
if (approval) {
|
|
57
|
+
if (rendersActions(approval)) {
|
|
54
58
|
// Attempt rich delivery with inline keyboard buttons.
|
|
55
59
|
// On failure, fall back to plain text below.
|
|
56
60
|
try {
|
|
@@ -72,8 +76,8 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
79
|
+
// Text without buttons carries the typed-reply instructions, whether
|
|
80
|
+
// the rich delivery failed or the request never had buttons to draw.
|
|
77
81
|
const sent = await sendTelegramReply(
|
|
78
82
|
chatId,
|
|
79
83
|
appendPlainTextFallback(messageText, approval),
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
accessRequestCardSubtitle,
|
|
18
18
|
accessRequestCardTitle,
|
|
19
19
|
buildAccessRequestCardView,
|
|
20
|
-
|
|
20
|
+
buildAccessRequestTextFallback,
|
|
21
21
|
buildIntroductionActionsForPayload,
|
|
22
22
|
parseAccessRequestPayload,
|
|
23
23
|
} from "./access-request-copy.js";
|
|
@@ -238,7 +238,10 @@ function resolveAccessRequestCard(
|
|
|
238
238
|
metadata,
|
|
239
239
|
requestId: view.requestId,
|
|
240
240
|
actions,
|
|
241
|
-
|
|
241
|
+
// The card's text sibling is what a client without buttons sees (the
|
|
242
|
+
// CLI, search, the model), so it is the one place the card itself
|
|
243
|
+
// carries the typed directive beside the context.
|
|
244
|
+
fallbackText: buildAccessRequestTextFallback(payload),
|
|
242
245
|
};
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
describeMedia,
|
|
37
37
|
mediaEmbeds,
|
|
38
38
|
sanitizeMessagePreview,
|
|
39
|
+
sanitizeMultilineMessagePreview,
|
|
39
40
|
sanitizeNotificationTitle,
|
|
40
41
|
stripMarkdownForPreview,
|
|
41
42
|
} from "./notification-utils.js";
|
|
@@ -50,10 +51,10 @@ const DESKTOP_PRESENCE_FLAG = "desktop-presence-suppression" as const;
|
|
|
50
51
|
const WEB_PRESENCE_FLAG = "web-presence-suppression" as const;
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
54
|
+
* Flatten a title onto one line. Notification titles cannot wrap, and
|
|
55
|
+
* `normalizeTitle` discards any string that still contains a newline.
|
|
55
56
|
*/
|
|
56
|
-
function
|
|
57
|
+
function flattenTitleWhitespace(value: string): string {
|
|
57
58
|
return value.replace(/\s+/g, " ").trim();
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -229,9 +230,8 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
229
230
|
// text nor media stays silent.
|
|
230
231
|
const text = stringifyMessageContent(assistantRow.content);
|
|
231
232
|
const preview =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
) || sanitizeMessagePreview(describeReplyMedia(text, assistantMessageId));
|
|
233
|
+
sanitizeMultilineMessagePreview(stripMarkdownForPreview(text)) ||
|
|
234
|
+
sanitizeMessagePreview(describeReplyMedia(text, assistantMessageId));
|
|
235
235
|
if (!preview) {
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
@@ -242,7 +242,7 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
242
242
|
// title from the body, which reads better than an empty or placeholder
|
|
243
243
|
// conversation title.
|
|
244
244
|
const requestedTitle = sanitizeNotificationTitle(
|
|
245
|
-
|
|
245
|
+
flattenTitleWhitespace(conversation.title ?? ""),
|
|
246
246
|
);
|
|
247
247
|
|
|
248
248
|
// Read as close to the emit as possible: nothing short-circuits on it.
|
|
@@ -16,7 +16,7 @@ import { getConversation } from "../persistence/conversation-crud.js";
|
|
|
16
16
|
import type { ApprovalUIMetadata } from "../runtime/channel-approval-types.js";
|
|
17
17
|
import { getLogger } from "../util/logger.js";
|
|
18
18
|
import {
|
|
19
|
-
|
|
19
|
+
buildAccessRequestReplyMechanics,
|
|
20
20
|
buildIntroductionActionsForPayload,
|
|
21
21
|
parseAccessRequestPayload,
|
|
22
22
|
} from "./access-request-copy.js";
|
|
@@ -33,14 +33,16 @@ import {
|
|
|
33
33
|
} from "./deliveries-store.js";
|
|
34
34
|
import { resolveDestinations } from "./destination-resolver.js";
|
|
35
35
|
import {
|
|
36
|
+
buildGuardianRequestCodeInstruction,
|
|
36
37
|
buildQuestionAnswerActions,
|
|
37
38
|
buildToolApprovalSourceView,
|
|
38
39
|
parseGuardianQuestionPayload,
|
|
39
40
|
parseInteractiveApprovalPayload,
|
|
40
41
|
resolveGuardianInstructionModeFromPayload,
|
|
42
|
+
resolveGuardianQuestionInstructionMode,
|
|
41
43
|
type ToolApprovalSourceView,
|
|
42
44
|
} from "./guardian-question-mode.js";
|
|
43
|
-
import { nonEmpty } from "./notification-utils.js";
|
|
45
|
+
import { nonEmpty, readPayloadString } from "./notification-utils.js";
|
|
44
46
|
import type { NotificationSignal } from "./signal.js";
|
|
45
47
|
import type {
|
|
46
48
|
ChannelAdapter,
|
|
@@ -97,25 +99,25 @@ function resolveApprovalContext(
|
|
|
97
99
|
actions: buildIntroductionActionsForPayload(
|
|
98
100
|
parseAccessRequestPayload(payload),
|
|
99
101
|
),
|
|
100
|
-
plainTextFallback:
|
|
102
|
+
plainTextFallback: buildAccessRequestReplyMechanics(payload),
|
|
101
103
|
},
|
|
102
104
|
};
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
if (signal.sourceEventName === "guardian.question") {
|
|
106
|
-
// Answer-mode question
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// rendering-only; the reply router recognizes the action
|
|
110
|
-
// selections (see parseQuestionAnswerActionId).
|
|
111
|
-
const questionContext =
|
|
108
|
+
// Answer-mode question: options render as card actions so every channel
|
|
109
|
+
// adapter shows tappable choices, and an option-less question carries
|
|
110
|
+
// its typed-reply instruction. Built here, once per broadcast, so
|
|
111
|
+
// adapters stay rendering-only; the reply router recognizes the action
|
|
112
|
+
// ids as answer selections (see parseQuestionAnswerActionId).
|
|
113
|
+
const questionContext = resolveQuestionContext(payload);
|
|
112
114
|
if (questionContext) {
|
|
113
115
|
return questionContext;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
const parsed = parseInteractiveApprovalPayload(payload);
|
|
117
119
|
if (!parsed) {
|
|
118
|
-
return
|
|
120
|
+
return resolveCodedTextContext(payload);
|
|
119
121
|
}
|
|
120
122
|
const requestId = parsed.requestId;
|
|
121
123
|
|
|
@@ -139,7 +141,10 @@ function resolveApprovalContext(
|
|
|
139
141
|
approval: {
|
|
140
142
|
requestId,
|
|
141
143
|
actions: APPROVAL_ACTIONS,
|
|
142
|
-
plainTextFallback:
|
|
144
|
+
plainTextFallback: buildGuardianRequestCodeInstruction(
|
|
145
|
+
parsed.requestCode?.toUpperCase() ?? requestId,
|
|
146
|
+
"approval",
|
|
147
|
+
),
|
|
143
148
|
permissionDetails: toolName
|
|
144
149
|
? {
|
|
145
150
|
toolName,
|
|
@@ -157,14 +162,43 @@ function resolveApprovalContext(
|
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* paths.
|
|
165
|
+
* A `guardian.question` payload that fails strict parsing draws no buttons
|
|
166
|
+
* anywhere, so its only way to be answered is the typed reply. When it
|
|
167
|
+
* still names a request id and code, it gets a context with no actions and
|
|
168
|
+
* the instruction for its mode, which the transports render as text plus
|
|
169
|
+
* the instruction. Without those it is undefined, as before.
|
|
166
170
|
*/
|
|
167
|
-
function
|
|
171
|
+
function resolveCodedTextContext(
|
|
172
|
+
payload: Record<string, unknown>,
|
|
173
|
+
): ResolvedApprovalContext | undefined {
|
|
174
|
+
const requestId = nonEmpty(readPayloadString(payload, "requestId"));
|
|
175
|
+
const requestCode = nonEmpty(readPayloadString(payload, "requestCode"));
|
|
176
|
+
if (!requestId || !requestCode) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
approval: {
|
|
181
|
+
requestId,
|
|
182
|
+
actions: [],
|
|
183
|
+
plainTextFallback: buildGuardianRequestCodeInstruction(
|
|
184
|
+
requestCode.toUpperCase(),
|
|
185
|
+
resolveGuardianQuestionInstructionMode(payload).mode,
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The card context for an answer-mode `pending_question`: one action per
|
|
193
|
+
* option plus Skip, ids in the answer-selection scheme, and the typed-reply
|
|
194
|
+
* instruction as the text fallback. An option-less question (a voice
|
|
195
|
+
* question, answered by free text) carries no actions, only the fallback:
|
|
196
|
+
* the transports read an empty action set as "send text, append the
|
|
197
|
+
* instruction", which is the one way that question stays answerable.
|
|
198
|
+
* Returns `undefined` for approval-mode payloads and anything that fails
|
|
199
|
+
* strict parsing; those fall through to the approval path.
|
|
200
|
+
*/
|
|
201
|
+
function resolveQuestionContext(
|
|
168
202
|
payload: Record<string, unknown>,
|
|
169
203
|
): ResolvedApprovalContext | undefined {
|
|
170
204
|
const parsed = parseGuardianQuestionPayload(payload);
|
|
@@ -174,21 +208,19 @@ function resolveQuestionOptionsContext(
|
|
|
174
208
|
if (resolveGuardianInstructionModeFromPayload(parsed).mode !== "answer") {
|
|
175
209
|
return undefined;
|
|
176
210
|
}
|
|
177
|
-
const options = parsed.options;
|
|
178
|
-
if (!options || options.length === 0) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
211
|
const requestId = nonEmpty(parsed.requestId);
|
|
182
212
|
if (!requestId) {
|
|
183
213
|
return undefined;
|
|
184
214
|
}
|
|
185
215
|
|
|
186
|
-
const code = parsed.requestCode?.toUpperCase() ?? requestId;
|
|
187
216
|
return {
|
|
188
217
|
approval: {
|
|
189
218
|
requestId,
|
|
190
|
-
actions: buildQuestionAnswerActions(options),
|
|
191
|
-
plainTextFallback:
|
|
219
|
+
actions: buildQuestionAnswerActions(parsed.options ?? []),
|
|
220
|
+
plainTextFallback: buildGuardianRequestCodeInstruction(
|
|
221
|
+
parsed.requestCode?.toUpperCase() ?? requestId,
|
|
222
|
+
"answer",
|
|
223
|
+
),
|
|
192
224
|
intent: "question",
|
|
193
225
|
},
|
|
194
226
|
};
|