@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
|
@@ -236,6 +236,55 @@ function seedFailedSlackEvent(opts: {
|
|
|
236
236
|
return inbound.eventId;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
function seedFailedTelegramEvent(opts: {
|
|
240
|
+
content: string;
|
|
241
|
+
externalChatId: string;
|
|
242
|
+
messageId: string;
|
|
243
|
+
threadId?: string;
|
|
244
|
+
/** Sender display name as `storePayload` records it at ingress. */
|
|
245
|
+
senderName?: string;
|
|
246
|
+
/** Stands in for the `channelInbound` the live path captures onto the payload. */
|
|
247
|
+
channelInbound?: Record<string, unknown>;
|
|
248
|
+
}): string {
|
|
249
|
+
const inbound = deliveryCrud.recordInbound(
|
|
250
|
+
"telegram",
|
|
251
|
+
opts.externalChatId,
|
|
252
|
+
opts.messageId,
|
|
253
|
+
);
|
|
254
|
+
deliveryCrud.storePayload(inbound.eventId, {
|
|
255
|
+
content: opts.content,
|
|
256
|
+
sourceChannel: "telegram",
|
|
257
|
+
interface: "telegram",
|
|
258
|
+
externalChatId: opts.externalChatId,
|
|
259
|
+
externalMessageId: opts.messageId,
|
|
260
|
+
sourceMetadata: {
|
|
261
|
+
messageId: opts.messageId,
|
|
262
|
+
...(opts.threadId ? { threadId: opts.threadId } : {}),
|
|
263
|
+
},
|
|
264
|
+
...(opts.senderName ? { senderName: opts.senderName } : {}),
|
|
265
|
+
...(opts.channelInbound ? { channelInbound: opts.channelInbound } : {}),
|
|
266
|
+
trustCtx: {
|
|
267
|
+
trustClass: "guardian",
|
|
268
|
+
sourceChannel: "telegram",
|
|
269
|
+
requesterExternalUserId: "user-1",
|
|
270
|
+
requesterChatId: opts.externalChatId,
|
|
271
|
+
guardianPrincipalId: "principal-1",
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
getDb()
|
|
276
|
+
.update(channelInboundEvents)
|
|
277
|
+
.set({
|
|
278
|
+
processingStatus: "failed",
|
|
279
|
+
processingAttempts: 1,
|
|
280
|
+
retryAfter: Date.now() - 1,
|
|
281
|
+
})
|
|
282
|
+
.where(eq(channelInboundEvents.id, inbound.eventId))
|
|
283
|
+
.run();
|
|
284
|
+
|
|
285
|
+
return inbound.eventId;
|
|
286
|
+
}
|
|
287
|
+
|
|
239
288
|
describe("channel-retry-sweep", () => {
|
|
240
289
|
beforeEach(() => {
|
|
241
290
|
resetTables();
|
|
@@ -352,6 +401,37 @@ describe("channel-retry-sweep", () => {
|
|
|
352
401
|
expect(eventRow?.processingStatus).toBe("processed");
|
|
353
402
|
});
|
|
354
403
|
|
|
404
|
+
test("restores the triggering message's id and thread on replay", async () => {
|
|
405
|
+
seedFailedEventWithTrustClass("guardian", {
|
|
406
|
+
sourceMessageId: "1700000000.000200",
|
|
407
|
+
sourceThreadId: "1700000000.000100",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
let capturedTrust: Record<string, unknown> | undefined;
|
|
411
|
+
await sweepFailedEvents(async (conversationId, _content, options) => {
|
|
412
|
+
capturedTrust = (options as { trustContext?: Record<string, unknown> })
|
|
413
|
+
.trustContext;
|
|
414
|
+
const messageId = "message-location";
|
|
415
|
+
getDb()
|
|
416
|
+
.insert(messages)
|
|
417
|
+
.values({
|
|
418
|
+
id: messageId,
|
|
419
|
+
conversationId,
|
|
420
|
+
role: "user",
|
|
421
|
+
content: JSON.stringify([{ type: "text", text: "retry me" }]),
|
|
422
|
+
createdAt: Date.now(),
|
|
423
|
+
})
|
|
424
|
+
.run();
|
|
425
|
+
return { messageId };
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// The replayed turn names the same message and thread the live turn
|
|
429
|
+
// did, so the turn's chat and thread lines and approval-card source
|
|
430
|
+
// links read the same on a retry as on the first attempt.
|
|
431
|
+
expect(capturedTrust?.sourceMessageId).toBe("1700000000.000200");
|
|
432
|
+
expect(capturedTrust?.sourceThreadId).toBe("1700000000.000100");
|
|
433
|
+
});
|
|
434
|
+
|
|
355
435
|
test("fences a non-guardian Slack replay in external_content and carries the ingress idempotency key", async () => {
|
|
356
436
|
seedFailedSlackEvent({
|
|
357
437
|
trustClass: "unverified_contact",
|
|
@@ -442,6 +522,125 @@ describe("channel-retry-sweep", () => {
|
|
|
442
522
|
expect(capturedOptions?.slackInbound?.channelTs).toBe("1700000000.000200");
|
|
443
523
|
});
|
|
444
524
|
|
|
525
|
+
test("replays a captured channelInbound envelope verbatim", async () => {
|
|
526
|
+
const captured = {
|
|
527
|
+
source: "telegram",
|
|
528
|
+
conversationExternalId: "chat-500",
|
|
529
|
+
messageId: "tg-msg-1",
|
|
530
|
+
threadId: "topic-3",
|
|
531
|
+
actorExternalId: "user-77",
|
|
532
|
+
displayName: "Alice",
|
|
533
|
+
eventKind: "message",
|
|
534
|
+
};
|
|
535
|
+
seedFailedTelegramEvent({
|
|
536
|
+
content: "retry me",
|
|
537
|
+
externalChatId: "chat-500",
|
|
538
|
+
messageId: "tg-msg-1",
|
|
539
|
+
channelInbound: captured,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
let capturedOptions:
|
|
543
|
+
| { channelInbound?: Record<string, unknown> }
|
|
544
|
+
| undefined;
|
|
545
|
+
await sweepFailedEvents(async (conversationId, content, options) => {
|
|
546
|
+
capturedOptions = options as {
|
|
547
|
+
channelInbound?: Record<string, unknown>;
|
|
548
|
+
};
|
|
549
|
+
const messageId = "message-tg-captured";
|
|
550
|
+
getDb()
|
|
551
|
+
.insert(messages)
|
|
552
|
+
.values({
|
|
553
|
+
id: messageId,
|
|
554
|
+
conversationId,
|
|
555
|
+
role: "user",
|
|
556
|
+
content: JSON.stringify([{ type: "text", text: content }]),
|
|
557
|
+
createdAt: Date.now(),
|
|
558
|
+
})
|
|
559
|
+
.run();
|
|
560
|
+
return { messageId };
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// The captured envelope is the EXACT object the live turn used, so the
|
|
564
|
+
// replayed row persists an identical providerMeta.
|
|
565
|
+
expect(capturedOptions?.channelInbound).toEqual(captured);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("reconstructs channelInbound from the stored payload when no capture exists", async () => {
|
|
569
|
+
// The crash window: `storePayload` ran at ingress but the daemon died
|
|
570
|
+
// before `storeInboundChannelMetadata`. Recovery must rebuild the same
|
|
571
|
+
// envelope the live turn would have persisted, sender fields included.
|
|
572
|
+
seedFailedTelegramEvent({
|
|
573
|
+
content: "retry me",
|
|
574
|
+
externalChatId: "chat-501",
|
|
575
|
+
messageId: "tg-msg-2",
|
|
576
|
+
threadId: "topic-9",
|
|
577
|
+
senderName: "Alice",
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
let capturedOptions:
|
|
581
|
+
| { channelInbound?: Record<string, unknown> }
|
|
582
|
+
| undefined;
|
|
583
|
+
await sweepFailedEvents(async (conversationId, content, options) => {
|
|
584
|
+
capturedOptions = options as {
|
|
585
|
+
channelInbound?: Record<string, unknown>;
|
|
586
|
+
};
|
|
587
|
+
const messageId = "message-tg-fallback";
|
|
588
|
+
getDb()
|
|
589
|
+
.insert(messages)
|
|
590
|
+
.values({
|
|
591
|
+
id: messageId,
|
|
592
|
+
conversationId,
|
|
593
|
+
role: "user",
|
|
594
|
+
content: JSON.stringify([{ type: "text", text: content }]),
|
|
595
|
+
createdAt: Date.now(),
|
|
596
|
+
})
|
|
597
|
+
.run();
|
|
598
|
+
return { messageId };
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
expect(capturedOptions?.channelInbound).toEqual({
|
|
602
|
+
source: "telegram",
|
|
603
|
+
conversationExternalId: "chat-501",
|
|
604
|
+
messageId: "tg-msg-2",
|
|
605
|
+
threadId: "topic-9",
|
|
606
|
+
displayName: "Alice",
|
|
607
|
+
actorExternalId: "user-1",
|
|
608
|
+
eventKind: "message",
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("a Slack replay carries no channelInbound (Slack keeps slackMeta)", async () => {
|
|
613
|
+
seedFailedSlackEvent({
|
|
614
|
+
trustClass: "guardian",
|
|
615
|
+
content: "hello",
|
|
616
|
+
externalChatId: "C0000001",
|
|
617
|
+
channelTs: "1700000001.000100",
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
let capturedOptions:
|
|
621
|
+
| { channelInbound?: Record<string, unknown> }
|
|
622
|
+
| undefined;
|
|
623
|
+
await sweepFailedEvents(async (conversationId, content, options) => {
|
|
624
|
+
capturedOptions = options as {
|
|
625
|
+
channelInbound?: Record<string, unknown>;
|
|
626
|
+
};
|
|
627
|
+
const messageId = "message-slack-no-neutral";
|
|
628
|
+
getDb()
|
|
629
|
+
.insert(messages)
|
|
630
|
+
.values({
|
|
631
|
+
id: messageId,
|
|
632
|
+
conversationId,
|
|
633
|
+
role: "user",
|
|
634
|
+
content: JSON.stringify([{ type: "text", text: content }]),
|
|
635
|
+
createdAt: Date.now(),
|
|
636
|
+
})
|
|
637
|
+
.run();
|
|
638
|
+
return { messageId };
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
expect(capturedOptions?.channelInbound).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
|
|
445
644
|
test("replays the sender's Slack app context from the captured slackInbound", async () => {
|
|
446
645
|
seedFailedSlackEvent({
|
|
447
646
|
trustClass: "guardian",
|
|
@@ -84,6 +84,7 @@ function createWebTurnContext(
|
|
|
84
84
|
size: () => 0,
|
|
85
85
|
} as unknown as MessageQueue;
|
|
86
86
|
let processing = false;
|
|
87
|
+
let owner = 0;
|
|
87
88
|
return {
|
|
88
89
|
conversationId: "conv-client-os-test",
|
|
89
90
|
messages: [],
|
|
@@ -91,6 +92,21 @@ function createWebTurnContext(
|
|
|
91
92
|
setProcessing: (value: boolean) => {
|
|
92
93
|
processing = value;
|
|
93
94
|
},
|
|
95
|
+
acquireProcessingFenced: async () => {
|
|
96
|
+
if (processing) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
processing = true;
|
|
100
|
+
owner += 1;
|
|
101
|
+
return owner;
|
|
102
|
+
},
|
|
103
|
+
releaseProcessing: (claim: number) => {
|
|
104
|
+
if (claim !== owner) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
processing = false;
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
94
110
|
abortController: null,
|
|
95
111
|
queue: queueStub,
|
|
96
112
|
clientOs,
|
|
@@ -111,7 +127,7 @@ describe("client OS surface metadata persistence", () => {
|
|
|
111
127
|
addMessageCalls.length = 0;
|
|
112
128
|
});
|
|
113
129
|
|
|
114
|
-
test.each(["macos", "windows", "ios", "android", "web"])(
|
|
130
|
+
test.each(["macos", "windows", "linux", "ios", "android", "web"])(
|
|
115
131
|
"stamps client.os = %s from the conversation's clientOs",
|
|
116
132
|
async (os) => {
|
|
117
133
|
const ctx = createWebTurnContext(os);
|
|
@@ -137,7 +153,7 @@ describe("client OS surface metadata persistence", () => {
|
|
|
137
153
|
});
|
|
138
154
|
|
|
139
155
|
test("omits the client bag for values outside the ClientOs vocabulary", async () => {
|
|
140
|
-
const ctx = createWebTurnContext("
|
|
156
|
+
const ctx = createWebTurnContext("plan9");
|
|
141
157
|
await persistQueuedMessageBody(ctx, {
|
|
142
158
|
content: "hello",
|
|
143
159
|
requestId: "req-invalid",
|
|
@@ -214,9 +230,22 @@ describe("client OS per-row evidence marker", () => {
|
|
|
214
230
|
expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
|
|
215
231
|
});
|
|
216
232
|
|
|
233
|
+
test("recognizes a Linux app turn as desktop-originated", async () => {
|
|
234
|
+
const ctx = createWebTurnContext("linux");
|
|
235
|
+
await persistQueuedMessageBody(ctx, {
|
|
236
|
+
content: "hello",
|
|
237
|
+
requestId: "req-transport-os-linux",
|
|
238
|
+
requestClientOs: "linux",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(lastUserMetadata().client).toEqual({ os: "linux" });
|
|
242
|
+
expect(lastUserMetadata().clientOsFromRequest).toBe(true);
|
|
243
|
+
expect(isDesktopOriginatedUserMessage(lastUserMetadata())).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
|
|
217
246
|
// Mirrors the Mac-originated case above: a plain browser tab reports
|
|
218
247
|
// `client.os: "web"`, while the Electron desktop renderer resolves its own
|
|
219
|
-
// host OS first and persists `macos`/`windows` instead.
|
|
248
|
+
// host OS first and persists `macos`/`windows`/`linux` instead.
|
|
220
249
|
test("marks an OS this row's own transport reported (web)", async () => {
|
|
221
250
|
const ctx = createWebTurnContext("web");
|
|
222
251
|
await persistQueuedMessageBody(ctx, {
|
|
@@ -1677,6 +1677,21 @@ describe("ConnectionResolutionError classification", () => {
|
|
|
1677
1677
|
expect(result.userMessage).toContain("Restart the assistant");
|
|
1678
1678
|
});
|
|
1679
1679
|
|
|
1680
|
+
it("classifies adapter_unavailable for a Vellum-hosted GPU model", () => {
|
|
1681
|
+
const err = new ConnectionResolutionError(
|
|
1682
|
+
"vellum",
|
|
1683
|
+
"adapter_unavailable",
|
|
1684
|
+
"no adapter",
|
|
1685
|
+
{ model: "qwen/qwen3-8b", profileName: "steer" },
|
|
1686
|
+
);
|
|
1687
|
+
const result = classifyConversationError(err, errCtx);
|
|
1688
|
+
expect(result.code).toBe("PROVIDER_NOT_CONFIGURED");
|
|
1689
|
+
expect(result.userMessage).toContain("qwen/qwen3-8b");
|
|
1690
|
+
expect(result.userMessage).toContain("Vellum GPU route");
|
|
1691
|
+
expect(result.userMessage).toContain('profile "steer"');
|
|
1692
|
+
expect(result.userMessage).toContain("was not sent through another provider");
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1680
1695
|
it("classifies missing_credential naming the connection and fix", () => {
|
|
1681
1696
|
const err = new ConnectionResolutionError(
|
|
1682
1697
|
"anthropic-personal",
|
|
@@ -589,6 +589,53 @@ describe("loadFromDb history repair", () => {
|
|
|
589
589
|
expect(allText).not.toContain("hunter2");
|
|
590
590
|
});
|
|
591
591
|
|
|
592
|
+
test("the assistant's own deleted post keeps its content plus a visibility marker", async () => {
|
|
593
|
+
mockConversation = {
|
|
594
|
+
id: "conv-1",
|
|
595
|
+
contextSummary: null,
|
|
596
|
+
contextCompactedMessageCount: 0,
|
|
597
|
+
totalInputTokens: 0,
|
|
598
|
+
totalOutputTokens: 0,
|
|
599
|
+
totalEstimatedCost: 0,
|
|
600
|
+
};
|
|
601
|
+
mockDbMessages = [
|
|
602
|
+
{
|
|
603
|
+
id: "m1",
|
|
604
|
+
role: "user",
|
|
605
|
+
content: [{ type: "text", text: "What time is the meeting?" }],
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "m2",
|
|
609
|
+
role: "assistant",
|
|
610
|
+
content: [{ type: "text", text: "The meeting is at 3pm." }],
|
|
611
|
+
metadata: JSON.stringify({
|
|
612
|
+
providerMeta: JSON.stringify({
|
|
613
|
+
source: "discord",
|
|
614
|
+
conversationExternalId: "chan-1",
|
|
615
|
+
messageId: "555.2",
|
|
616
|
+
eventKind: "message",
|
|
617
|
+
deletedAt: 1700000001000,
|
|
618
|
+
}),
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
621
|
+
];
|
|
622
|
+
|
|
623
|
+
const conversation = makeConversation();
|
|
624
|
+
await conversation.loadFromDb();
|
|
625
|
+
const assistantRow = conversation
|
|
626
|
+
.getMessages()
|
|
627
|
+
.find((m) => m.role === "assistant");
|
|
628
|
+
const texts = (assistantRow?.content ?? [])
|
|
629
|
+
.filter((b) => b.type === "text")
|
|
630
|
+
.map((b) => (b.type === "text" ? b.text : ""));
|
|
631
|
+
// Erasure is for a retracted user message; the assistant's own words
|
|
632
|
+
// stay (it did say them), and the rendered fact is lost visibility.
|
|
633
|
+
expect(texts.join("\n")).toContain("The meeting is at 3pm.");
|
|
634
|
+
expect(texts.join("\n")).toContain(
|
|
635
|
+
"[This message was deleted from the channel and is no longer visible to participants]",
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
|
|
592
639
|
test("a reaction never quotes a target that was later deleted", async () => {
|
|
593
640
|
mockConversation = {
|
|
594
641
|
id: "conv-1",
|
|
@@ -703,6 +750,77 @@ describe("loadFromDb history repair", () => {
|
|
|
703
750
|
expect(allText).not.toContain("an earlier message");
|
|
704
751
|
});
|
|
705
752
|
|
|
753
|
+
test("a partially deleted split reply quotes only its surviving posts", async () => {
|
|
754
|
+
mockConversation = {
|
|
755
|
+
id: "conv-1",
|
|
756
|
+
contextSummary: null,
|
|
757
|
+
contextCompactedMessageCount: 0,
|
|
758
|
+
totalInputTokens: 0,
|
|
759
|
+
totalOutputTokens: 0,
|
|
760
|
+
totalEstimatedCost: 0,
|
|
761
|
+
};
|
|
762
|
+
const reactionRow = (target: string, emoji: string, id: string) => ({
|
|
763
|
+
id,
|
|
764
|
+
role: "user",
|
|
765
|
+
content: [{ type: "text", text: "[reaction]" }],
|
|
766
|
+
metadata: JSON.stringify({
|
|
767
|
+
providerMeta: JSON.stringify({
|
|
768
|
+
source: "discord",
|
|
769
|
+
conversationExternalId: "chan-1",
|
|
770
|
+
eventKind: "reaction",
|
|
771
|
+
reaction: {
|
|
772
|
+
targetMessageId: target,
|
|
773
|
+
emoji,
|
|
774
|
+
op: "added",
|
|
775
|
+
actorDisplayName: "Alice",
|
|
776
|
+
},
|
|
777
|
+
}),
|
|
778
|
+
}),
|
|
779
|
+
});
|
|
780
|
+
mockDbMessages = [
|
|
781
|
+
{
|
|
782
|
+
id: "m1",
|
|
783
|
+
role: "assistant",
|
|
784
|
+
content: [{ type: "text", text: "Deploy finished cleanly." }],
|
|
785
|
+
metadata: JSON.stringify({
|
|
786
|
+
providerMeta: JSON.stringify({
|
|
787
|
+
source: "discord",
|
|
788
|
+
conversationExternalId: "chan-1",
|
|
789
|
+
messageId: "555.1",
|
|
790
|
+
additionalMessageIds: ["555.2"],
|
|
791
|
+
deletedMessageIds: ["555.2"],
|
|
792
|
+
eventKind: "message",
|
|
793
|
+
}),
|
|
794
|
+
}),
|
|
795
|
+
},
|
|
796
|
+
// Reaction on the surviving post: still quotable.
|
|
797
|
+
reactionRow("555.1", "tada", "m2"),
|
|
798
|
+
// Reaction on the deleted post: degrades to the unresolved form
|
|
799
|
+
// rather than quoting content no longer on the channel.
|
|
800
|
+
reactionRow("555.2", "eyes", "m3"),
|
|
801
|
+
];
|
|
802
|
+
|
|
803
|
+
const conversation = makeConversation();
|
|
804
|
+
await conversation.loadFromDb();
|
|
805
|
+
const allText = conversation
|
|
806
|
+
.getMessages()
|
|
807
|
+
.flatMap((m) => m.content)
|
|
808
|
+
.filter((b) => b.type === "text")
|
|
809
|
+
.map((b) => (b.type === "text" ? b.text : ""))
|
|
810
|
+
.join("\n");
|
|
811
|
+
// The row itself is NOT fully deleted: its content stays.
|
|
812
|
+
expect(allText).toContain("Deploy finished cleanly.");
|
|
813
|
+
// :tada: (surviving target) quotes; :eyes: (deleted target) does not.
|
|
814
|
+
const tadaLine = allText
|
|
815
|
+
.split("\n")
|
|
816
|
+
.find((line) => line.includes(":tada:"));
|
|
817
|
+
const eyesLine = allText
|
|
818
|
+
.split("\n")
|
|
819
|
+
.find((line) => line.includes(":eyes:"));
|
|
820
|
+
expect(tadaLine).toContain("Deploy finished cleanly.");
|
|
821
|
+
expect(eyesLine).toContain("an earlier message");
|
|
822
|
+
});
|
|
823
|
+
|
|
706
824
|
test("the assistant's own reaction row renders second-person", async () => {
|
|
707
825
|
mockConversation = {
|
|
708
826
|
id: "conv-1",
|
|
@@ -19,9 +19,17 @@ let addMessageShouldThrow = false;
|
|
|
19
19
|
/** Simulated existing conversations for getConversation mock. */
|
|
20
20
|
let mockExistingConversations: Record<
|
|
21
21
|
string,
|
|
22
|
-
{
|
|
22
|
+
{
|
|
23
|
+
id: string;
|
|
24
|
+
source: string;
|
|
25
|
+
title: string | null;
|
|
26
|
+
conversationType?: string;
|
|
27
|
+
}
|
|
23
28
|
> = {};
|
|
24
29
|
|
|
30
|
+
/** Mutable arm for the assistant-initiated-threads gate mock. */
|
|
31
|
+
let assistantInitiatedThreadsEnabled = false;
|
|
32
|
+
|
|
25
33
|
const createConversationMock = mock((_opts?: unknown) => {
|
|
26
34
|
if (createConversationShouldThrow) {
|
|
27
35
|
throw new Error("DB write failed");
|
|
@@ -102,6 +110,12 @@ mock.module("../persistence/external-conversation-store.js", () => ({
|
|
|
102
110
|
upsertOutboundBinding: upsertOutboundBindingMock,
|
|
103
111
|
}));
|
|
104
112
|
|
|
113
|
+
/* The registry resolves through workspace override files; a mutable arm keeps
|
|
114
|
+
these tests off the filesystem, mirroring the section-split suite. */
|
|
115
|
+
mock.module("../config/assistant-initiated-threads-gate.js", () => ({
|
|
116
|
+
isAssistantInitiatedThreadsEnabled: () => assistantInitiatedThreadsEnabled,
|
|
117
|
+
}));
|
|
118
|
+
|
|
105
119
|
import { pairDeliveryWithConversation } from "../notifications/conversation-pairing.js";
|
|
106
120
|
import type { NotificationSignal } from "../notifications/signal.js";
|
|
107
121
|
import type {
|
|
@@ -162,6 +176,7 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
162
176
|
addMessageShouldThrow = false;
|
|
163
177
|
mockExistingConversations = {};
|
|
164
178
|
mockBindings = {};
|
|
179
|
+
assistantInitiatedThreadsEnabled = false;
|
|
165
180
|
});
|
|
166
181
|
|
|
167
182
|
// ── start_new_conversation (vellum) ─────────────────────────────────
|
|
@@ -1356,4 +1371,129 @@ describe("pairDeliveryWithConversation", () => {
|
|
|
1356
1371
|
expect(result.conversationId).toBe("conv-001");
|
|
1357
1372
|
expect(result.messageId).toBe("msg-001");
|
|
1358
1373
|
});
|
|
1374
|
+
// ── assistant.share → assistant-initiated thread (flag-gated) ────────
|
|
1375
|
+
|
|
1376
|
+
describe("assistant.share promotion under assistant-initiated-threads", () => {
|
|
1377
|
+
const share = (overrides?: Partial<NotificationSignal>) =>
|
|
1378
|
+
makeSignal({
|
|
1379
|
+
requiresConversation: undefined,
|
|
1380
|
+
sourceEventName: "assistant.share",
|
|
1381
|
+
sourceContextId: "conv-heartbeat",
|
|
1382
|
+
...overrides,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
test("flag on: a share from a background run materializes a section thread", async () => {
|
|
1386
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1387
|
+
mockExistingConversations["conv-heartbeat"] = {
|
|
1388
|
+
id: "conv-heartbeat",
|
|
1389
|
+
source: "heartbeat",
|
|
1390
|
+
title: null,
|
|
1391
|
+
conversationType: "background",
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
const result = await pairDeliveryWithConversation(
|
|
1395
|
+
share(),
|
|
1396
|
+
"vellum" as NotificationChannel,
|
|
1397
|
+
makeCopy({ conversationTitle: "Noticed something" }),
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
expect(result.createdNewConversation).toBe(true);
|
|
1401
|
+
const callArgs = createConversationMock.mock.calls[0]![0] as Record<
|
|
1402
|
+
string,
|
|
1403
|
+
unknown
|
|
1404
|
+
>;
|
|
1405
|
+
expect(callArgs.source).toBe("assistant_initiated");
|
|
1406
|
+
expect(callArgs.conversationType).toBe("standard");
|
|
1407
|
+
expect(callArgs.title).toBe("Noticed something");
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
test("flag on: an unresolvable source context still materializes", async () => {
|
|
1411
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1412
|
+
|
|
1413
|
+
const result = await pairDeliveryWithConversation(
|
|
1414
|
+
share({ sourceContextId: "cli-123" }),
|
|
1415
|
+
"vellum" as NotificationChannel,
|
|
1416
|
+
makeCopy(),
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
expect(result.createdNewConversation).toBe(true);
|
|
1420
|
+
const callArgs = createConversationMock.mock.calls[0]![0] as Record<
|
|
1421
|
+
string,
|
|
1422
|
+
unknown
|
|
1423
|
+
>;
|
|
1424
|
+
expect(callArgs.source).toBe("assistant_initiated");
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
test("flag on: a share from a user-facing thread keeps the passive append", async () => {
|
|
1428
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1429
|
+
mockExistingConversations["conv-user"] = {
|
|
1430
|
+
id: "conv-user",
|
|
1431
|
+
source: "user",
|
|
1432
|
+
title: "Ongoing chat",
|
|
1433
|
+
conversationType: "standard",
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
const result = await pairDeliveryWithConversation(
|
|
1437
|
+
share({ sourceContextId: "conv-user" }),
|
|
1438
|
+
"vellum" as NotificationChannel,
|
|
1439
|
+
makeCopy(),
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
expect(result.createdNewConversation).toBe(false);
|
|
1443
|
+
expect(result.conversationId).toBe("conv-user");
|
|
1444
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
test("flag off: shares keep the passive append even from background runs", async () => {
|
|
1448
|
+
mockExistingConversations["conv-heartbeat"] = {
|
|
1449
|
+
id: "conv-heartbeat",
|
|
1450
|
+
source: "heartbeat",
|
|
1451
|
+
title: null,
|
|
1452
|
+
conversationType: "background",
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
const result = await pairDeliveryWithConversation(
|
|
1456
|
+
share(),
|
|
1457
|
+
"vellum" as NotificationChannel,
|
|
1458
|
+
makeCopy(),
|
|
1459
|
+
);
|
|
1460
|
+
|
|
1461
|
+
expect(result.createdNewConversation).toBe(false);
|
|
1462
|
+
expect(result.conversationId).toBe("conv-heartbeat");
|
|
1463
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
test("flag on: an explicit conversationMetadata.source wins over promotion", async () => {
|
|
1467
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1468
|
+
|
|
1469
|
+
const result = await pairDeliveryWithConversation(
|
|
1470
|
+
share({ conversationMetadata: { source: "schedule" } }),
|
|
1471
|
+
"vellum" as NotificationChannel,
|
|
1472
|
+
makeCopy(),
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
// The producer declared its own filing, so the passive rule holds.
|
|
1476
|
+
expect(result.createdNewConversation).toBe(false);
|
|
1477
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
test("flag on: non-share passive events keep the passive append", async () => {
|
|
1481
|
+
assistantInitiatedThreadsEnabled = true;
|
|
1482
|
+
mockExistingConversations["conv-heartbeat"] = {
|
|
1483
|
+
id: "conv-heartbeat",
|
|
1484
|
+
source: "heartbeat",
|
|
1485
|
+
title: null,
|
|
1486
|
+
conversationType: "background",
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
const result = await pairDeliveryWithConversation(
|
|
1490
|
+
share({ sourceEventName: "scheduler.reminder_fired" }),
|
|
1491
|
+
"vellum" as NotificationChannel,
|
|
1492
|
+
makeCopy(),
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
expect(result.createdNewConversation).toBe(false);
|
|
1496
|
+
expect(createConversationMock).not.toHaveBeenCalled();
|
|
1497
|
+
});
|
|
1498
|
+
});
|
|
1359
1499
|
});
|
|
@@ -97,8 +97,35 @@ function createFakeConversation(conversationId: string): Conversation {
|
|
|
97
97
|
isProcessing(this: { processing: boolean }) {
|
|
98
98
|
return this.processing;
|
|
99
99
|
},
|
|
100
|
-
setProcessing(
|
|
100
|
+
setProcessing(
|
|
101
|
+
this: { processing: boolean; owner: number },
|
|
102
|
+
value: boolean,
|
|
103
|
+
) {
|
|
101
104
|
this.processing = value;
|
|
105
|
+
this.owner = value ? this.owner + 1 : 0;
|
|
106
|
+
},
|
|
107
|
+
owner: 0,
|
|
108
|
+
async acquireProcessingFenced(this: {
|
|
109
|
+
processing: boolean;
|
|
110
|
+
owner: number;
|
|
111
|
+
}) {
|
|
112
|
+
if (this.processing) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
this.processing = true;
|
|
116
|
+
this.owner += 1;
|
|
117
|
+
return this.owner;
|
|
118
|
+
},
|
|
119
|
+
releaseProcessing(
|
|
120
|
+
this: { processing: boolean; owner: number },
|
|
121
|
+
claim: number,
|
|
122
|
+
) {
|
|
123
|
+
if (claim !== this.owner) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
this.processing = false;
|
|
127
|
+
this.owner = 0;
|
|
128
|
+
return true;
|
|
102
129
|
},
|
|
103
130
|
setChannelCapabilities: () => {},
|
|
104
131
|
setAssistantId: () => {},
|