@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa
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/.env.example +2 -0
- package/ARCHITECTURE.md +26 -3
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -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/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -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/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +38 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +161 -17
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +54 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +9 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +14 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/process-message.ts +5 -0
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/tool-approval-handler.ts +9 -1
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -32,16 +32,12 @@
|
|
|
32
32
|
import type { ConversationStrategy } from "../channels/config.js";
|
|
33
33
|
import { getConversationStrategy } from "../channels/config.js";
|
|
34
34
|
import { type ChannelId, isChannelId } from "../channels/types.js";
|
|
35
|
-
import { isAssistantInitiatedThreadsEnabled } from "../config/assistant-initiated-threads-gate.js";
|
|
36
35
|
import {
|
|
37
36
|
addMessage,
|
|
38
37
|
createConversation,
|
|
39
38
|
getConversation,
|
|
40
39
|
} from "../persistence/conversation-crud.js";
|
|
41
|
-
import {
|
|
42
|
-
ASSISTANT_INITIATED_SOURCE,
|
|
43
|
-
type ConversationCreateType,
|
|
44
|
-
} from "../persistence/conversation-types.js";
|
|
40
|
+
import type { ConversationCreateType } from "../persistence/conversation-types.js";
|
|
45
41
|
import {
|
|
46
42
|
findInboundConversationId,
|
|
47
43
|
resolveInboundConversation,
|
|
@@ -56,6 +52,10 @@ import {
|
|
|
56
52
|
} from "../runtime/sync/resource-sync-events.js";
|
|
57
53
|
import { getLogger } from "../util/logger.js";
|
|
58
54
|
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
55
|
+
import {
|
|
56
|
+
type AssistantInitiatedThreadResolution,
|
|
57
|
+
resolveAssistantInitiatedThread,
|
|
58
|
+
} from "./assistant-initiated-thread.js";
|
|
59
59
|
import {
|
|
60
60
|
hasPersistedCompletionResult,
|
|
61
61
|
notificationConversationId,
|
|
@@ -103,6 +103,8 @@ export interface PairingOptions {
|
|
|
103
103
|
conversationAction?: ConversationAction;
|
|
104
104
|
/** Destination binding data for channel-scoped conversation continuation. */
|
|
105
105
|
bindingContext?: DestinationBindingContext;
|
|
106
|
+
/** Reuse the resolution made before delivery policy selects channels. */
|
|
107
|
+
assistantInitiatedThread?: AssistantInitiatedThreadResolution;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
/**
|
|
@@ -126,72 +128,9 @@ export interface PairingOptions {
|
|
|
126
128
|
* The returned `conversationId` is that producing conversation, with
|
|
127
129
|
* `createdNewConversation` false.
|
|
128
130
|
*
|
|
129
|
-
* Errors are caught and logged
|
|
131
|
+
* Errors are caught and logged; this function never throws so the
|
|
130
132
|
* notification pipeline is not disrupted by pairing failures.
|
|
131
133
|
*/
|
|
132
|
-
/**
|
|
133
|
-
* The event the assistant emits when it has a thought worth the user's time
|
|
134
|
-
* ("assistant.share" - the notifications skill's default), as opposed to a
|
|
135
|
-
* transactional request or a system alert.
|
|
136
|
-
*/
|
|
137
|
-
const ASSISTANT_SHARE_EVENT = "assistant.share";
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Promote a background share into an assistant-initiated thread, under the
|
|
141
|
-
* `assistant-initiated-threads` flag.
|
|
142
|
-
*
|
|
143
|
-
* The heartbeat's "have a thought, share it" path emits an assistant.share
|
|
144
|
-
* signal from its own background conversation, and the passive-vellum rule
|
|
145
|
-
* below would append the body there - a row the sidebar never shows. When the
|
|
146
|
-
* section exists, that share is exactly what it is for, so the signal is
|
|
147
|
-
* rewritten to materialize a fresh standard conversation stamped
|
|
148
|
-
* {@link ASSISTANT_INITIATED_SOURCE}, which is the section's membership mark.
|
|
149
|
-
*
|
|
150
|
-
* Deliberately narrow, in every direction it can be:
|
|
151
|
-
* - vellum channel only: other channels deliver the share as a native
|
|
152
|
-
* message and need no in-app thread;
|
|
153
|
-
* - assistant.share only: transactional events keep their own pairing
|
|
154
|
-
* rules, and their threads stay out of the section by source;
|
|
155
|
-
* - only when the producing conversation is a background/scheduled run (or
|
|
156
|
-
* nothing resolvable): a share emitted from inside a user-facing thread
|
|
157
|
-
* keeps the append, since the user is already looking at that thread;
|
|
158
|
-
* - never over an explicit `conversationMetadata.source` or an existing
|
|
159
|
-
* `requiresConversation`: a producer that declared its own filing wins.
|
|
160
|
-
*
|
|
161
|
-
* Flag off, the signal passes through untouched and shares keep the passive
|
|
162
|
-
* append: nothing changes for anyone outside the rollout.
|
|
163
|
-
*/
|
|
164
|
-
function withAssistantInitiatedThread(
|
|
165
|
-
signal: NotificationSignal,
|
|
166
|
-
channel: NotificationChannel,
|
|
167
|
-
): NotificationSignal {
|
|
168
|
-
if (
|
|
169
|
-
channel !== "vellum" ||
|
|
170
|
-
signal.sourceEventName !== ASSISTANT_SHARE_EVENT ||
|
|
171
|
-
signal.requiresConversation === true ||
|
|
172
|
-
signal.conversationMetadata?.source !== undefined ||
|
|
173
|
-
!isAssistantInitiatedThreadsEnabled()
|
|
174
|
-
) {
|
|
175
|
-
return signal;
|
|
176
|
-
}
|
|
177
|
-
const producing = getConversation(signal.sourceContextId);
|
|
178
|
-
if (
|
|
179
|
-
producing &&
|
|
180
|
-
producing.conversationType !== "background" &&
|
|
181
|
-
producing.conversationType !== "scheduled"
|
|
182
|
-
) {
|
|
183
|
-
return signal;
|
|
184
|
-
}
|
|
185
|
-
return {
|
|
186
|
-
...signal,
|
|
187
|
-
requiresConversation: true,
|
|
188
|
-
conversationMetadata: {
|
|
189
|
-
...signal.conversationMetadata,
|
|
190
|
-
source: ASSISTANT_INITIATED_SOURCE,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
134
|
export async function pairDeliveryWithConversation(
|
|
196
135
|
rawSignal: NotificationSignal,
|
|
197
136
|
channel: NotificationChannel,
|
|
@@ -199,7 +138,13 @@ export async function pairDeliveryWithConversation(
|
|
|
199
138
|
options?: PairingOptions,
|
|
200
139
|
): Promise<PairingResult> {
|
|
201
140
|
try {
|
|
202
|
-
const signal =
|
|
141
|
+
const signal =
|
|
142
|
+
channel === "vellum"
|
|
143
|
+
? (
|
|
144
|
+
options?.assistantInitiatedThread ??
|
|
145
|
+
resolveAssistantInitiatedThread(rawSignal)
|
|
146
|
+
).vellumSignal
|
|
147
|
+
: rawSignal;
|
|
203
148
|
const strategy = getConversationStrategy(channel as ChannelId);
|
|
204
149
|
|
|
205
150
|
if (strategy === "not_deliverable" || strategy === "push_only") {
|
|
@@ -27,6 +27,7 @@ import { VellumAdapter } from "./adapters/macos.js";
|
|
|
27
27
|
import { PlatformPushAdapter } from "./adapters/platform.js";
|
|
28
28
|
import { SlackAdapter } from "./adapters/slack.js";
|
|
29
29
|
import { TelegramAdapter } from "./adapters/telegram.js";
|
|
30
|
+
import { resolveAssistantInitiatedThread } from "./assistant-initiated-thread.js";
|
|
30
31
|
import {
|
|
31
32
|
type ConversationCreatedInfo,
|
|
32
33
|
NotificationBroadcaster,
|
|
@@ -428,6 +429,8 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
428
429
|
});
|
|
429
430
|
}
|
|
430
431
|
|
|
432
|
+
const assistantInitiatedThread = resolveAssistantInitiatedThread(signal);
|
|
433
|
+
|
|
431
434
|
// Step 2: Evaluate the signal through the decision engine
|
|
432
435
|
const connectedChannels = await getConnectedChannels();
|
|
433
436
|
|
|
@@ -444,6 +447,9 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
444
447
|
// steps (2.5a/2.5b/2.5c) so any of them replacing the decision triggers
|
|
445
448
|
// the re-persist and the stored row matches what is dispatched.
|
|
446
449
|
const prePolicyDecision = decision;
|
|
450
|
+
let platformConfigured: Promise<boolean> | undefined;
|
|
451
|
+
const platformIsConfigured = () =>
|
|
452
|
+
(platformConfigured ??= isPlatformClientConfigured());
|
|
447
453
|
|
|
448
454
|
// Step 2.5a: High/critical urgency signals always get both the in-app
|
|
449
455
|
// system notification (vellum) and the remote push (platform),
|
|
@@ -497,7 +503,7 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
497
503
|
}
|
|
498
504
|
if (
|
|
499
505
|
!selectedChannels.includes("platform") &&
|
|
500
|
-
(await
|
|
506
|
+
(await platformIsConfigured())
|
|
501
507
|
) {
|
|
502
508
|
selectedChannels.push("platform");
|
|
503
509
|
forcedChannels.push("platform");
|
|
@@ -559,9 +565,30 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
559
565
|
};
|
|
560
566
|
}
|
|
561
567
|
|
|
568
|
+
let assistantInitiatedPlatformAdded = false;
|
|
569
|
+
if (
|
|
570
|
+
assistantInitiatedThread.isCandidate &&
|
|
571
|
+
decision.shouldNotify &&
|
|
572
|
+
decision.selectedChannels.includes("vellum") &&
|
|
573
|
+
!decision.selectedChannels.includes("platform") &&
|
|
574
|
+
!(
|
|
575
|
+
"quiet" in signal.contextPayload && signal.contextPayload.quiet === true
|
|
576
|
+
) &&
|
|
577
|
+
signal.routingIntent !== "single_channel" &&
|
|
578
|
+
!exclusiveAllowlist &&
|
|
579
|
+
(await platformIsConfigured())
|
|
580
|
+
) {
|
|
581
|
+
assistantInitiatedPlatformAdded = true;
|
|
582
|
+
decision = {
|
|
583
|
+
...decision,
|
|
584
|
+
selectedChannels: [...decision.selectedChannels, "platform"],
|
|
585
|
+
reasoningSummary: `${decision.reasoningSummary} (platform added: assistant-initiated thread candidate)`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
562
589
|
// Re-persist the decision if any policy step changed it (urgency channel
|
|
563
|
-
// forcing, routing intent enforcement,
|
|
564
|
-
//
|
|
590
|
+
// forcing, routing intent enforcement, the access-request vellum floor,
|
|
591
|
+
// or the From me push default), so the audit matches the dispatch.
|
|
565
592
|
if (decision !== prePolicyDecision && decision.persistedDecisionId) {
|
|
566
593
|
try {
|
|
567
594
|
updateDecision(decision.persistedDecisionId, {
|
|
@@ -634,6 +661,10 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
634
661
|
{
|
|
635
662
|
onConversationCreated: params.onConversationCreated,
|
|
636
663
|
resultsSink: channelResults,
|
|
664
|
+
assistantInitiatedThread: {
|
|
665
|
+
resolution: assistantInitiatedThread,
|
|
666
|
+
platformAdded: assistantInitiatedPlatformAdded,
|
|
667
|
+
},
|
|
637
668
|
},
|
|
638
669
|
);
|
|
639
670
|
|
|
@@ -65,6 +65,8 @@ export type NotificationReceiptClass = z.infer<
|
|
|
65
65
|
|
|
66
66
|
export const DeliveryResultSchema = z.object({
|
|
67
67
|
success: z.boolean(),
|
|
68
|
+
/** Delivery was intentionally suppressed before the provider accepted it. */
|
|
69
|
+
skipped: z.boolean().optional(),
|
|
68
70
|
error: z.string().optional(),
|
|
69
71
|
messageId: z.string().optional(),
|
|
70
72
|
/** Set only by the platform push adapter: true when the platform accepted
|
|
@@ -147,7 +147,7 @@ export async function orchestrateOAuthConnect(
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// Deserialize JSON fields from the DB row
|
|
150
|
-
const
|
|
150
|
+
const authorizeParams = safeJsonParse<Record<string, string> | undefined>(
|
|
151
151
|
providerRow.authorizeParams,
|
|
152
152
|
undefined,
|
|
153
153
|
);
|
|
@@ -155,7 +155,6 @@ export async function orchestrateOAuthConnect(
|
|
|
155
155
|
// Resolve all protocol-level config from the DB
|
|
156
156
|
const authorizeUrl = providerRow.authorizeUrl;
|
|
157
157
|
const tokenExchangeUrl = providerRow.tokenExchangeUrl;
|
|
158
|
-
const authorizeParams = dbAuthorizeParams;
|
|
159
158
|
const userinfoUrl = providerRow.userinfoUrl ?? undefined;
|
|
160
159
|
const tokenEndpointAuthMethod = providerRow.tokenEndpointAuthMethod as
|
|
161
160
|
| TokenEndpointAuthMethod
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
mock,
|
|
7
|
+
setSystemTime,
|
|
8
|
+
test,
|
|
9
|
+
} from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { eq } from "drizzle-orm";
|
|
12
|
+
|
|
13
|
+
mock.module("../../config/sidebar-done-gate.js", () => ({
|
|
14
|
+
isSidebarDoneEnabled: () => true,
|
|
15
|
+
SIDEBAR_DONE_FLAG_KEY: "sidebar-done",
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
archiveInactiveConversation,
|
|
20
|
+
listAutoArchiveCandidates,
|
|
21
|
+
} from "../conversation-auto-archive.js";
|
|
22
|
+
import {
|
|
23
|
+
archiveConversation,
|
|
24
|
+
resurfaceArchivedConversation,
|
|
25
|
+
unarchiveConversation,
|
|
26
|
+
} from "../conversation-crud.js";
|
|
27
|
+
import { ensureGroupMigration } from "../conversation-group-migration.js";
|
|
28
|
+
import { getDb } from "../db-connection.js";
|
|
29
|
+
import { initializeDb } from "../db-init.js";
|
|
30
|
+
import { rawGet, rawRun } from "../raw-query.js";
|
|
31
|
+
import {
|
|
32
|
+
acpSessionHistory,
|
|
33
|
+
conversationAssistantAttentionState,
|
|
34
|
+
conversationModeSessions,
|
|
35
|
+
conversations,
|
|
36
|
+
externalConversationBindings,
|
|
37
|
+
workflowRuns,
|
|
38
|
+
} from "../schema/index.js";
|
|
39
|
+
import { upsertSubagentRecord } from "../subagent-store.js";
|
|
40
|
+
|
|
41
|
+
await initializeDb();
|
|
42
|
+
ensureGroupMigration();
|
|
43
|
+
const NOW = 1_800_000_000_000;
|
|
44
|
+
const DAY = 86_400_000;
|
|
45
|
+
const CUTOFF = NOW - 7 * DAY;
|
|
46
|
+
const ID = "conv-123";
|
|
47
|
+
|
|
48
|
+
function insert(
|
|
49
|
+
id = ID,
|
|
50
|
+
values: Partial<typeof conversations.$inferInsert> = {},
|
|
51
|
+
) {
|
|
52
|
+
getDb()
|
|
53
|
+
.insert(conversations)
|
|
54
|
+
.values({
|
|
55
|
+
id,
|
|
56
|
+
createdAt: CUTOFF - DAY,
|
|
57
|
+
updatedAt: NOW,
|
|
58
|
+
lastMessageAt: CUTOFF,
|
|
59
|
+
...values,
|
|
60
|
+
})
|
|
61
|
+
.run();
|
|
62
|
+
}
|
|
63
|
+
function change(values: Partial<typeof conversations.$inferInsert>) {
|
|
64
|
+
getDb()
|
|
65
|
+
.update(conversations)
|
|
66
|
+
.set(values)
|
|
67
|
+
.where(eq(conversations.id, ID))
|
|
68
|
+
.run();
|
|
69
|
+
}
|
|
70
|
+
function candidates(limit = 100) {
|
|
71
|
+
return listAutoArchiveCandidates({ cutoff: CUTOFF, limit });
|
|
72
|
+
}
|
|
73
|
+
function state() {
|
|
74
|
+
return getDb()
|
|
75
|
+
.select({
|
|
76
|
+
archivedAt: conversations.archivedAt,
|
|
77
|
+
lastReopenedAt: conversations.lastReopenedAt,
|
|
78
|
+
})
|
|
79
|
+
.from(conversations)
|
|
80
|
+
.where(eq(conversations.id, ID))
|
|
81
|
+
.get();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
setSystemTime(NOW);
|
|
86
|
+
const db = getDb();
|
|
87
|
+
db.delete(conversationAssistantAttentionState).run();
|
|
88
|
+
db.delete(conversationModeSessions).run();
|
|
89
|
+
db.delete(externalConversationBindings).run();
|
|
90
|
+
db.delete(acpSessionHistory).run();
|
|
91
|
+
db.delete(workflowRuns).run();
|
|
92
|
+
rawRun("test:clearSubagents", "DELETE FROM subagents");
|
|
93
|
+
db.delete(conversations).run();
|
|
94
|
+
insert();
|
|
95
|
+
});
|
|
96
|
+
afterAll(() => setSystemTime());
|
|
97
|
+
|
|
98
|
+
test("includes the exact cutoff, excludes newer activity, and ignores title/update timestamps", () => {
|
|
99
|
+
expect(candidates().map((row) => row.id)).toEqual([ID]);
|
|
100
|
+
change({ lastMessageAt: CUTOFF + 1 });
|
|
101
|
+
expect(candidates()).toEqual([]);
|
|
102
|
+
change({ lastMessageAt: null, createdAt: CUTOFF });
|
|
103
|
+
expect(candidates()).toHaveLength(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const exclusions: Array<[string, () => void]> = [
|
|
107
|
+
["already Done", () => change({ archivedAt: NOW })],
|
|
108
|
+
["processing", () => change({ processingStartedAt: NOW })],
|
|
109
|
+
["background", () => change({ conversationType: "background" })],
|
|
110
|
+
["scheduled", () => change({ conversationType: "scheduled" })],
|
|
111
|
+
["private", () => change({ conversationType: "private" })],
|
|
112
|
+
["system", () => change({ source: "system" })],
|
|
113
|
+
["subagent", () => change({ source: "subagent" })],
|
|
114
|
+
["spawned child", () => change({ parentConversationId: "conv-parent" })],
|
|
115
|
+
["memory", () => change({ source: "memory_v2_consolidation" })],
|
|
116
|
+
["schedule owner", () => change({ scheduleJobId: "schedule-123" })],
|
|
117
|
+
[
|
|
118
|
+
"pin",
|
|
119
|
+
() => {
|
|
120
|
+
rawRun(
|
|
121
|
+
"test:pin",
|
|
122
|
+
"UPDATE conversations SET group_id = 'system:pinned' WHERE id = ?",
|
|
123
|
+
ID,
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
"background group",
|
|
129
|
+
() => {
|
|
130
|
+
rawRun(
|
|
131
|
+
"test:background",
|
|
132
|
+
"UPDATE conversations SET group_id = 'system:background' WHERE id = ?",
|
|
133
|
+
ID,
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
["external origin", () => change({ originChannel: "slack" })],
|
|
138
|
+
[
|
|
139
|
+
"external binding",
|
|
140
|
+
() => {
|
|
141
|
+
getDb()
|
|
142
|
+
.insert(externalConversationBindings)
|
|
143
|
+
.values({
|
|
144
|
+
conversationId: ID,
|
|
145
|
+
sourceChannel: "telegram",
|
|
146
|
+
externalChatId: "chat-123",
|
|
147
|
+
createdAt: NOW,
|
|
148
|
+
updatedAt: NOW,
|
|
149
|
+
})
|
|
150
|
+
.run();
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
[
|
|
154
|
+
"unread",
|
|
155
|
+
() => {
|
|
156
|
+
getDb()
|
|
157
|
+
.insert(conversationAssistantAttentionState)
|
|
158
|
+
.values({
|
|
159
|
+
conversationId: ID,
|
|
160
|
+
latestAssistantMessageAt: CUTOFF,
|
|
161
|
+
lastSeenAssistantMessageAt: null,
|
|
162
|
+
createdAt: NOW,
|
|
163
|
+
updatedAt: NOW,
|
|
164
|
+
})
|
|
165
|
+
.run();
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"ACP run",
|
|
170
|
+
() => {
|
|
171
|
+
getDb()
|
|
172
|
+
.insert(acpSessionHistory)
|
|
173
|
+
.values({
|
|
174
|
+
id: "acp-123",
|
|
175
|
+
agentId: "agent-123",
|
|
176
|
+
acpSessionId: "session-123",
|
|
177
|
+
parentConversationId: ID,
|
|
178
|
+
status: "running",
|
|
179
|
+
startedAt: CUTOFF,
|
|
180
|
+
})
|
|
181
|
+
.run();
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
"workflow run",
|
|
186
|
+
() => {
|
|
187
|
+
getDb()
|
|
188
|
+
.insert(workflowRuns)
|
|
189
|
+
.values({
|
|
190
|
+
id: "run-123",
|
|
191
|
+
scriptSource: "",
|
|
192
|
+
scriptHash: "hash-123",
|
|
193
|
+
status: "running",
|
|
194
|
+
conversationId: ID,
|
|
195
|
+
})
|
|
196
|
+
.run();
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
[
|
|
200
|
+
"mode session",
|
|
201
|
+
() => {
|
|
202
|
+
getDb()
|
|
203
|
+
.insert(conversationModeSessions)
|
|
204
|
+
.values({
|
|
205
|
+
id: "mode-123",
|
|
206
|
+
conversationId: ID,
|
|
207
|
+
mode: "browser",
|
|
208
|
+
status: "active",
|
|
209
|
+
sourceStartedAt: CUTOFF,
|
|
210
|
+
lastActivityAt: CUTOFF,
|
|
211
|
+
})
|
|
212
|
+
.run();
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
[
|
|
216
|
+
"subagent run",
|
|
217
|
+
() => {
|
|
218
|
+
upsertSubagentRecord({
|
|
219
|
+
id: "task-123",
|
|
220
|
+
parentConversationId: ID,
|
|
221
|
+
conversationId: "conv-child",
|
|
222
|
+
label: "Example task",
|
|
223
|
+
objective: "Example objective",
|
|
224
|
+
role: "builder",
|
|
225
|
+
isFork: false,
|
|
226
|
+
sendResultToUser: true,
|
|
227
|
+
parentToolUseId: null,
|
|
228
|
+
status: "running",
|
|
229
|
+
error: null,
|
|
230
|
+
createdAt: CUTOFF,
|
|
231
|
+
startedAt: CUTOFF,
|
|
232
|
+
completedAt: null,
|
|
233
|
+
inputTokens: 0,
|
|
234
|
+
outputTokens: 0,
|
|
235
|
+
estimatedCost: 0,
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
describe.each(exclusions)("%s exclusion", (_name, exclude) => {
|
|
242
|
+
test("is filtered from candidates and rechecked after sampling", () => {
|
|
243
|
+
const sampled = candidates()[0]!;
|
|
244
|
+
exclude();
|
|
245
|
+
expect(candidates()).toEqual([]);
|
|
246
|
+
expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("native notification origins and read chats stay eligible", () => {
|
|
251
|
+
change({ originChannel: "notification:reminder" });
|
|
252
|
+
getDb()
|
|
253
|
+
.insert(conversationAssistantAttentionState)
|
|
254
|
+
.values({
|
|
255
|
+
conversationId: ID,
|
|
256
|
+
latestAssistantMessageAt: CUTOFF,
|
|
257
|
+
lastSeenAssistantMessageAt: CUTOFF,
|
|
258
|
+
createdAt: NOW,
|
|
259
|
+
updatedAt: NOW,
|
|
260
|
+
})
|
|
261
|
+
.run();
|
|
262
|
+
expect(candidates()).toHaveLength(1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a sampled old message change cannot be overwritten even if it remains below the cutoff", () => {
|
|
266
|
+
const sampled = candidates()[0]!;
|
|
267
|
+
change({ lastMessageAt: CUTOFF - 1 });
|
|
268
|
+
expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("manual reopen receives a full interval and repeated unarchive does not extend it", () => {
|
|
272
|
+
archiveConversation(ID);
|
|
273
|
+
unarchiveConversation(ID);
|
|
274
|
+
expect(state()).toEqual({ archivedAt: null, lastReopenedAt: NOW });
|
|
275
|
+
expect(candidates()).toEqual([]);
|
|
276
|
+
setSystemTime(NOW + DAY);
|
|
277
|
+
unarchiveConversation(ID);
|
|
278
|
+
expect(state()?.lastReopenedAt).toBe(NOW);
|
|
279
|
+
expect(listAutoArchiveCandidates({ cutoff: NOW - 1, limit: 100 })).toEqual(
|
|
280
|
+
[],
|
|
281
|
+
);
|
|
282
|
+
expect(listAutoArchiveCandidates({ cutoff: NOW, limit: 100 })).toHaveLength(
|
|
283
|
+
1,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("reopen after sampling prevents the conditional archive", () => {
|
|
288
|
+
const sampled = candidates()[0]!;
|
|
289
|
+
archiveConversation(ID);
|
|
290
|
+
unarchiveConversation(ID);
|
|
291
|
+
expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("archive is idempotent and new visible activity resurfaces it", () => {
|
|
295
|
+
const sampled = candidates()[0]!;
|
|
296
|
+
expect(archiveInactiveConversation(sampled, CUTOFF, NOW)).toBe(true);
|
|
297
|
+
expect(archiveInactiveConversation(sampled, CUTOFF, NOW + 1)).toBe(false);
|
|
298
|
+
expect(resurfaceArchivedConversation(ID, NOW + 1)).toBe(true);
|
|
299
|
+
expect(state()).toEqual({ archivedAt: null, lastReopenedAt: null });
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("keyset paging covers all rows while earlier pages are marked Done", () => {
|
|
303
|
+
for (let i = 0; i < 205; i++) {
|
|
304
|
+
insert(`conv-page-${String(i).padStart(3, "0")}`);
|
|
305
|
+
}
|
|
306
|
+
const visited: string[] = [];
|
|
307
|
+
let after: ReturnType<typeof candidates>[number] | undefined;
|
|
308
|
+
do {
|
|
309
|
+
const page = listAutoArchiveCandidates({
|
|
310
|
+
cutoff: CUTOFF,
|
|
311
|
+
limit: 100,
|
|
312
|
+
after,
|
|
313
|
+
});
|
|
314
|
+
if (page.length === 0) {
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
for (const candidate of page) {
|
|
318
|
+
expect(archiveInactiveConversation(candidate, CUTOFF, NOW)).toBe(true);
|
|
319
|
+
visited.push(candidate.id);
|
|
320
|
+
}
|
|
321
|
+
after = page[page.length - 1];
|
|
322
|
+
} while (true);
|
|
323
|
+
expect(new Set(visited).size).toBe(206);
|
|
324
|
+
expect(
|
|
325
|
+
rawGet<{ count: number }>(
|
|
326
|
+
"test:doneCount",
|
|
327
|
+
"SELECT count(*) AS count FROM conversations WHERE archived_at IS NOT NULL",
|
|
328
|
+
)?.count,
|
|
329
|
+
).toBe(206);
|
|
330
|
+
});
|