@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
|
@@ -162,6 +162,7 @@ mock.module("../ipc/gateway-client.js", () => ({
|
|
|
162
162
|
ipcCall: ipcCallMock,
|
|
163
163
|
}));
|
|
164
164
|
|
|
165
|
+
import { CONVERSATION_BUSY_MESSAGE } from "../daemon/conversation-messaging.js";
|
|
165
166
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
166
167
|
import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
|
|
167
168
|
import { callHandler } from "./helpers/call-route-handler.js";
|
|
@@ -226,6 +227,7 @@ function makeConversation() {
|
|
|
226
227
|
const events: unknown[] = [];
|
|
227
228
|
const messages: unknown[] = [];
|
|
228
229
|
let processing = false;
|
|
230
|
+
let owner = 0;
|
|
229
231
|
const conversation = {
|
|
230
232
|
conversationId: "conv-slash-test",
|
|
231
233
|
messages,
|
|
@@ -244,6 +246,23 @@ function makeConversation() {
|
|
|
244
246
|
isProcessing: () => processing,
|
|
245
247
|
setProcessing: (value: boolean) => {
|
|
246
248
|
processing = value;
|
|
249
|
+
owner = value ? owner + 1 : 0;
|
|
250
|
+
},
|
|
251
|
+
acquireProcessingFenced: async () => {
|
|
252
|
+
if (processing) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
processing = true;
|
|
256
|
+
owner += 1;
|
|
257
|
+
return owner;
|
|
258
|
+
},
|
|
259
|
+
releaseProcessing: (claim: number) => {
|
|
260
|
+
if (claim !== owner) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
processing = false;
|
|
264
|
+
owner = 0;
|
|
265
|
+
return true;
|
|
247
266
|
},
|
|
248
267
|
hasAnyPendingConfirmation: () => false,
|
|
249
268
|
denyAllPendingConfirmations: () => {},
|
|
@@ -530,6 +549,36 @@ describe("handleSendMessage canned wake-up greeting", () => {
|
|
|
530
549
|
}
|
|
531
550
|
});
|
|
532
551
|
|
|
552
|
+
test("queues a first greeting whose flag went away during the awaits", async () => {
|
|
553
|
+
// The canned wake-up greeting used to claim the flag unconditionally, so
|
|
554
|
+
// it greeted over a turn already writing and both mutated one history.
|
|
555
|
+
// It declines instead and falls through, the way its sibling branches do,
|
|
556
|
+
// and the gate below answers as a busy conversation owes.
|
|
557
|
+
const { conversation, persistUserMessage, runAgentLoop } =
|
|
558
|
+
makeConversation();
|
|
559
|
+
conversation.setProcessing(true);
|
|
560
|
+
const stub = conversation as unknown as { isProcessing: () => boolean };
|
|
561
|
+
stub.isProcessing = () => false;
|
|
562
|
+
// What the real one does while another holder has the flag, which is what
|
|
563
|
+
// the fall-through below runs into.
|
|
564
|
+
persistUserMessage.mockImplementationOnce(async () => {
|
|
565
|
+
throw new Error(CONVERSATION_BUSY_MESSAGE);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const res = await callHandler(
|
|
569
|
+
(args) => handleSendMessage(args, makeDeps(conversation)),
|
|
570
|
+
makeRequest("Wake up, my friend!"),
|
|
571
|
+
undefined,
|
|
572
|
+
202,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
expect(res.status).toBe(202);
|
|
576
|
+
expect(await res.json()).toMatchObject({ accepted: true, queued: true });
|
|
577
|
+
expect(runAgentLoop).not.toHaveBeenCalled();
|
|
578
|
+
// No greeting rows: the branch never ran.
|
|
579
|
+
expect(addMessageMock).not.toHaveBeenCalled();
|
|
580
|
+
});
|
|
581
|
+
|
|
533
582
|
test("persists the clientMessageId on the user row", async () => {
|
|
534
583
|
const { conversation, runAgentLoop } = makeConversation();
|
|
535
584
|
const res = await callHandler(
|
|
@@ -550,3 +599,79 @@ describe("handleSendMessage canned wake-up greeting", () => {
|
|
|
550
599
|
expect(options?.clientMessageId).toBe("nonce-wake-123");
|
|
551
600
|
});
|
|
552
601
|
});
|
|
602
|
+
|
|
603
|
+
describe("handleSendMessage flag taken after its queue decision", () => {
|
|
604
|
+
beforeEach(() => {
|
|
605
|
+
addMessageMock.mockClear();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("queues a plain send whose flag went away during the awaits", async () => {
|
|
609
|
+
// The route decides to queue or run at the top, then awaits guardian
|
|
610
|
+
// cleanup, history scoping and slash resolution. A camera frame taking the
|
|
611
|
+
// flag inside those awaits leaves the persist rejecting busy, and the send
|
|
612
|
+
// has to reach the queue the early decision would have used rather than
|
|
613
|
+
// failing in the client's face.
|
|
614
|
+
const { conversation, persistUserMessage, runAgentLoop } =
|
|
615
|
+
makeConversation();
|
|
616
|
+
persistUserMessage.mockImplementationOnce(async () => {
|
|
617
|
+
throw new Error(CONVERSATION_BUSY_MESSAGE);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const res = await callHandler(
|
|
621
|
+
(args) => handleSendMessage(args, makeDeps(conversation)),
|
|
622
|
+
makeRequest("what is this"),
|
|
623
|
+
undefined,
|
|
624
|
+
202,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
expect(res.status).toBe(202);
|
|
628
|
+
expect(await res.json()).toMatchObject({ accepted: true, queued: true });
|
|
629
|
+
expect(runAgentLoop).not.toHaveBeenCalled();
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
test("still fails a send whose persist failed for any other reason", async () => {
|
|
633
|
+
// The control: only the busy rejection queues. Everything else is a real
|
|
634
|
+
// failure and must not be answered as an accepted message.
|
|
635
|
+
const { conversation } = makeConversation();
|
|
636
|
+
const { persistUserMessage } = makeConversation();
|
|
637
|
+
void persistUserMessage;
|
|
638
|
+
(
|
|
639
|
+
conversation as unknown as {
|
|
640
|
+
persistUserMessage: ReturnType<typeof mock>;
|
|
641
|
+
}
|
|
642
|
+
).persistUserMessage = mock(async () => {
|
|
643
|
+
throw new Error("disk on fire");
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
await expect(
|
|
647
|
+
callHandler(
|
|
648
|
+
(args) => handleSendMessage(args, makeDeps(conversation)),
|
|
649
|
+
makeRequest("what is this"),
|
|
650
|
+
undefined,
|
|
651
|
+
202,
|
|
652
|
+
),
|
|
653
|
+
).rejects.toThrow("disk on fire");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("queues a slash command whose flag went away during the awaits", async () => {
|
|
657
|
+
// Same window, the branch that hand-rolls its own claim. It takes the flag
|
|
658
|
+
// rather than setting it, so a hold taken since is answered by the queue
|
|
659
|
+
// instead of being claimed away from the turn that owns it.
|
|
660
|
+
const { conversation, runAgentLoop } = makeConversation();
|
|
661
|
+
conversation.setProcessing(true);
|
|
662
|
+
// The early gate reads idle, the claim below does not.
|
|
663
|
+
const stub = conversation as unknown as { isProcessing: () => boolean };
|
|
664
|
+
stub.isProcessing = () => false;
|
|
665
|
+
|
|
666
|
+
const res = await callHandler(
|
|
667
|
+
(args) => handleSendMessage(args, makeDeps(conversation)),
|
|
668
|
+
makeRequest("/context"),
|
|
669
|
+
undefined,
|
|
670
|
+
202,
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
expect(res.status).toBe(202);
|
|
674
|
+
expect(await res.json()).toMatchObject({ accepted: true, queued: true });
|
|
675
|
+
expect(runAgentLoop).not.toHaveBeenCalled();
|
|
676
|
+
});
|
|
677
|
+
});
|
|
@@ -885,6 +885,28 @@ describe("trust-gating via channel capabilities", () => {
|
|
|
885
885
|
expect(injected).not.toContain("CHANNEL CONSTRAINTS");
|
|
886
886
|
});
|
|
887
887
|
|
|
888
|
+
test("vellum channel with Linux client OS injects host_bash guidance", () => {
|
|
889
|
+
const caps: ChannelCapabilities = {
|
|
890
|
+
channel: "vellum",
|
|
891
|
+
dashboardCapable: true,
|
|
892
|
+
supportsDynamicUi: true,
|
|
893
|
+
supportsVoiceInput: true,
|
|
894
|
+
clientOS: "web",
|
|
895
|
+
};
|
|
896
|
+
const message: Message = {
|
|
897
|
+
role: "user",
|
|
898
|
+
content: [{ type: "text", text: "List my downloads" }],
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
const result = injectChannelCapabilityContext(message, caps, "linux");
|
|
902
|
+
|
|
903
|
+
expect(result).not.toBe(message);
|
|
904
|
+
const injected = (result.content[0] as { type: "text"; text: string }).text;
|
|
905
|
+
expect(injected).toContain("client_os: linux");
|
|
906
|
+
expect(injected).toContain("On Linux, prefer CLI via `host_bash`");
|
|
907
|
+
expect(injected).not.toContain("CHANNEL CONSTRAINTS");
|
|
908
|
+
});
|
|
909
|
+
|
|
888
910
|
test("non-dashboard channel adds constraint rules preventing UI references", () => {
|
|
889
911
|
const caps = resolveChannelCapabilities("telegram");
|
|
890
912
|
const message: Message = {
|
|
@@ -6009,6 +6031,63 @@ describe("assembleSlackChronologicalMessages", () => {
|
|
|
6009
6031
|
expect(replyText).not.toContain("→ M");
|
|
6010
6032
|
});
|
|
6011
6033
|
|
|
6034
|
+
test("post-reconciliation: an assistant row on the neutral envelope renders chronologically too", () => {
|
|
6035
|
+
// A Slack reply row is written with the neutral envelope every channel
|
|
6036
|
+
// writes; the chronological assembler reads it through the envelope's
|
|
6037
|
+
// Slack view, so it takes the same path as a legacy `slackMeta` row.
|
|
6038
|
+
const SLACK_CHANNEL_ID_2 = "C0THREAD";
|
|
6039
|
+
const ASSISTANT_TS = "1700001000.000111";
|
|
6040
|
+
const REPLY_TS = "1700001020.000222";
|
|
6041
|
+
const SLACK_CAPS_CHANNEL: ChannelCapabilities = {
|
|
6042
|
+
channel: "slack",
|
|
6043
|
+
dashboardCapable: false,
|
|
6044
|
+
supportsDynamicUi: false,
|
|
6045
|
+
supportsVoiceInput: false,
|
|
6046
|
+
chatType: "channel",
|
|
6047
|
+
};
|
|
6048
|
+
const userReplyMeta: SlackMessageMetadata = {
|
|
6049
|
+
source: "slack",
|
|
6050
|
+
channelId: SLACK_CHANNEL_ID_2,
|
|
6051
|
+
channelTs: REPLY_TS,
|
|
6052
|
+
threadTs: ASSISTANT_TS,
|
|
6053
|
+
displayName: "@alice",
|
|
6054
|
+
eventKind: "message",
|
|
6055
|
+
};
|
|
6056
|
+
const rows: SlackTranscriptInputRow[] = [
|
|
6057
|
+
row(
|
|
6058
|
+
"assistant",
|
|
6059
|
+
"Earlier reply",
|
|
6060
|
+
1700001000_000,
|
|
6061
|
+
JSON.stringify({
|
|
6062
|
+
userMessageChannel: "slack",
|
|
6063
|
+
assistantMessageChannel: "slack",
|
|
6064
|
+
providerMeta: JSON.stringify({
|
|
6065
|
+
source: "slack",
|
|
6066
|
+
conversationExternalId: SLACK_CHANNEL_ID_2,
|
|
6067
|
+
messageId: ASSISTANT_TS,
|
|
6068
|
+
eventKind: "message",
|
|
6069
|
+
}),
|
|
6070
|
+
}),
|
|
6071
|
+
),
|
|
6072
|
+
row(
|
|
6073
|
+
"user",
|
|
6074
|
+
"Following up",
|
|
6075
|
+
1700001020_000,
|
|
6076
|
+
metadataEnvelope(userReplyMeta),
|
|
6077
|
+
),
|
|
6078
|
+
];
|
|
6079
|
+
|
|
6080
|
+
const result = assembleSlackChronologicalMessages(rows, SLACK_CAPS_CHANNEL);
|
|
6081
|
+
expect(result).not.toBeNull();
|
|
6082
|
+
expect(result!.length).toBe(2);
|
|
6083
|
+
const assistantText = (result![0].content[0] as { text: string }).text;
|
|
6084
|
+
expect(assistantText).toContain("Earlier reply");
|
|
6085
|
+
const replyText = (result![1].content[0] as { text: string }).text;
|
|
6086
|
+
expect(replyText).toBe(
|
|
6087
|
+
`[11/14/23 22:30 @alice]: ${slackExternal("Following up", "@alice")}`,
|
|
6088
|
+
);
|
|
6089
|
+
});
|
|
6090
|
+
|
|
6012
6091
|
test("post-reconciliation: assistant row appears in active-thread focus block", () => {
|
|
6013
6092
|
// The active-thread focus block at
|
|
6014
6093
|
// `conversation-runtime-assembly.ts:1387` filters out rows with null
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* When an acquire writes a `conversations` row, and when it refuses to.
|
|
3
|
+
*
|
|
2
4
|
* Regression: an ephemeral `getOrCreateConversation` call (the empty-state
|
|
3
5
|
* greeting side-chain via POST /v1/btw) must NOT persist a `conversations`
|
|
4
6
|
* row. A persisted row surfaces as an "Untitled" conversation with the literal
|
|
5
7
|
* id "greeting" in every client's sidebar. A normal (non-ephemeral) call still
|
|
6
8
|
* creates the row so real conversations remain sidebar-visible.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* `getConversationIfExists` refuses for a different reason: its callers run
|
|
11
|
+
* queued work whose conversation can be deleted before the job starts, and
|
|
12
|
+
* writing the row back would bring the conversation home. The window that
|
|
13
|
+
* matters is the provider, prompt, and tool setup the acquire awaits, which a
|
|
14
|
+
* delete can land in the middle of.
|
|
15
|
+
*
|
|
16
|
+
* The DB layer is mocked so the assertions target exactly the row-creation
|
|
17
|
+
* decision inside the acquire (`createConversation` /
|
|
10
18
|
* `ensureConversationExists`) rather than the surrounding provider wiring.
|
|
11
19
|
*/
|
|
12
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
20
|
+
import { describe, expect, mock, spyOn, test } from "bun:test";
|
|
13
21
|
|
|
14
22
|
import type { Message } from "../providers/types.js";
|
|
15
23
|
import { setConfig } from "./helpers/set-config.js";
|
|
@@ -46,8 +54,51 @@ setConfig("llm", {
|
|
|
46
54
|
});
|
|
47
55
|
setConfig("memory", { enabled: false, v2: { enabled: false } });
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Whether the `conversations` row reads as present. Default false, which is
|
|
59
|
+
* the brand-new conversation the row-creation tests below need.
|
|
60
|
+
*/
|
|
61
|
+
let conversationRowPresent = false;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The `created_at` the row reads with, which is what tells one incarnation of
|
|
65
|
+
* an id from the next. Every write below stamps a fresh one, the way an insert
|
|
66
|
+
* does.
|
|
67
|
+
*/
|
|
68
|
+
let conversationCreatedAt = 1000;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Set to model a delete that lands while the acquire is awaiting its setup
|
|
72
|
+
* work: the row reads present when the acquire starts and gone by the time it
|
|
73
|
+
* would insert.
|
|
74
|
+
*/
|
|
75
|
+
let deleteDuringSetup = false;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Set to model a delete that lands while the acquire is hydrating the instance
|
|
79
|
+
* it just built, which is the window before it reaches the registry.
|
|
80
|
+
*/
|
|
81
|
+
let deleteDuringHydration = false;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Set to hold an acquire inside its setup work until the test resolves it,
|
|
85
|
+
* which is what lets a second acquire join the first one's flight and lets a
|
|
86
|
+
* delete land while both are waiting on it.
|
|
87
|
+
*/
|
|
88
|
+
let holdSetup: Promise<void> | null = null;
|
|
89
|
+
|
|
49
90
|
mock.module("../prompts/system-prompt.js", () => ({
|
|
50
|
-
buildSystemPrompt: () =>
|
|
91
|
+
buildSystemPrompt: async () => {
|
|
92
|
+
// Awaited by the acquire between its first read of the row and the insert,
|
|
93
|
+
// so this is the window a delete has to land in.
|
|
94
|
+
if (deleteDuringSetup) {
|
|
95
|
+
conversationRowPresent = false;
|
|
96
|
+
}
|
|
97
|
+
if (holdSetup) {
|
|
98
|
+
await holdSetup;
|
|
99
|
+
}
|
|
100
|
+
return "system prompt";
|
|
101
|
+
},
|
|
51
102
|
}));
|
|
52
103
|
|
|
53
104
|
mock.module("../prompts/persona-resolver.js", () => ({
|
|
@@ -77,17 +128,45 @@ mock.module("../workspace/git-service.js", () => ({
|
|
|
77
128
|
}),
|
|
78
129
|
}));
|
|
79
130
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
/** Write the row the way an insert does, under a fresh `created_at`. */
|
|
132
|
+
function writeConversationRow(): void {
|
|
133
|
+
conversationRowPresent = true;
|
|
134
|
+
conversationCreatedAt += 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The row-creation spies. `getConversation` answers from
|
|
138
|
+
// `conversationRowPresent`, which starts false so the conversation reads as
|
|
139
|
+
// brand-new: the branch that would create a row. Both spies leave the row
|
|
140
|
+
// readable under a `created_at` of their own, which is what lets a test tell
|
|
141
|
+
// the conversation an acquire was asked about from one written since.
|
|
142
|
+
const mockCreateConversation = mock((_opts?: unknown) => {
|
|
143
|
+
writeConversationRow();
|
|
144
|
+
return { id: "conv-x" };
|
|
145
|
+
});
|
|
146
|
+
const mockEnsureConversationExists = mock((_id: string) => {
|
|
147
|
+
// Matches the real one, which reports false and writes nothing when the row
|
|
148
|
+
// is already there.
|
|
149
|
+
if (conversationRowPresent) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
writeConversationRow();
|
|
153
|
+
return true;
|
|
154
|
+
});
|
|
84
155
|
|
|
85
156
|
mock.module("../persistence/conversation-crud.js", () => ({
|
|
86
157
|
ADOPTABLE_CONVERSATION_ID_RE: /^[A-Za-z0-9_-]{1,128}$/,
|
|
87
158
|
createConversation: mockCreateConversation,
|
|
88
159
|
ensureConversationExists: mockEnsureConversationExists,
|
|
89
|
-
getConversation: () =>
|
|
90
|
-
|
|
160
|
+
getConversation: (id: string) =>
|
|
161
|
+
conversationRowPresent ? { id, createdAt: conversationCreatedAt } : null,
|
|
162
|
+
getMessages: () => {
|
|
163
|
+
// Read by `loadFromDb`, which the acquire awaits after building the
|
|
164
|
+
// instance and before it reaches the registry.
|
|
165
|
+
if (deleteDuringHydration) {
|
|
166
|
+
conversationRowPresent = false;
|
|
167
|
+
}
|
|
168
|
+
return [];
|
|
169
|
+
},
|
|
91
170
|
setConversationProcessingStartedAt: () => {},
|
|
92
171
|
isConversationProcessing: () => false,
|
|
93
172
|
}));
|
|
@@ -139,11 +218,28 @@ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
|
139
218
|
getSummaryFromContextMessage: () => null,
|
|
140
219
|
}));
|
|
141
220
|
|
|
221
|
+
import { Conversation } from "../daemon/conversation.js";
|
|
222
|
+
import {
|
|
223
|
+
conversationCount,
|
|
224
|
+
findConversation,
|
|
225
|
+
} from "../daemon/conversation-registry.js";
|
|
142
226
|
import {
|
|
143
227
|
clearAllActiveConversations,
|
|
228
|
+
getConversationIfExists,
|
|
144
229
|
getOrCreateConversation,
|
|
145
230
|
} from "../daemon/conversation-store.js";
|
|
146
231
|
|
|
232
|
+
function resetAcquireState(): void {
|
|
233
|
+
clearAllActiveConversations();
|
|
234
|
+
mockCreateConversation.mockClear();
|
|
235
|
+
mockEnsureConversationExists.mockClear();
|
|
236
|
+
conversationRowPresent = false;
|
|
237
|
+
conversationCreatedAt += 1;
|
|
238
|
+
deleteDuringSetup = false;
|
|
239
|
+
deleteDuringHydration = false;
|
|
240
|
+
holdSetup = null;
|
|
241
|
+
}
|
|
242
|
+
|
|
147
243
|
describe("getOrCreateConversation ephemeral flag", () => {
|
|
148
244
|
test("ephemeral call does not persist a conversations row", async () => {
|
|
149
245
|
clearAllActiveConversations();
|
|
@@ -172,3 +268,220 @@ describe("getOrCreateConversation ephemeral flag", () => {
|
|
|
172
268
|
);
|
|
173
269
|
});
|
|
174
270
|
});
|
|
271
|
+
|
|
272
|
+
describe("getConversationIfExists", () => {
|
|
273
|
+
test("returns null for a conversation whose row is already gone", async () => {
|
|
274
|
+
resetAcquireState();
|
|
275
|
+
|
|
276
|
+
expect(await getConversationIfExists("deleted-conversation")).toBeNull();
|
|
277
|
+
|
|
278
|
+
expect(mockEnsureConversationExists).not.toHaveBeenCalled();
|
|
279
|
+
expect(mockCreateConversation).not.toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("returns null when the delete lands during provider and tool setup", async () => {
|
|
283
|
+
// The window the up-front read cannot cover: the acquire awaits provider
|
|
284
|
+
// resolution, the system prompt, and tool setup before it reaches the
|
|
285
|
+
// insert, and a delete inside those awaits used to be answered by writing
|
|
286
|
+
// the row back.
|
|
287
|
+
resetAcquireState();
|
|
288
|
+
conversationRowPresent = true;
|
|
289
|
+
deleteDuringSetup = true;
|
|
290
|
+
|
|
291
|
+
expect(await getConversationIfExists("deleted-mid-setup")).toBeNull();
|
|
292
|
+
|
|
293
|
+
expect(mockEnsureConversationExists).not.toHaveBeenCalled();
|
|
294
|
+
expect(mockCreateConversation).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("builds the conversation when its row survives the setup", async () => {
|
|
298
|
+
resetAcquireState();
|
|
299
|
+
conversationRowPresent = true;
|
|
300
|
+
|
|
301
|
+
const conversation = await getConversationIfExists("live-conversation");
|
|
302
|
+
|
|
303
|
+
expect(conversation).not.toBeNull();
|
|
304
|
+
// The row was there all along, so nothing had to write one.
|
|
305
|
+
expect(mockEnsureConversationExists).not.toHaveBeenCalled();
|
|
306
|
+
expect(mockCreateConversation).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("does not inherit an instance from the acquire it joined", async () => {
|
|
310
|
+
// Both acquires share one flight, and they do not share a contract. The
|
|
311
|
+
// creating caller builds its conversation whatever became of the row,
|
|
312
|
+
// which is its own business; an observer that took the same instance
|
|
313
|
+
// would be answering someone else's question.
|
|
314
|
+
resetAcquireState();
|
|
315
|
+
conversationRowPresent = true;
|
|
316
|
+
let releaseSetup = () => {};
|
|
317
|
+
holdSetup = new Promise<void>((resolve) => {
|
|
318
|
+
releaseSetup = resolve;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Ephemeral, so the creating acquire never writes the row back and the
|
|
322
|
+
// delete below stands.
|
|
323
|
+
const creating = getOrCreateConversation("joined-conversation", {
|
|
324
|
+
ephemeral: true,
|
|
325
|
+
});
|
|
326
|
+
// Reaches the flight the creating acquire registered before its first
|
|
327
|
+
// await, and waits on it.
|
|
328
|
+
const observing = getConversationIfExists("joined-conversation");
|
|
329
|
+
|
|
330
|
+
conversationRowPresent = false;
|
|
331
|
+
releaseSetup();
|
|
332
|
+
|
|
333
|
+
expect(await creating).not.toBeNull();
|
|
334
|
+
expect(await observing).toBeNull();
|
|
335
|
+
expect(mockEnsureConversationExists).not.toHaveBeenCalled();
|
|
336
|
+
expect(mockCreateConversation).not.toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("does not accept a row the acquire it joined wrote back", async () => {
|
|
340
|
+
// The creating caller it shares flight with writes the row back when the
|
|
341
|
+
// delete lands inside that caller's setup, so by the time this one looks
|
|
342
|
+
// the id names a row again. Reading presence alone cannot tell that row
|
|
343
|
+
// from the one this caller was asked about, and persisting into it would
|
|
344
|
+
// put the frame in a conversation created after the deletion.
|
|
345
|
+
resetAcquireState();
|
|
346
|
+
conversationRowPresent = true;
|
|
347
|
+
let releaseSetup = () => {};
|
|
348
|
+
holdSetup = new Promise<void>((resolve) => {
|
|
349
|
+
releaseSetup = resolve;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const creating = getOrCreateConversation("recreated-conversation");
|
|
353
|
+
const observing = getConversationIfExists("recreated-conversation");
|
|
354
|
+
|
|
355
|
+
conversationRowPresent = false;
|
|
356
|
+
releaseSetup();
|
|
357
|
+
|
|
358
|
+
// The creating caller builds whatever became of the row, which is its own
|
|
359
|
+
// contract and unchanged.
|
|
360
|
+
expect(await creating).not.toBeNull();
|
|
361
|
+
expect(mockEnsureConversationExists).toHaveBeenCalledTimes(1);
|
|
362
|
+
expect(conversationRowPresent).toBe(true);
|
|
363
|
+
|
|
364
|
+
expect(await observing).toBeNull();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("disposes rather than registers an instance whose row went away", async () => {
|
|
368
|
+
// Hydration awaits, so the row can go between the read that let this build
|
|
369
|
+
// and the registry write. Registering leaves an active conversation with
|
|
370
|
+
// no row behind it, which later session work finds and reuses.
|
|
371
|
+
resetAcquireState();
|
|
372
|
+
conversationRowPresent = true;
|
|
373
|
+
deleteDuringHydration = true;
|
|
374
|
+
const disposeSpy = spyOn(Conversation.prototype, "dispose");
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
expect(await getConversationIfExists("hydrated-conversation")).toBeNull();
|
|
378
|
+
|
|
379
|
+
expect(findConversation("hydrated-conversation")).toBeUndefined();
|
|
380
|
+
expect(conversationCount()).toBe(0);
|
|
381
|
+
expect(conversationRowPresent).toBe(false);
|
|
382
|
+
// The instance it built is torn down rather than left to leak its
|
|
383
|
+
// timers and proxies.
|
|
384
|
+
expect(disposeSpy).toHaveBeenCalledTimes(1);
|
|
385
|
+
} finally {
|
|
386
|
+
disposeSpy.mockRestore();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("acquisitions sharing one flight", () => {
|
|
392
|
+
test("creating callers behind a declined flight build one conversation", async () => {
|
|
393
|
+
// A flight that declines to create leaves everyone waiting on it with no
|
|
394
|
+
// answer, and each of them was asked to build. Building unconditionally
|
|
395
|
+
// from there gives one id two instances: the registry keeps whichever was
|
|
396
|
+
// written last, and the other one runs with its own processing flag and
|
|
397
|
+
// its own queue.
|
|
398
|
+
resetAcquireState();
|
|
399
|
+
conversationRowPresent = true;
|
|
400
|
+
let releaseSetup = () => {};
|
|
401
|
+
holdSetup = new Promise<void>((resolve) => {
|
|
402
|
+
releaseSetup = resolve;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Passes its up-front read, publishes the flight, then waits inside setup.
|
|
406
|
+
const observing = getConversationIfExists("herd-conversation");
|
|
407
|
+
// Both reach that flight and wait on it.
|
|
408
|
+
const firstCreating = getOrCreateConversation("herd-conversation");
|
|
409
|
+
const secondCreating = getOrCreateConversation("herd-conversation");
|
|
410
|
+
|
|
411
|
+
// The delete the observing acquire will refuse on when it wakes.
|
|
412
|
+
conversationRowPresent = false;
|
|
413
|
+
releaseSetup();
|
|
414
|
+
|
|
415
|
+
expect(await observing).toBeNull();
|
|
416
|
+
const first = await firstCreating;
|
|
417
|
+
const second = await secondCreating;
|
|
418
|
+
|
|
419
|
+
// One acquire built; the other joined the flight it published.
|
|
420
|
+
expect(first).toBe(second);
|
|
421
|
+
expect(findConversation("herd-conversation")).toBe(first);
|
|
422
|
+
expect(conversationCount()).toBe(1);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("a joiner holding a later incarnation does not inherit the decline", async () => {
|
|
426
|
+
// The flight it joined was asked about the row that has since been
|
|
427
|
+
// deleted, and reports that. This caller was accepted for the row written
|
|
428
|
+
// after it, which is still there, so taking the null would drop work the
|
|
429
|
+
// conversation on screen can still accept.
|
|
430
|
+
resetAcquireState();
|
|
431
|
+
conversationRowPresent = true;
|
|
432
|
+
let releaseSetup = () => {};
|
|
433
|
+
holdSetup = new Promise<void>((resolve) => {
|
|
434
|
+
releaseSetup = resolve;
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Passes its up-front read, publishes the flight, then waits inside setup.
|
|
438
|
+
const doomed = getConversationIfExists("rejoin-conversation");
|
|
439
|
+
|
|
440
|
+
// The delete the flight above will refuse on, and the recreate the caller
|
|
441
|
+
// below is accepted for.
|
|
442
|
+
conversationRowPresent = false;
|
|
443
|
+
writeConversationRow();
|
|
444
|
+
const acceptedIncarnation = conversationCreatedAt;
|
|
445
|
+
|
|
446
|
+
// Reads the recreated row, finds no instance, and joins the flight above.
|
|
447
|
+
const joining = getConversationIfExists("rejoin-conversation");
|
|
448
|
+
releaseSetup();
|
|
449
|
+
|
|
450
|
+
expect(await doomed).toBeNull();
|
|
451
|
+
|
|
452
|
+
const conversation = await joining;
|
|
453
|
+
expect(conversation).not.toBeNull();
|
|
454
|
+
expect(findConversation("rejoin-conversation")).toBe(conversation!);
|
|
455
|
+
// The row it was accepted for, untouched: a non-creating acquire that
|
|
456
|
+
// finds its own row present writes nothing back.
|
|
457
|
+
expect(conversationCreatedAt).toBe(acceptedIncarnation);
|
|
458
|
+
expect(mockEnsureConversationExists).not.toHaveBeenCalled();
|
|
459
|
+
expect(mockCreateConversation).not.toHaveBeenCalled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("a joiner whose own row is gone still answers null", async () => {
|
|
463
|
+
// The control on the retry above. Reconsulting is for a caller whose row
|
|
464
|
+
// outlived the flight's, and a caller with no row at all has to keep
|
|
465
|
+
// getting the answer its callers reclaim uploads on.
|
|
466
|
+
resetAcquireState();
|
|
467
|
+
conversationRowPresent = true;
|
|
468
|
+
let releaseSetup = () => {};
|
|
469
|
+
holdSetup = new Promise<void>((resolve) => {
|
|
470
|
+
releaseSetup = resolve;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const doomed = getConversationIfExists("rejoin-absent-conversation");
|
|
474
|
+
// Joins while the row is still the one both were asked about.
|
|
475
|
+
const joining = getConversationIfExists("rejoin-absent-conversation");
|
|
476
|
+
|
|
477
|
+
// Deleted for good, with nothing written back.
|
|
478
|
+
conversationRowPresent = false;
|
|
479
|
+
releaseSetup();
|
|
480
|
+
|
|
481
|
+
expect(await doomed).toBeNull();
|
|
482
|
+
expect(await joining).toBeNull();
|
|
483
|
+
expect(conversationCount()).toBe(0);
|
|
484
|
+
expect(mockEnsureConversationExists).not.toHaveBeenCalled();
|
|
485
|
+
expect(mockCreateConversation).not.toHaveBeenCalled();
|
|
486
|
+
});
|
|
487
|
+
});
|