@vellumai/assistant 0.11.8 → 0.11.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -23,11 +23,13 @@
|
|
|
23
23
|
import type { ConversationStrategy } from "../channels/config.js";
|
|
24
24
|
import { getConversationStrategy } from "../channels/config.js";
|
|
25
25
|
import type { ChannelId } from "../channels/types.js";
|
|
26
|
+
import { isAssistantInitiatedThreadsEnabled } from "../config/assistant-initiated-threads-gate.js";
|
|
26
27
|
import {
|
|
27
28
|
addMessage,
|
|
28
29
|
createConversation,
|
|
29
30
|
getConversation,
|
|
30
31
|
} from "../persistence/conversation-crud.js";
|
|
32
|
+
import { ASSISTANT_INITIATED_SOURCE } from "../persistence/conversation-types.js";
|
|
31
33
|
import {
|
|
32
34
|
getBindingByChannelChat,
|
|
33
35
|
upsertOutboundBinding,
|
|
@@ -106,13 +108,77 @@ export interface PairingOptions {
|
|
|
106
108
|
* Errors are caught and logged — this function never throws so the
|
|
107
109
|
* notification pipeline is not disrupted by pairing failures.
|
|
108
110
|
*/
|
|
109
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The event the assistant emits when it has a thought worth the user's time
|
|
113
|
+
* ("assistant.share" - the notifications skill's default), as opposed to a
|
|
114
|
+
* transactional request or a system alert.
|
|
115
|
+
*/
|
|
116
|
+
const ASSISTANT_SHARE_EVENT = "assistant.share";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Promote a background share into an assistant-initiated thread, under the
|
|
120
|
+
* `assistant-initiated-threads` flag.
|
|
121
|
+
*
|
|
122
|
+
* The heartbeat's "have a thought, share it" path emits an assistant.share
|
|
123
|
+
* signal from its own background conversation, and the passive-vellum rule
|
|
124
|
+
* below would append the body there - a row the sidebar never shows. When the
|
|
125
|
+
* section exists, that share is exactly what it is for, so the signal is
|
|
126
|
+
* rewritten to materialize a fresh standard conversation stamped
|
|
127
|
+
* {@link ASSISTANT_INITIATED_SOURCE}, which is the section's membership mark.
|
|
128
|
+
*
|
|
129
|
+
* Deliberately narrow, in every direction it can be:
|
|
130
|
+
* - vellum channel only: other channels deliver the share as a native
|
|
131
|
+
* message and need no in-app thread;
|
|
132
|
+
* - assistant.share only: transactional events keep their own pairing
|
|
133
|
+
* rules, and their threads stay out of the section by source;
|
|
134
|
+
* - only when the producing conversation is a background/scheduled run (or
|
|
135
|
+
* nothing resolvable): a share emitted from inside a user-facing thread
|
|
136
|
+
* keeps the append, since the user is already looking at that thread;
|
|
137
|
+
* - never over an explicit `conversationMetadata.source` or an existing
|
|
138
|
+
* `requiresConversation`: a producer that declared its own filing wins.
|
|
139
|
+
*
|
|
140
|
+
* Flag off, the signal passes through untouched and shares keep the passive
|
|
141
|
+
* append: nothing changes for anyone outside the rollout.
|
|
142
|
+
*/
|
|
143
|
+
function withAssistantInitiatedThread(
|
|
110
144
|
signal: NotificationSignal,
|
|
111
145
|
channel: NotificationChannel,
|
|
146
|
+
): NotificationSignal {
|
|
147
|
+
if (
|
|
148
|
+
channel !== "vellum" ||
|
|
149
|
+
signal.sourceEventName !== ASSISTANT_SHARE_EVENT ||
|
|
150
|
+
signal.requiresConversation === true ||
|
|
151
|
+
signal.conversationMetadata?.source !== undefined ||
|
|
152
|
+
!isAssistantInitiatedThreadsEnabled()
|
|
153
|
+
) {
|
|
154
|
+
return signal;
|
|
155
|
+
}
|
|
156
|
+
const producing = getConversation(signal.sourceContextId);
|
|
157
|
+
if (
|
|
158
|
+
producing &&
|
|
159
|
+
producing.conversationType !== "background" &&
|
|
160
|
+
producing.conversationType !== "scheduled"
|
|
161
|
+
) {
|
|
162
|
+
return signal;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...signal,
|
|
166
|
+
requiresConversation: true,
|
|
167
|
+
conversationMetadata: {
|
|
168
|
+
...signal.conversationMetadata,
|
|
169
|
+
source: ASSISTANT_INITIATED_SOURCE,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function pairDeliveryWithConversation(
|
|
175
|
+
rawSignal: NotificationSignal,
|
|
176
|
+
channel: NotificationChannel,
|
|
112
177
|
copy: RenderedChannelCopy,
|
|
113
178
|
options?: PairingOptions,
|
|
114
179
|
): Promise<PairingResult> {
|
|
115
180
|
try {
|
|
181
|
+
const signal = withAssistantInitiatedThread(rawSignal, channel);
|
|
116
182
|
const strategy = getConversationStrategy(channel as ChannelId);
|
|
117
183
|
|
|
118
184
|
if (strategy === "not_deliverable" || strategy === "push_only") {
|
|
@@ -490,7 +556,7 @@ export async function pairDeliveryWithConversation(
|
|
|
490
556
|
};
|
|
491
557
|
} catch (err) {
|
|
492
558
|
log.error(
|
|
493
|
-
{ err, signalId:
|
|
559
|
+
{ err, signalId: rawSignal.signalId, channel },
|
|
494
560
|
"Failed to pair notification delivery with conversation — continuing without pairing",
|
|
495
561
|
);
|
|
496
562
|
const fallbackStrategy = (() => {
|
|
@@ -13,7 +13,7 @@ import { normalizeTitle } from "../util/short-title.js";
|
|
|
13
13
|
import { truncate } from "../util/truncate.js";
|
|
14
14
|
import {
|
|
15
15
|
accessRequestCardTitle,
|
|
16
|
-
|
|
16
|
+
buildAccessRequestContextText,
|
|
17
17
|
isAdmittedIntroduction,
|
|
18
18
|
} from "./access-request-copy.js";
|
|
19
19
|
import {
|
|
@@ -21,10 +21,8 @@ import {
|
|
|
21
21
|
buildToolApprovalSeedContentBlocks,
|
|
22
22
|
} from "./approval-card-data.js";
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
applyGuardianReplyMechanics,
|
|
25
25
|
parseGuardianQuestionPayload,
|
|
26
|
-
resolveGuardianInstructionModeFromPayload,
|
|
27
|
-
resolveGuardianQuestionInstructionMode,
|
|
28
26
|
} from "./guardian-question-mode.js";
|
|
29
27
|
import {
|
|
30
28
|
nonEmpty,
|
|
@@ -231,34 +229,12 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
|
231
229
|
? buildToolApprovalSeedContentBlocks(parsed)
|
|
232
230
|
: buildToolApprovalSeedContentBlocks(payload)) ?? undefined;
|
|
233
231
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
typeof payload.requestCode === "string"
|
|
238
|
-
? payload.requestCode
|
|
239
|
-
: undefined,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
if (!requestCode) {
|
|
243
|
-
return {
|
|
244
|
-
title: "Guardian Question",
|
|
245
|
-
body: question,
|
|
246
|
-
conversationSeedMessage,
|
|
247
|
-
seedContentBlocks,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const normalizedCode = requestCode.toUpperCase();
|
|
252
|
-
const modeResolution = parsed
|
|
253
|
-
? resolveGuardianInstructionModeFromPayload(parsed)
|
|
254
|
-
: resolveGuardianQuestionInstructionMode(payload);
|
|
255
|
-
const instruction = buildGuardianRequestCodeInstruction(
|
|
256
|
-
normalizedCode,
|
|
257
|
-
modeResolution.mode,
|
|
258
|
-
);
|
|
232
|
+
// The request-code reply instruction is not part of the template: whether
|
|
233
|
+
// a channel's copy carries it is decided per channel by
|
|
234
|
+
// `applyGuardianReplyMechanics` when the copy is composed.
|
|
259
235
|
return {
|
|
260
236
|
title: "Guardian Question",
|
|
261
|
-
body:
|
|
237
|
+
body: question,
|
|
262
238
|
conversationSeedMessage,
|
|
263
239
|
seedContentBlocks,
|
|
264
240
|
};
|
|
@@ -275,7 +251,7 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
|
275
251
|
|
|
276
252
|
"ingress.access_request": (payload) => ({
|
|
277
253
|
title: accessRequestCardTitle(isAdmittedIntroduction(payload)),
|
|
278
|
-
body:
|
|
254
|
+
body: buildAccessRequestContextText(payload),
|
|
279
255
|
seedContentBlocks: buildAccessRequestSeedContentBlocks(payload),
|
|
280
256
|
}),
|
|
281
257
|
|
|
@@ -438,7 +414,12 @@ export function composeFallbackCopy(
|
|
|
438
414
|
|
|
439
415
|
const result: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {};
|
|
440
416
|
for (const ch of channels) {
|
|
441
|
-
|
|
417
|
+
// Mechanics first, so a chat channel's derived deliveryText inherits the
|
|
418
|
+
// instruction the rule adds to the body.
|
|
419
|
+
result[ch] = applyChannelDefaults(
|
|
420
|
+
ch,
|
|
421
|
+
applyGuardianReplyMechanics(baseCopy, ch, signal),
|
|
422
|
+
);
|
|
442
423
|
}
|
|
443
424
|
return result;
|
|
444
425
|
}
|
|
@@ -28,12 +28,8 @@ import type { Provider } from "../providers/types.js";
|
|
|
28
28
|
import { getLogger } from "../util/logger.js";
|
|
29
29
|
import { truncate } from "../util/truncate.js";
|
|
30
30
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
hasAccessRequestInstructions,
|
|
34
|
-
hasInviteFlowDirective,
|
|
35
|
-
isHandshakeOfferedForPayload,
|
|
36
|
-
parseAccessRequestPayload,
|
|
31
|
+
ensureAccessRequestInviteDirectiveInCopy,
|
|
32
|
+
stripAccessRequestReplyMechanicsFromCopy,
|
|
37
33
|
} from "./access-request-copy.js";
|
|
38
34
|
import {
|
|
39
35
|
buildAccessRequestSeedContentBlocks,
|
|
@@ -47,15 +43,10 @@ import {
|
|
|
47
43
|
import { composeFallbackCopy, resolveTitle } from "./copy-composer.js";
|
|
48
44
|
import { createDecision } from "./decisions-store.js";
|
|
49
45
|
import {
|
|
50
|
-
|
|
46
|
+
applyGuardianReplyMechanics,
|
|
51
47
|
buildQuestionDeliveryText,
|
|
52
|
-
hasGuardianRequestCodeInstruction,
|
|
53
48
|
parseGuardianQuestionPayload,
|
|
54
|
-
parseInteractiveApprovalPayload,
|
|
55
49
|
resolveGuardianInstructionModeFromPayload,
|
|
56
|
-
resolveGuardianQuestionInstructionMode,
|
|
57
|
-
stripConflictingGuardianRequestInstructions,
|
|
58
|
-
stripGuardianRequestCodeInstructions,
|
|
59
50
|
} from "./guardian-question-mode.js";
|
|
60
51
|
import {
|
|
61
52
|
nonEmpty,
|
|
@@ -165,6 +156,7 @@ function buildSystemPrompt(
|
|
|
165
156
|
` - Avoid intermediary-instruction phrasing like "consider telling the guardian", "ask the recipient to", or "the assistant should remind them". Rewrite it as final copy the recipient can act on directly.`,
|
|
166
157
|
` - For telegram: 1-2 concise sentences.`,
|
|
167
158
|
` - For slack approval requests: the message renders inside an interactive card with Approve/Reject buttons. Describe only what needs approval — never include approval/reference codes, code-reply instructions, or directions to respond in another app.`,
|
|
159
|
+
` - For vellum and platform guardian requests: the copy is read in the notification bell, the banner, and the push, which act through the request card. Never include reference codes, code-reply instructions, or directions to respond elsewhere.`,
|
|
168
160
|
`- \`conversationSeedMessage\` is the opening message in the internal notification conversation and also the expanded detail shown in the home feed. It should be richer and more contextual than the popup body.`,
|
|
169
161
|
` - For vellum (desktop): use structured markdown for readability. Break content into bullet points, numbered lists, or short sections with **bold** labels. Avoid long unbroken paragraphs — scan-friendly formatting is preferred.`,
|
|
170
162
|
` - Never dump raw JSON. Include only human-readable context.`,
|
|
@@ -600,77 +592,14 @@ export function validateConversationActions(
|
|
|
600
592
|
return result;
|
|
601
593
|
}
|
|
602
594
|
|
|
603
|
-
function ensureGuardianRequestCodeInCopy(
|
|
604
|
-
copy: RenderedChannelCopy,
|
|
605
|
-
requestCode: string,
|
|
606
|
-
mode: "approval" | "answer",
|
|
607
|
-
): RenderedChannelCopy {
|
|
608
|
-
const instruction = buildGuardianRequestCodeInstruction(requestCode, mode);
|
|
609
|
-
|
|
610
|
-
const ensureText = (text: string | undefined): string => {
|
|
611
|
-
const base = typeof text === "string" ? text.trim() : "";
|
|
612
|
-
const sanitized = stripConflictingGuardianRequestInstructions(
|
|
613
|
-
base,
|
|
614
|
-
requestCode,
|
|
615
|
-
mode,
|
|
616
|
-
);
|
|
617
|
-
if (hasGuardianRequestCodeInstruction(sanitized, requestCode, mode)) {
|
|
618
|
-
return sanitized;
|
|
619
|
-
}
|
|
620
|
-
return sanitized.length > 0
|
|
621
|
-
? `${sanitized}\n\n${instruction}`
|
|
622
|
-
: instruction;
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
return {
|
|
626
|
-
...copy,
|
|
627
|
-
body: ensureText(copy.body),
|
|
628
|
-
deliveryText: copy.deliveryText
|
|
629
|
-
? ensureText(copy.deliveryText)
|
|
630
|
-
: copy.deliveryText,
|
|
631
|
-
conversationSeedMessage: copy.conversationSeedMessage
|
|
632
|
-
? ensureText(copy.conversationSeedMessage)
|
|
633
|
-
: copy.conversationSeedMessage,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Remove request-code reply instructions from copy for a channel whose
|
|
639
|
-
* approval UI makes them redundant. Instruction-only fields keep their
|
|
640
|
-
* original text rather than becoming empty — downstream treats an empty
|
|
641
|
-
* body as missing copy.
|
|
642
|
-
*/
|
|
643
|
-
function stripGuardianRequestCodeInCopy(
|
|
644
|
-
copy: RenderedChannelCopy,
|
|
645
|
-
requestCode: string,
|
|
646
|
-
): RenderedChannelCopy {
|
|
647
|
-
const strip = (text: string): string => {
|
|
648
|
-
const stripped = stripGuardianRequestCodeInstructions(text, requestCode);
|
|
649
|
-
return stripped.length > 0 ? stripped : text;
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
return {
|
|
653
|
-
...copy,
|
|
654
|
-
body: strip(copy.body),
|
|
655
|
-
deliveryText: copy.deliveryText
|
|
656
|
-
? strip(copy.deliveryText)
|
|
657
|
-
: copy.deliveryText,
|
|
658
|
-
conversationSeedMessage: copy.conversationSeedMessage
|
|
659
|
-
? strip(copy.conversationSeedMessage)
|
|
660
|
-
: copy.conversationSeedMessage,
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
|
|
664
595
|
/**
|
|
665
596
|
* Guardian questions that share a conversation require explicit request-code
|
|
666
|
-
* targeting
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* `parseInteractiveApprovalPayload`), so code-reply instructions are
|
|
673
|
-
* redundant noise there and are stripped instead of enforced in.
|
|
597
|
+
* targeting, so the reply-mechanics rule runs on every decision path: a
|
|
598
|
+
* channel the guardian answers by typing gets the instruction enforced into
|
|
599
|
+
* its copy even when the model left it out, and a channel that acts through
|
|
600
|
+
* a card (the vellum bell and banner, the platform push, Slack approval
|
|
601
|
+
* cards) gets it stripped, model-authored code phrasing included. The
|
|
602
|
+
* per-channel rule is `guardianCopyCarriesReplyMechanics`.
|
|
674
603
|
*/
|
|
675
604
|
function enforceGuardianRequestCode(
|
|
676
605
|
decision: NotificationDecision,
|
|
@@ -679,17 +608,6 @@ function enforceGuardianRequestCode(
|
|
|
679
608
|
if (signal.sourceEventName !== "guardian.question") {
|
|
680
609
|
return decision;
|
|
681
610
|
}
|
|
682
|
-
const rawCode = signal.contextPayload.requestCode;
|
|
683
|
-
if (typeof rawCode !== "string" || rawCode.trim().length === 0) {
|
|
684
|
-
return decision;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const requestCode = rawCode.trim().toUpperCase();
|
|
688
|
-
const modeResolution = resolveGuardianQuestionInstructionMode(
|
|
689
|
-
signal.contextPayload,
|
|
690
|
-
);
|
|
691
|
-
const slackRendersApprovalButtons =
|
|
692
|
-
parseInteractiveApprovalPayload(signal.contextPayload) != null;
|
|
693
611
|
const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
|
|
694
612
|
...decision.renderedCopy,
|
|
695
613
|
};
|
|
@@ -699,14 +617,7 @@ function enforceGuardianRequestCode(
|
|
|
699
617
|
if (!copy) {
|
|
700
618
|
continue;
|
|
701
619
|
}
|
|
702
|
-
nextCopy[channel] =
|
|
703
|
-
channel === "slack" && slackRendersApprovalButtons
|
|
704
|
-
? stripGuardianRequestCodeInCopy(copy, requestCode)
|
|
705
|
-
: ensureGuardianRequestCodeInCopy(
|
|
706
|
-
copy,
|
|
707
|
-
requestCode,
|
|
708
|
-
modeResolution.mode,
|
|
709
|
-
);
|
|
620
|
+
nextCopy[channel] = applyGuardianReplyMechanics(copy, channel, signal);
|
|
710
621
|
}
|
|
711
622
|
|
|
712
623
|
return {
|
|
@@ -759,6 +670,10 @@ function ensureSeedContentBlocks(
|
|
|
759
670
|
*
|
|
760
671
|
* Scoped to answer mode. A `pending_question` carrying a tool name is a voice
|
|
761
672
|
* tool approval, where composed copy is the right thing.
|
|
673
|
+
*
|
|
674
|
+
* The pinned text carries the reference-code instruction; the
|
|
675
|
+
* reply-mechanics pass runs after this one and keeps or strips it per
|
|
676
|
+
* channel, so this pin never has to know which surfaces take a typed reply.
|
|
762
677
|
*/
|
|
763
678
|
function pinQuestionDeliveryCopy(
|
|
764
679
|
decision: NotificationDecision,
|
|
@@ -814,131 +729,38 @@ function enforceToolApprovalSeedBlocks(
|
|
|
814
729
|
}
|
|
815
730
|
|
|
816
731
|
/**
|
|
817
|
-
* Access-request
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
* When requestCode is NOT present: still check for the invite-flow
|
|
826
|
-
* directive and append it if missing. Per the documented contract, the
|
|
827
|
-
* invite directive should always be present in access-request copy.
|
|
732
|
+
* Access-request copy never carries the request-code directive: that is the
|
|
733
|
+
* card's `plainTextFallback`, appended by a transport only when it sends
|
|
734
|
+
* text without buttons. A model's echo of it comes out on every channel.
|
|
735
|
+
* The invite-flow directive is the opposite case, context rather than
|
|
736
|
+
* mechanics (no surface has an invite button, so the sentence is the only
|
|
737
|
+
* way to start the flow), and model-composed copy that leaves it out gets
|
|
738
|
+
* it appended. The Surface card is ensured on every decision path, the
|
|
739
|
+
* same way tool approvals are.
|
|
828
740
|
*/
|
|
829
|
-
function
|
|
741
|
+
function enforceAccessRequestCopy(
|
|
830
742
|
decision: NotificationDecision,
|
|
831
743
|
signal: NotificationSignal,
|
|
832
744
|
): NotificationDecision {
|
|
833
745
|
if (signal.sourceEventName !== "ingress.access_request") {
|
|
834
746
|
return decision;
|
|
835
747
|
}
|
|
836
|
-
|
|
837
|
-
const rawCode = signal.contextPayload.requestCode;
|
|
838
|
-
const hasRequestCode =
|
|
839
|
-
typeof rawCode === "string" && rawCode.trim().length > 0;
|
|
840
|
-
|
|
841
748
|
const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
|
|
842
749
|
...decision.renderedCopy,
|
|
843
750
|
};
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const handshakeOffered = isHandshakeOfferedForPayload(
|
|
849
|
-
parseAccessRequestPayload(signal.contextPayload),
|
|
850
|
-
);
|
|
851
|
-
|
|
852
|
-
for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
|
|
853
|
-
const copy = nextCopy[channel];
|
|
854
|
-
if (!copy) {
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
|
-
nextCopy[channel] = ensureAccessRequestInstructionsInCopy(
|
|
858
|
-
copy,
|
|
859
|
-
requestCode,
|
|
860
|
-
contractText,
|
|
861
|
-
handshakeOffered,
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
} else {
|
|
865
|
-
// No requestCode — still enforce the invite-flow directive.
|
|
866
|
-
const inviteDirective = buildAccessRequestInviteDirective();
|
|
867
|
-
|
|
868
|
-
for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
|
|
869
|
-
const copy = nextCopy[channel];
|
|
870
|
-
if (!copy) {
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
nextCopy[channel] = ensureInviteFlowDirectiveInCopy(
|
|
874
|
-
copy,
|
|
875
|
-
inviteDirective,
|
|
876
|
-
);
|
|
751
|
+
for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
|
|
752
|
+
const copy = nextCopy[channel];
|
|
753
|
+
if (!copy) {
|
|
754
|
+
continue;
|
|
877
755
|
}
|
|
756
|
+
nextCopy[channel] = ensureAccessRequestInviteDirectiveInCopy(
|
|
757
|
+
stripAccessRequestReplyMechanicsFromCopy(copy, signal.contextPayload),
|
|
758
|
+
);
|
|
878
759
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
...decision,
|
|
882
|
-
renderedCopy: nextCopy,
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
// Ensure seedContentBlocks on all paths (LLM, assistant_tool, fallback).
|
|
886
|
-
result = ensureSeedContentBlocks(
|
|
887
|
-
result,
|
|
760
|
+
return ensureSeedContentBlocks(
|
|
761
|
+
{ ...decision, renderedCopy: nextCopy },
|
|
888
762
|
buildAccessRequestSeedContentBlocks(signal.contextPayload),
|
|
889
763
|
);
|
|
890
|
-
|
|
891
|
-
return result;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function ensureAccessRequestInstructionsInCopy(
|
|
895
|
-
copy: RenderedChannelCopy,
|
|
896
|
-
requestCode: string,
|
|
897
|
-
contractText: string,
|
|
898
|
-
handshakeOffered: boolean,
|
|
899
|
-
): RenderedChannelCopy {
|
|
900
|
-
const ensureText = (text: string | undefined): string => {
|
|
901
|
-
const base = typeof text === "string" ? text.trim() : "";
|
|
902
|
-
if (hasAccessRequestInstructions(base, requestCode, { handshakeOffered })) {
|
|
903
|
-
return base;
|
|
904
|
-
}
|
|
905
|
-
return base.length > 0 ? `${base}\n\n${contractText}` : contractText;
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
return {
|
|
909
|
-
...copy,
|
|
910
|
-
body: ensureText(copy.body),
|
|
911
|
-
deliveryText: copy.deliveryText
|
|
912
|
-
? ensureText(copy.deliveryText)
|
|
913
|
-
: copy.deliveryText,
|
|
914
|
-
conversationSeedMessage: copy.conversationSeedMessage
|
|
915
|
-
? ensureText(copy.conversationSeedMessage)
|
|
916
|
-
: copy.conversationSeedMessage,
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function ensureInviteFlowDirectiveInCopy(
|
|
921
|
-
copy: RenderedChannelCopy,
|
|
922
|
-
inviteDirective: string,
|
|
923
|
-
): RenderedChannelCopy {
|
|
924
|
-
const ensureText = (text: string | undefined): string => {
|
|
925
|
-
const base = typeof text === "string" ? text.trim() : "";
|
|
926
|
-
if (hasInviteFlowDirective(base)) {
|
|
927
|
-
return base;
|
|
928
|
-
}
|
|
929
|
-
return base.length > 0 ? `${base}\n\n${inviteDirective}` : inviteDirective;
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
return {
|
|
933
|
-
...copy,
|
|
934
|
-
body: ensureText(copy.body),
|
|
935
|
-
deliveryText: copy.deliveryText
|
|
936
|
-
? ensureText(copy.deliveryText)
|
|
937
|
-
: copy.deliveryText,
|
|
938
|
-
conversationSeedMessage: copy.conversationSeedMessage
|
|
939
|
-
? ensureText(copy.conversationSeedMessage)
|
|
940
|
-
: copy.conversationSeedMessage,
|
|
941
|
-
};
|
|
942
764
|
}
|
|
943
765
|
|
|
944
766
|
// ── Producer pass-through decisions ────────────────────────────────────
|
|
@@ -993,17 +815,28 @@ function buildPassThroughDecision(params: {
|
|
|
993
815
|
verbatimCopy: true,
|
|
994
816
|
...(deepLinkTarget ? { deepLinkTarget } : {}),
|
|
995
817
|
};
|
|
818
|
+
decision = applyDecisionGuards(decision, signal);
|
|
819
|
+
decision.persistedDecisionId = persistDecision(signal, decision);
|
|
820
|
+
return decision;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* The deterministic guards every decision passes through once the model,
|
|
825
|
+
* the assistant-tool pass-through, or the fallback has rendered copy. In
|
|
826
|
+
* dependency order: the question pin writes the copy the reply-mechanics
|
|
827
|
+
* pass then settles per channel, and the seed-block guard is independent
|
|
828
|
+
* of both.
|
|
829
|
+
*/
|
|
830
|
+
function applyDecisionGuards(
|
|
831
|
+
decision: NotificationDecision,
|
|
832
|
+
signal: NotificationSignal,
|
|
833
|
+
): NotificationDecision {
|
|
834
|
+
decision = pinQuestionDeliveryCopy(decision, signal);
|
|
996
835
|
decision = enforceGuardianRequestCode(decision, signal);
|
|
997
836
|
decision = enforceToolApprovalSeedBlocks(decision, signal);
|
|
998
|
-
decision =
|
|
999
|
-
decision = enforceAccessRequestInstructions(decision, signal);
|
|
837
|
+
decision = enforceAccessRequestCopy(decision, signal);
|
|
1000
838
|
decision = enforceGuardianRequestConversationAffinity(decision, signal);
|
|
1001
|
-
|
|
1002
|
-
decision,
|
|
1003
|
-
signal.conversationAffinityHint,
|
|
1004
|
-
);
|
|
1005
|
-
decision.persistedDecisionId = persistDecision(signal, decision);
|
|
1006
|
-
return decision;
|
|
839
|
+
return enforceConversationAffinity(decision, signal.conversationAffinityHint);
|
|
1007
840
|
}
|
|
1008
841
|
|
|
1009
842
|
// ── Core evaluation function ───────────────────────────────────────────
|
|
@@ -1118,15 +951,9 @@ export async function evaluateSignal(
|
|
|
1118
951
|
log.warn(
|
|
1119
952
|
"Configured provider unavailable for notification decision, using fallback",
|
|
1120
953
|
);
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
decision = pinQuestionDeliveryCopy(decision, signal);
|
|
1125
|
-
decision = enforceAccessRequestInstructions(decision, signal);
|
|
1126
|
-
decision = enforceGuardianRequestConversationAffinity(decision, signal);
|
|
1127
|
-
decision = enforceConversationAffinity(
|
|
1128
|
-
decision,
|
|
1129
|
-
signal.conversationAffinityHint,
|
|
954
|
+
const decision = applyDecisionGuards(
|
|
955
|
+
buildFallbackDecision(signal, availableChannels),
|
|
956
|
+
signal,
|
|
1130
957
|
);
|
|
1131
958
|
decision.persistedDecisionId = persistDecision(signal, decision);
|
|
1132
959
|
return decision;
|
|
@@ -1150,15 +977,7 @@ export async function evaluateSignal(
|
|
|
1150
977
|
decision = buildFallbackDecision(signal, availableChannels);
|
|
1151
978
|
}
|
|
1152
979
|
|
|
1153
|
-
decision =
|
|
1154
|
-
decision = enforceToolApprovalSeedBlocks(decision, signal);
|
|
1155
|
-
decision = pinQuestionDeliveryCopy(decision, signal);
|
|
1156
|
-
decision = enforceAccessRequestInstructions(decision, signal);
|
|
1157
|
-
decision = enforceGuardianRequestConversationAffinity(decision, signal);
|
|
1158
|
-
decision = enforceConversationAffinity(
|
|
1159
|
-
decision,
|
|
1160
|
-
signal.conversationAffinityHint,
|
|
1161
|
-
);
|
|
980
|
+
decision = applyDecisionGuards(decision, signal);
|
|
1162
981
|
decision.persistedDecisionId = persistDecision(signal, decision);
|
|
1163
982
|
|
|
1164
983
|
return decision;
|
|
@@ -8,12 +8,8 @@
|
|
|
8
8
|
* reached. Capturing the surface address here — the conversation id for the
|
|
9
9
|
* in-app vellum card, or the channel-native message id (e.g. Slack `ts`) for a
|
|
10
10
|
* channel card — is what lets a delivered card be addressed back to its request
|
|
11
|
-
* later
|
|
12
|
-
*
|
|
13
|
-
* - to withdraw it in place when the request resolves
|
|
14
|
-
* (`approvals/guardian-card-withdrawal.ts`), and
|
|
15
|
-
* - to resolve an emoji reaction on the card to the right request when several
|
|
16
|
-
* are pending in the same chat (the by-destination-message lookup).
|
|
11
|
+
* later, to withdraw it in place when the request resolves
|
|
12
|
+
* (`approvals/guardian-card-withdrawal.ts`).
|
|
17
13
|
*
|
|
18
14
|
* Every producer records through here so the addressing convention lives in one
|
|
19
15
|
* place and cannot drift between the path that writes the row and the paths that
|
|
@@ -48,7 +44,7 @@ export interface ApprovalCardDeliveryAddress {
|
|
|
48
44
|
conversationId?: string;
|
|
49
45
|
/** Channel addressing: the chat the card was delivered to. */
|
|
50
46
|
chatId?: string;
|
|
51
|
-
/** Channel-native message id (e.g. Slack `ts`)
|
|
47
|
+
/** Channel-native message id (e.g. Slack `ts`): the withdrawal key. */
|
|
52
48
|
messageId?: string;
|
|
53
49
|
/** Initial delivery status (defaults to `DELIVERY_STATUS.pending`). */
|
|
54
50
|
status?: string;
|
|
@@ -78,7 +74,7 @@ export async function recordApprovalCardDelivery(
|
|
|
78
74
|
} catch (err) {
|
|
79
75
|
log.error(
|
|
80
76
|
{ err, requestId: address.requestId, channel: address.channel },
|
|
81
|
-
"Failed to record approval card delivery;
|
|
77
|
+
"Failed to record approval card delivery; withdrawal of this card will not resolve",
|
|
82
78
|
);
|
|
83
79
|
return null;
|
|
84
80
|
}
|
|
@@ -100,7 +96,7 @@ export async function recordApprovalCardDelivery(
|
|
|
100
96
|
* `conversationId`. Platform push results are skipped entirely: a push
|
|
101
97
|
* has no channel-native card surface to address back to.
|
|
102
98
|
* Channel results additionally carry the chat (`destination`) and channel-native
|
|
103
|
-
* id (`messageId`) used to match inbound replies
|
|
99
|
+
* id (`messageId`) used to match inbound replies; a blank `destination`
|
|
104
100
|
* is recorded as unknown rather than persisting the literal channel name as a
|
|
105
101
|
* chat id. Status has two readers: the voice guardian-action sweep acts only
|
|
106
102
|
* on `sent`/`pending` rows, and card withdrawal skips rows already marked
|
|
@@ -13,13 +13,11 @@
|
|
|
13
13
|
* daemon updates it wherever `withdrawGuardianRequestCards` settles the
|
|
14
14
|
* other surfaces.
|
|
15
15
|
*/
|
|
16
|
-
import { z } from "zod";
|
|
17
16
|
|
|
18
17
|
import {
|
|
19
18
|
type FeedItem,
|
|
20
19
|
type FeedItemGuardianIntent,
|
|
21
20
|
type FeedItemGuardianRequest,
|
|
22
|
-
FeedItemGuardianStatusSchema,
|
|
23
21
|
isPendingGuardianFeedItem,
|
|
24
22
|
} from "../api/responses/home.js";
|
|
25
23
|
import {
|
|
@@ -47,19 +45,6 @@ import { readPayloadString } from "./notification-utils.js";
|
|
|
47
45
|
|
|
48
46
|
const log = getLogger("guardian-feed-projection");
|
|
49
47
|
|
|
50
|
-
// The wire enum in `api/responses/home.ts` cannot import the gateway
|
|
51
|
-
// contract (the api directory is copied verbatim into client packages),
|
|
52
|
-
// so it mirrors `GuardianRequestStatusSchema` by value. This assignment
|
|
53
|
-
// fails to compile if the enums ever diverge in either direction.
|
|
54
|
-
type FeedStatus = z.infer<typeof FeedItemGuardianStatusSchema>;
|
|
55
|
-
type StatusEnumsAligned = [GuardianRequestStatus] extends [FeedStatus]
|
|
56
|
-
? [FeedStatus] extends [GuardianRequestStatus]
|
|
57
|
-
? true
|
|
58
|
-
: never
|
|
59
|
-
: never;
|
|
60
|
-
const _statusEnumsAligned: StatusEnumsAligned = true;
|
|
61
|
-
void _statusEnumsAligned;
|
|
62
|
-
|
|
63
48
|
/**
|
|
64
49
|
* The signal events that carry a guardian request. `guardian.question`
|
|
65
50
|
* covers tool approvals, tool grants, and questions; access requests
|