@vellumai/assistant 0.11.8 → 0.11.9-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +12 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +15 -1
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +12 -7
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +31 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for POST /v1/conversations/:id/sight-frame, the HTTP door the text
|
|
3
|
+
* chat's camera tile persists a kept frame through.
|
|
4
|
+
*
|
|
5
|
+
* Exercises the guards the handler owns (an id that names no conversation, a
|
|
6
|
+
* body with no attachment), the actor the row is attributed to, and the
|
|
7
|
+
* persist behaviour it inherits from the live-voice path, including the one
|
|
8
|
+
* difference: a keep taken beside the composer is not a voice session turn.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, mock, spyOn, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createMockProvider,
|
|
15
|
+
textResponse,
|
|
16
|
+
} from "../../../__tests__/helpers/mock-provider.js";
|
|
17
|
+
import { setConfig } from "../../../__tests__/helpers/set-config.js";
|
|
18
|
+
|
|
19
|
+
setConfig("memory", { enabled: false });
|
|
20
|
+
|
|
21
|
+
import { Conversation } from "../../../daemon/conversation.js";
|
|
22
|
+
import {
|
|
23
|
+
deleteConversation,
|
|
24
|
+
setConversation,
|
|
25
|
+
} from "../../../daemon/conversation-registry.js";
|
|
26
|
+
import { destroyActiveConversation } from "../../../daemon/conversation-store.js";
|
|
27
|
+
import { _setProcessingWaitMsForTests } from "../../../live-voice/live-voice-photo.js";
|
|
28
|
+
import {
|
|
29
|
+
getAttachmentById,
|
|
30
|
+
uploadAttachment,
|
|
31
|
+
} from "../../../persistence/attachments-store.js";
|
|
32
|
+
import {
|
|
33
|
+
createConversation,
|
|
34
|
+
deleteConversation as deleteConversationRows,
|
|
35
|
+
getConversation,
|
|
36
|
+
getMessages,
|
|
37
|
+
type MessageRow,
|
|
38
|
+
} from "../../../persistence/conversation-crud.js";
|
|
39
|
+
import { sightFrameAttachmentIdsFromMetadata } from "../../../persistence/conversation-types.js";
|
|
40
|
+
import { getDb } from "../../../persistence/db-connection.js";
|
|
41
|
+
import { initializeDb } from "../../../persistence/db-init.js";
|
|
42
|
+
import { conversations } from "../../../persistence/schema/index.js";
|
|
43
|
+
import { BadRequestError, NotFoundError } from "../errors.js";
|
|
44
|
+
import type { RouteDefinition } from "../types.js";
|
|
45
|
+
import * as vellumActorTrust from "../vellum-actor-trust.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run something in the window the handler opens between accepting the request
|
|
49
|
+
* and persisting the frame.
|
|
50
|
+
*
|
|
51
|
+
* Resolving the request's actor is the only thing the handler awaits in
|
|
52
|
+
* between, so a hook here lands inside that window without a timer to race.
|
|
53
|
+
* Armed per test and cleared as it fires; every other test leaves it null and
|
|
54
|
+
* gets the real resolver.
|
|
55
|
+
*/
|
|
56
|
+
let duringTrustResolution: (() => void) | null = null;
|
|
57
|
+
const realVellumActorTrust = { ...vellumActorTrust };
|
|
58
|
+
mock.module("../vellum-actor-trust.js", () => ({
|
|
59
|
+
...realVellumActorTrust,
|
|
60
|
+
resolveVellumActorTrustContext: (
|
|
61
|
+
...args: Parameters<
|
|
62
|
+
typeof realVellumActorTrust.resolveVellumActorTrustContext
|
|
63
|
+
>
|
|
64
|
+
) => {
|
|
65
|
+
const hook = duringTrustResolution;
|
|
66
|
+
duringTrustResolution = null;
|
|
67
|
+
hook?.();
|
|
68
|
+
return realVellumActorTrust.resolveVellumActorTrustContext(...args);
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const { ROUTES: SIGHT_FRAME_ROUTES } = await import("../sight-frame-routes.js");
|
|
73
|
+
|
|
74
|
+
await initializeDb();
|
|
75
|
+
|
|
76
|
+
const IMAGE_BASE64 =
|
|
77
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk";
|
|
78
|
+
|
|
79
|
+
function findHandler(routes: RouteDefinition[], operationId: string) {
|
|
80
|
+
const route = routes.find((r) => r.operationId === operationId);
|
|
81
|
+
if (!route) {
|
|
82
|
+
throw new Error(`Route ${operationId} not found`);
|
|
83
|
+
}
|
|
84
|
+
return route.handler;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sightFrameHandler = findHandler(
|
|
88
|
+
SIGHT_FRAME_ROUTES,
|
|
89
|
+
"conversationSightFrame",
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
interface SightFrameResponse {
|
|
93
|
+
persisted: boolean;
|
|
94
|
+
messageId?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function persist(
|
|
98
|
+
id: string,
|
|
99
|
+
body: Record<string, unknown>,
|
|
100
|
+
headers?: Record<string, string>,
|
|
101
|
+
): Promise<SightFrameResponse> {
|
|
102
|
+
return sightFrameHandler({
|
|
103
|
+
pathParams: { id },
|
|
104
|
+
body,
|
|
105
|
+
...(headers ? { headers } : {}),
|
|
106
|
+
}) as Promise<SightFrameResponse>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Tear the conversation down the way DELETE /v1/conversations/:id does. */
|
|
110
|
+
function deleteEverywhere(id: string): void {
|
|
111
|
+
destroyActiveConversation(id, { keepSubagentRecords: true });
|
|
112
|
+
deleteConversationRows(id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A registered conversation the persist path can reach, plus its teardown. */
|
|
116
|
+
function liveConversation(title: string) {
|
|
117
|
+
const row = createConversation(title);
|
|
118
|
+
const { provider } = createMockProvider([textResponse("")]);
|
|
119
|
+
const activeConversation = new Conversation(
|
|
120
|
+
row.id,
|
|
121
|
+
provider,
|
|
122
|
+
"system prompt",
|
|
123
|
+
() => {},
|
|
124
|
+
"/tmp",
|
|
125
|
+
{ maxTokens: 4096 },
|
|
126
|
+
);
|
|
127
|
+
activeConversation.setTrustContext({
|
|
128
|
+
trustClass: "guardian",
|
|
129
|
+
sourceChannel: "vellum",
|
|
130
|
+
});
|
|
131
|
+
setConversation(row.id, activeConversation);
|
|
132
|
+
return {
|
|
133
|
+
id: row.id,
|
|
134
|
+
activeConversation,
|
|
135
|
+
dispose: () => {
|
|
136
|
+
deleteConversation(row.id);
|
|
137
|
+
activeConversation.dispose();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function metadataOf(row: MessageRow): Record<string, unknown> {
|
|
143
|
+
return JSON.parse(row.metadata ?? "{}") as Record<string, unknown>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function conversationCount(): number {
|
|
147
|
+
return getDb().select({ id: conversations.id }).from(conversations).all()
|
|
148
|
+
.length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function uploadFrame(name: string): Promise<string> {
|
|
152
|
+
return uploadAttachment(name, "image/png", IMAGE_BASE64).then((a) => a.id);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** True while the attachment's row is still in the store. */
|
|
156
|
+
function frameStored(attachmentId: string): boolean {
|
|
157
|
+
return getAttachmentById(attachmentId) !== null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sleep(ms: number): Promise<void> {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
describe("POST /v1/conversations/:id/sight-frame", () => {
|
|
165
|
+
test("404s for an unknown id without minting a conversation for it", async () => {
|
|
166
|
+
// The persist calls `getOrCreateConversation`, so an id the handler lets
|
|
167
|
+
// through becomes a conversation holding nothing but camera frames.
|
|
168
|
+
const before = conversationCount();
|
|
169
|
+
|
|
170
|
+
await expect(
|
|
171
|
+
persist(crypto.randomUUID(), { attachmentId: "att-1" }),
|
|
172
|
+
).rejects.toThrow(NotFoundError);
|
|
173
|
+
|
|
174
|
+
expect(conversationCount()).toBe(before);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("400s when the body carries no attachment", async () => {
|
|
178
|
+
const live = liveConversation("Chat keep bad body");
|
|
179
|
+
try {
|
|
180
|
+
await expect(persist(live.id, {})).rejects.toThrow(BadRequestError);
|
|
181
|
+
await expect(persist(live.id, { attachmentId: "" })).rejects.toThrow(
|
|
182
|
+
BadRequestError,
|
|
183
|
+
);
|
|
184
|
+
await expect(persist(live.id, { attachmentId: 7 })).rejects.toThrow(
|
|
185
|
+
BadRequestError,
|
|
186
|
+
);
|
|
187
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
188
|
+
} finally {
|
|
189
|
+
live.dispose();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("persists the frame as its own tagged row and names it", async () => {
|
|
194
|
+
const live = liveConversation("Chat keep happy path");
|
|
195
|
+
try {
|
|
196
|
+
const attachmentId = await uploadFrame("frame.png");
|
|
197
|
+
|
|
198
|
+
const response = await persist(live.id, { attachmentId });
|
|
199
|
+
|
|
200
|
+
const [row] = getMessages(live.id);
|
|
201
|
+
expect(response).toEqual({ persisted: true, messageId: row.id });
|
|
202
|
+
expect(row.content.filter((block) => block.type === "text")).toEqual([
|
|
203
|
+
{ type: "text", text: "(camera frame)" },
|
|
204
|
+
]);
|
|
205
|
+
// Retention reads the tag and the memory-privacy guard reads the pair of
|
|
206
|
+
// `scripted` and the tag, so a chat keep has to carry both.
|
|
207
|
+
expect(metadataOf(row).scripted).toBe(true);
|
|
208
|
+
expect(sightFrameAttachmentIdsFromMetadata(metadataOf(row))).toEqual([
|
|
209
|
+
attachmentId,
|
|
210
|
+
]);
|
|
211
|
+
} finally {
|
|
212
|
+
live.dispose();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("attributes the row to the verified actor, not the conversation's trust", async () => {
|
|
217
|
+
// The conversation rests on the trust of whoever used it last, so a row
|
|
218
|
+
// stamped from the slot names that actor rather than the one this request
|
|
219
|
+
// was verified as. Actor-scoped history reads the stamp, so the wrong one
|
|
220
|
+
// either hides the frame from its owner or lends guardian standing to
|
|
221
|
+
// content that has none.
|
|
222
|
+
const live = liveConversation("Chat keep actor attribution");
|
|
223
|
+
try {
|
|
224
|
+
const attachmentId = await uploadFrame("frame.png");
|
|
225
|
+
// A principal no guardian binding names, which the vellum-channel
|
|
226
|
+
// resolver fails closed to `unknown`.
|
|
227
|
+
const headers = { "x-vellum-actor-principal-id": "actor-not-bound" };
|
|
228
|
+
expect(live.activeConversation.trustContext?.trustClass).toBe("guardian");
|
|
229
|
+
|
|
230
|
+
expect(await persist(live.id, { attachmentId }, headers)).toMatchObject({
|
|
231
|
+
persisted: true,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const metadata = metadataOf(getMessages(live.id)[0]);
|
|
235
|
+
expect(metadata.provenanceTrustClass).toBe("unknown");
|
|
236
|
+
expect(metadata.provenanceSourceChannel).toBe("vellum");
|
|
237
|
+
} finally {
|
|
238
|
+
live.dispose();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("attributes a caller with no actor header to the guardian", async () => {
|
|
243
|
+
// A local or IPC caller carries no actor header and is the guardian by
|
|
244
|
+
// construction, the same reading canonical ingress gives it.
|
|
245
|
+
const live = liveConversation("Chat keep local caller");
|
|
246
|
+
try {
|
|
247
|
+
const attachmentId = await uploadFrame("frame.png");
|
|
248
|
+
|
|
249
|
+
expect(await persist(live.id, { attachmentId })).toMatchObject({
|
|
250
|
+
persisted: true,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const metadata = metadataOf(getMessages(live.id)[0]);
|
|
254
|
+
expect(metadata.provenanceTrustClass).toBe("guardian");
|
|
255
|
+
expect(metadata.provenanceSourceChannel).toBe("vellum");
|
|
256
|
+
} finally {
|
|
257
|
+
live.dispose();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("does not mark a composer keep as a voice session turn", async () => {
|
|
262
|
+
// The mark says a reply reaches the user over a session that is still
|
|
263
|
+
// open. Nothing is open here, so a reply that follows this row has to be
|
|
264
|
+
// pushed like any other.
|
|
265
|
+
const live = liveConversation("Chat keep voice mark");
|
|
266
|
+
try {
|
|
267
|
+
const attachmentId = await uploadFrame("frame.png");
|
|
268
|
+
|
|
269
|
+
expect(await persist(live.id, { attachmentId })).toMatchObject({
|
|
270
|
+
persisted: true,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(
|
|
274
|
+
metadataOf(getMessages(live.id)[0]).voiceSessionTurn,
|
|
275
|
+
).toBeUndefined();
|
|
276
|
+
} finally {
|
|
277
|
+
live.dispose();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("a frame from another actor makes the resident history reload", async () => {
|
|
282
|
+
// The persist stamps the ROW for the actor that sent it, but the message
|
|
283
|
+
// it appends to the resident history carries no scope of its own. A
|
|
284
|
+
// conversation resident under one actor would go on sending that array to
|
|
285
|
+
// the model, image included, while a reload would have filtered it out.
|
|
286
|
+
const live = liveConversation("Chat keep cross trust");
|
|
287
|
+
// Scope the resident history for the conversation's own actor.
|
|
288
|
+
await live.activeConversation.ensureActorScopedHistory();
|
|
289
|
+
const reload = spyOn(Conversation.prototype, "loadFromDb");
|
|
290
|
+
try {
|
|
291
|
+
const attachmentId = await uploadFrame("cross-trust.png");
|
|
292
|
+
// A principal no guardian binding names, which the vellum-channel
|
|
293
|
+
// resolver fails closed to `unknown`: not the class this history holds.
|
|
294
|
+
expect(
|
|
295
|
+
await persist(
|
|
296
|
+
live.id,
|
|
297
|
+
{ attachmentId },
|
|
298
|
+
{ "x-vellum-actor-principal-id": "actor-not-bound" },
|
|
299
|
+
),
|
|
300
|
+
).toMatchObject({ persisted: true });
|
|
301
|
+
|
|
302
|
+
await live.activeConversation.ensureActorScopedHistory();
|
|
303
|
+
|
|
304
|
+
// Reloaded and re-filtered rather than reusing the array the frame was
|
|
305
|
+
// appended to.
|
|
306
|
+
expect(reload).toHaveBeenCalled();
|
|
307
|
+
} finally {
|
|
308
|
+
reload.mockRestore();
|
|
309
|
+
live.dispose();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a frame from the resident actor does not force a reload", async () => {
|
|
314
|
+
// The control on the check above. Frames arrive on a camera's cadence, so
|
|
315
|
+
// a reload apiece is a real cost and only a differing scope earns one.
|
|
316
|
+
const live = liveConversation("Chat keep same trust");
|
|
317
|
+
await live.activeConversation.ensureActorScopedHistory();
|
|
318
|
+
const reload = spyOn(Conversation.prototype, "loadFromDb");
|
|
319
|
+
try {
|
|
320
|
+
const attachmentId = await uploadFrame("same-trust.png");
|
|
321
|
+
// No actor header, which the resolver reads as the guardian: the class
|
|
322
|
+
// the resident history is already scoped for.
|
|
323
|
+
expect(await persist(live.id, { attachmentId })).toMatchObject({
|
|
324
|
+
persisted: true,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await live.activeConversation.ensureActorScopedHistory();
|
|
328
|
+
|
|
329
|
+
expect(reload).not.toHaveBeenCalled();
|
|
330
|
+
} finally {
|
|
331
|
+
reload.mockRestore();
|
|
332
|
+
live.dispose();
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("reports a frame whose attachment does not resolve", async () => {
|
|
337
|
+
const live = liveConversation("Chat keep missing attachment");
|
|
338
|
+
try {
|
|
339
|
+
expect(await persist(live.id, { attachmentId: "att-missing" })).toEqual({
|
|
340
|
+
persisted: false,
|
|
341
|
+
});
|
|
342
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
343
|
+
} finally {
|
|
344
|
+
live.dispose();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("two overlapping keeps write one at a time", async () => {
|
|
349
|
+
// The processing flag is a boolean rather than a counted lock, so two
|
|
350
|
+
// persists in flight at once would both read it free and both take it.
|
|
351
|
+
const live = liveConversation("Chat keep serialized");
|
|
352
|
+
try {
|
|
353
|
+
const first = await uploadFrame("first.png");
|
|
354
|
+
const second = await uploadFrame("second.png");
|
|
355
|
+
|
|
356
|
+
live.activeConversation.setProcessing(true);
|
|
357
|
+
const firstKeep = persist(live.id, { attachmentId: first });
|
|
358
|
+
// Long enough for the first keep to reach the idle wait, so the second
|
|
359
|
+
// queues behind a job that has already begun rather than replacing it.
|
|
360
|
+
await sleep(30);
|
|
361
|
+
const secondKeep = persist(live.id, { attachmentId: second });
|
|
362
|
+
await sleep(30);
|
|
363
|
+
live.activeConversation.setProcessing(false);
|
|
364
|
+
|
|
365
|
+
expect(await firstKeep).toMatchObject({ persisted: true });
|
|
366
|
+
expect(await secondKeep).toMatchObject({ persisted: true });
|
|
367
|
+
expect(getMessages(live.id)).toHaveLength(2);
|
|
368
|
+
} finally {
|
|
369
|
+
live.dispose();
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("a newer keep replaces one still waiting, and its upload is reclaimed", async () => {
|
|
374
|
+
// What bounds the chain: keeps arrive every few seconds while each job can
|
|
375
|
+
// wait out a turn for far longer. The displaced upload is the daemon's to
|
|
376
|
+
// collect, because the client's own abandon-delete fires on a refused
|
|
377
|
+
// send and this send was accepted.
|
|
378
|
+
const live = liveConversation("Chat keep superseded");
|
|
379
|
+
try {
|
|
380
|
+
const running = await uploadFrame("running.png");
|
|
381
|
+
const stale = await uploadFrame("stale.png");
|
|
382
|
+
const newest = await uploadFrame("newest.png");
|
|
383
|
+
|
|
384
|
+
live.activeConversation.setProcessing(true);
|
|
385
|
+
const runningKeep = persist(live.id, { attachmentId: running });
|
|
386
|
+
await sleep(30);
|
|
387
|
+
const staleKeep = persist(live.id, { attachmentId: stale });
|
|
388
|
+
const newestKeep = persist(live.id, { attachmentId: newest });
|
|
389
|
+
await sleep(30);
|
|
390
|
+
live.activeConversation.setProcessing(false);
|
|
391
|
+
|
|
392
|
+
expect(await runningKeep).toMatchObject({ persisted: true });
|
|
393
|
+
expect(await staleKeep).toEqual({ persisted: false });
|
|
394
|
+
expect(await newestKeep).toMatchObject({ persisted: true });
|
|
395
|
+
|
|
396
|
+
expect(getMessages(live.id)).toHaveLength(2);
|
|
397
|
+
expect(frameStored(stale)).toBe(false);
|
|
398
|
+
expect(frameStored(running)).toBe(true);
|
|
399
|
+
expect(frameStored(newest)).toBe(true);
|
|
400
|
+
} finally {
|
|
401
|
+
live.dispose();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("a deletion is final for a keep still queued behind another", async () => {
|
|
406
|
+
// The route's 404 speaks only for the moment it ran. A keep queued behind
|
|
407
|
+
// another one starts later, so the job re-reads the conversation before it
|
|
408
|
+
// touches anything: the persist reaches it through
|
|
409
|
+
// `getOrCreateConversation`, which writes the row back for an id it finds
|
|
410
|
+
// missing, and a deleted conversation must not return carrying nothing but
|
|
411
|
+
// camera frames.
|
|
412
|
+
const restoreWait = _setProcessingWaitMsForTests(150);
|
|
413
|
+
const live = liveConversation("Chat keep deleted mid-queue");
|
|
414
|
+
const holder = await uploadFrame("holder.png");
|
|
415
|
+
const queued = await uploadFrame("queued.png");
|
|
416
|
+
try {
|
|
417
|
+
// The first keep holds the chain by waiting out a turn that never ends,
|
|
418
|
+
// so the second is still queued and has touched nothing.
|
|
419
|
+
live.activeConversation.setProcessing(true);
|
|
420
|
+
const holderKeep = persist(live.id, { attachmentId: holder });
|
|
421
|
+
await sleep(30);
|
|
422
|
+
const queuedKeep = persist(live.id, { attachmentId: queued });
|
|
423
|
+
|
|
424
|
+
deleteEverywhere(live.id);
|
|
425
|
+
const countAfterDelete = conversationCount();
|
|
426
|
+
|
|
427
|
+
expect(await holderKeep).toEqual({ persisted: false });
|
|
428
|
+
expect(await queuedKeep).toEqual({ persisted: false });
|
|
429
|
+
|
|
430
|
+
expect(getConversation(live.id)).toBeNull();
|
|
431
|
+
expect(conversationCount()).toBe(countAfterDelete);
|
|
432
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
433
|
+
expect(frameStored(queued)).toBe(false);
|
|
434
|
+
} finally {
|
|
435
|
+
_setProcessingWaitMsForTests(restoreWait);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("a frame does not land in a conversation replaced while the actor resolves", async () => {
|
|
440
|
+
// The 404 gate is where the frame is accepted, and resolving the request's
|
|
441
|
+
// actor is awaited after it. A delete and recreate under this id inside
|
|
442
|
+
// that await leaves a row the gate never saw, and a persist that read the
|
|
443
|
+
// id for itself afterwards would take the replacement for the conversation
|
|
444
|
+
// the client addressed.
|
|
445
|
+
const live = liveConversation("Chat keep replaced mid-trust");
|
|
446
|
+
try {
|
|
447
|
+
const frame = await uploadFrame("mid-trust.png");
|
|
448
|
+
|
|
449
|
+
duringTrustResolution = () => {
|
|
450
|
+
deleteConversationRows(live.id);
|
|
451
|
+
createConversation({ id: live.id, title: "Recreated" });
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
expect(await persist(live.id, { attachmentId: frame })).toEqual({
|
|
455
|
+
persisted: false,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
459
|
+
// Nothing else would ever collect it: the client's abandon-delete fires
|
|
460
|
+
// on a refused send, and this send was accepted.
|
|
461
|
+
expect(frameStored(frame)).toBe(false);
|
|
462
|
+
} finally {
|
|
463
|
+
duringTrustResolution = null;
|
|
464
|
+
live.dispose();
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("reports a keep that outwaited the turn ahead of it", async () => {
|
|
469
|
+
const restoreWait = _setProcessingWaitMsForTests(120);
|
|
470
|
+
const live = liveConversation("Chat keep wait timeout");
|
|
471
|
+
try {
|
|
472
|
+
const frame = await uploadFrame("timed-out.png");
|
|
473
|
+
|
|
474
|
+
live.activeConversation.setProcessing(true);
|
|
475
|
+
expect(await persist(live.id, { attachmentId: frame })).toEqual({
|
|
476
|
+
persisted: false,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(frameStored(frame)).toBe(false);
|
|
480
|
+
expect(getMessages(live.id)).toHaveLength(0);
|
|
481
|
+
} finally {
|
|
482
|
+
live.activeConversation.setProcessing(false);
|
|
483
|
+
live.dispose();
|
|
484
|
+
_setProcessingWaitMsForTests(restoreWait);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
@@ -204,7 +204,7 @@ function awaitRouteApproval(args: {
|
|
|
204
204
|
broadcastMessage(confirmationMsg, conversationId);
|
|
205
205
|
|
|
206
206
|
// Promote the confirmation to a guardian request so channel
|
|
207
|
-
// guardian decisions (
|
|
207
|
+
// guardian decisions (buttons, text) can resolve it.
|
|
208
208
|
void createGuardianRequestForConfirmation(confirmationMsg, conversationId);
|
|
209
209
|
});
|
|
210
210
|
}
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
* they release only after that burst so the next queued message cannot start
|
|
10
10
|
* processing ahead of the reply the client is still being told about.
|
|
11
11
|
*
|
|
12
|
-
* The release
|
|
13
|
-
*
|
|
14
|
-
* loop
|
|
15
|
-
*
|
|
12
|
+
* The release runs from a `finally`, because a throw from any broadcast in the
|
|
13
|
+
* burst would otherwise latch the conversation "processing" with no agent
|
|
14
|
+
* loop and no later path left to clear it, so every subsequent send is queued
|
|
15
|
+
* behind an idle conversation for the life of the daemon.
|
|
16
|
+
*
|
|
17
|
+
* It names the claim its scheduler took. The timer fires a tick after the
|
|
18
|
+
* route returned, and a Stop in between force-clears the flag and lets the
|
|
19
|
+
* next request acquire, so a bare clear here would release a turn that is
|
|
20
|
+
* running and kick the queue into it. Naming the claim makes that the no-op
|
|
21
|
+
* the ownership design already provides: the newer hold is somebody else's to
|
|
22
|
+
* release, and the follow-up work waits for whoever holds it.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
import type { Conversation } from "../../daemon/conversation.js";
|
|
@@ -23,11 +30,13 @@ const log = getLogger("canned-reply-release");
|
|
|
23
30
|
/** The conversation surface {@link scheduleCannedReplyRelease} touches. */
|
|
24
31
|
export type CannedReplyReleaseTarget = Pick<
|
|
25
32
|
Conversation,
|
|
26
|
-
"
|
|
33
|
+
"releaseProcessing" | "kickDrainQueue"
|
|
27
34
|
>;
|
|
28
35
|
|
|
29
36
|
export function scheduleCannedReplyRelease(params: {
|
|
30
37
|
conversation: CannedReplyReleaseTarget;
|
|
38
|
+
/** The claim the scheduling route holds, and the only one this releases. */
|
|
39
|
+
owner: number;
|
|
31
40
|
/** Names the release site in drain logs (e.g. `"canned_greeting"`). */
|
|
32
41
|
origin: string;
|
|
33
42
|
/** The deferred client event burst. Runs before the release. */
|
|
@@ -35,7 +44,7 @@ export function scheduleCannedReplyRelease(params: {
|
|
|
35
44
|
/** Optional work that only makes sense once the lock is free. */
|
|
36
45
|
afterRelease?: () => void;
|
|
37
46
|
}): void {
|
|
38
|
-
const { conversation, origin, emit, afterRelease } = params;
|
|
47
|
+
const { conversation, owner, origin, emit, afterRelease } = params;
|
|
39
48
|
setTimeout(() => {
|
|
40
49
|
try {
|
|
41
50
|
emit();
|
|
@@ -46,9 +55,10 @@ export function scheduleCannedReplyRelease(params: {
|
|
|
46
55
|
// reconcile a missed event burst on their next fetch.
|
|
47
56
|
log.error({ err, origin }, "Canned reply event burst failed");
|
|
48
57
|
} finally {
|
|
49
|
-
conversation.
|
|
50
|
-
|
|
58
|
+
if (conversation.releaseProcessing(owner)) {
|
|
59
|
+
void conversation.kickDrainQueue("loop_complete", origin);
|
|
60
|
+
afterRelease?.();
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
|
-
afterRelease?.();
|
|
53
63
|
}, 0);
|
|
54
64
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ChannelId } from "../../channels/types.js";
|
|
5
5
|
import {
|
|
6
|
-
type ApprovalAction,
|
|
7
6
|
type ApprovalDecisionResult,
|
|
8
7
|
type ApprovalUIMetadata,
|
|
9
8
|
isApprovalAction,
|
|
@@ -58,44 +57,6 @@ export function parseCallbackData(
|
|
|
58
57
|
return { action, source, requestId };
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Reaction decision vocabulary
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Map of reaction emoji to approval actions, in each channel's own
|
|
67
|
-
* vocabulary: Slack reactions carry colon names (with aliasing, e.g. `+1`
|
|
68
|
-
* and `thumbsup` are both the thumbs-up emoji), Telegram and Discord carry
|
|
69
|
-
* the unicode character itself. Telegram's fixed reaction set offers the
|
|
70
|
-
* thumbs but not the check mark or alarm clock; Discord offers all four.
|
|
71
|
-
*/
|
|
72
|
-
const REACTION_EMOJI_MAP: ReadonlyMap<string, ApprovalAction> = new Map([
|
|
73
|
-
["+1", "approve_once"],
|
|
74
|
-
["thumbsup", "approve_once"],
|
|
75
|
-
["white_check_mark", "approve_once"],
|
|
76
|
-
["alarm_clock", "approve_once"],
|
|
77
|
-
["-1", "reject"],
|
|
78
|
-
["thumbsdown", "reject"],
|
|
79
|
-
["\u{1F44D}", "approve_once"], // thumbs up
|
|
80
|
-
["\u2705", "approve_once"], // check mark button
|
|
81
|
-
["\u23F0", "approve_once"], // alarm clock
|
|
82
|
-
["\u{1F44E}", "reject"], // thumbs down
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Map a reaction emoji, in the source channel's own vocabulary, to an
|
|
87
|
-
* approval decision. Returns null if the emoji is not mapped to any action.
|
|
88
|
-
*/
|
|
89
|
-
export function reactionDecisionForEmoji(
|
|
90
|
-
emoji: string,
|
|
91
|
-
): ApprovalDecisionResult | null {
|
|
92
|
-
const action = REACTION_EMOJI_MAP.get(emoji);
|
|
93
|
-
if (!action) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
return { action, source: "reaction" };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
60
|
// ---------------------------------------------------------------------------
|
|
100
61
|
// Context builders
|
|
101
62
|
// ---------------------------------------------------------------------------
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
createInboundChallenge,
|
|
29
29
|
getVerificationStatus,
|
|
30
30
|
revokeVerificationForChannel,
|
|
31
|
+
type VerificationStatusResponse,
|
|
32
|
+
verificationStatusResponseSchema,
|
|
31
33
|
verifyTrustedContact,
|
|
32
34
|
} from "../../daemon/handlers/config-channels.js";
|
|
33
35
|
import { normalizePhoneNumber } from "../../util/phone.js";
|
|
@@ -182,7 +184,7 @@ export async function handleCreateVerificationSession({
|
|
|
182
184
|
async function handleGetVerificationStatus({
|
|
183
185
|
queryParams = {},
|
|
184
186
|
body = {},
|
|
185
|
-
}: RouteHandlerArgs) {
|
|
187
|
+
}: RouteHandlerArgs): Promise<VerificationStatusResponse> {
|
|
186
188
|
const channel = (queryParams.channel ??
|
|
187
189
|
(body as Record<string, unknown>).channel) as ChannelId | undefined;
|
|
188
190
|
return await getVerificationStatus(channel);
|
|
@@ -356,6 +358,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
356
358
|
description: "Optional channel ID filter",
|
|
357
359
|
},
|
|
358
360
|
],
|
|
361
|
+
responseBody: verificationStatusResponseSchema,
|
|
359
362
|
handler: handleGetVerificationStatus,
|
|
360
363
|
},
|
|
361
364
|
];
|