@vellumai/assistant 0.11.1 → 0.11.2-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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Platform push adapter
|
|
3
|
-
* the platform's APNs dispatch endpoint.
|
|
2
|
+
* Platform push adapter for native mobile notification delivery.
|
|
4
3
|
*
|
|
5
4
|
* POSTs a `notification_intent` payload to
|
|
6
5
|
* `/v1/assistants/{id}/push/dispatch/`. The platform endpoint fans the
|
|
7
|
-
* notification out to
|
|
8
|
-
* gates
|
|
9
|
-
* with `{ skipped: "flag_off" }` when the flag is OFF — no action needed
|
|
10
|
-
* from the daemon).
|
|
6
|
+
* notification out to registered device tokens for the bound user. Provider
|
|
7
|
+
* feature gates return 202 with `{ skipped: "flag_off" }` when no provider runs.
|
|
11
8
|
*
|
|
12
9
|
* Guardian-sensitive notifications (approval requests, access requests)
|
|
13
10
|
* are annotated with `targetGuardianPrincipalId` so the platform can
|
|
14
|
-
* scope
|
|
11
|
+
* scope native fan-out to guardian-bound devices, mirroring the macOS adapter.
|
|
15
12
|
*/
|
|
16
13
|
|
|
17
14
|
import { VellumPlatformClient } from "../../platform/client.js";
|
|
@@ -23,6 +20,7 @@ import {
|
|
|
23
20
|
} from "../../util/retry.js";
|
|
24
21
|
import type {
|
|
25
22
|
ChannelAdapter,
|
|
23
|
+
ChannelDeliveryObserver,
|
|
26
24
|
ChannelDeliveryPayload,
|
|
27
25
|
ChannelDestination,
|
|
28
26
|
DeliveryResult,
|
|
@@ -35,6 +33,17 @@ const log = getLogger("notif-adapter-platform");
|
|
|
35
33
|
// Exponential backoff delays for 5xx/timeout retries: 250ms → 1s → 4s
|
|
36
34
|
const RETRY_DELAYS_MS = [250, 1_000, 4_000] as const;
|
|
37
35
|
|
|
36
|
+
// Per-attempt abort timeout. The underlying platform fetch has no timeout of
|
|
37
|
+
// its own, and the broadcaster defers the urgent local banner on this
|
|
38
|
+
// dispatch's outcome -- a hung platform must fail the attempt, not stall it.
|
|
39
|
+
const ATTEMPT_TIMEOUT_MS = 5_000;
|
|
40
|
+
|
|
41
|
+
/** Whether a fetch error is the per-attempt abort timeout firing. */
|
|
42
|
+
function isAttemptTimeout(err: unknown): boolean {
|
|
43
|
+
const name = (err as { name?: unknown } | null)?.name;
|
|
44
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
interface DispatchBody {
|
|
39
48
|
delivery_id?: string;
|
|
40
49
|
source_event_name: string;
|
|
@@ -45,12 +54,29 @@ interface DispatchBody {
|
|
|
45
54
|
target_guardian_principal_id?: string;
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
type RemotePushPlatform = "ios" | "android";
|
|
58
|
+
|
|
59
|
+
function acceptedPlatforms(body: unknown): RemotePushPlatform[] | undefined {
|
|
60
|
+
if (typeof body !== "object" || body === null) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const value = (body as { accepted_platforms?: unknown }).accepted_platforms;
|
|
64
|
+
if (!Array.isArray(value)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return value.filter(
|
|
68
|
+
(platform): platform is RemotePushPlatform =>
|
|
69
|
+
platform === "ios" || platform === "android",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
export class PlatformPushAdapter implements ChannelAdapter {
|
|
49
74
|
readonly channel: NotificationChannel = "platform";
|
|
50
75
|
|
|
51
76
|
async send(
|
|
52
77
|
payload: ChannelDeliveryPayload,
|
|
53
78
|
destination: ChannelDestination,
|
|
79
|
+
observer?: ChannelDeliveryObserver,
|
|
54
80
|
): Promise<DeliveryResult> {
|
|
55
81
|
const client = await VellumPlatformClient.create();
|
|
56
82
|
if (!client) {
|
|
@@ -80,7 +106,7 @@ export class PlatformPushAdapter implements ChannelAdapter {
|
|
|
80
106
|
: undefined;
|
|
81
107
|
|
|
82
108
|
const body: DispatchBody = {
|
|
83
|
-
delivery_id: payload.deliveryId,
|
|
109
|
+
delivery_id: payload.correlationId ?? payload.deliveryId,
|
|
84
110
|
source_event_name: payload.sourceEventName,
|
|
85
111
|
title: payload.copy.title,
|
|
86
112
|
body: payload.copy.body,
|
|
@@ -90,6 +116,10 @@ export class PlatformPushAdapter implements ChannelAdapter {
|
|
|
90
116
|
};
|
|
91
117
|
|
|
92
118
|
const path = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/push/dispatch/`;
|
|
119
|
+
const accumulatedPlatforms = new Set<RemotePushPlatform>();
|
|
120
|
+
let platformsReported = false;
|
|
121
|
+
const remotePushPlatforms = () =>
|
|
122
|
+
platformsReported ? [...accumulatedPlatforms] : undefined;
|
|
93
123
|
|
|
94
124
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
|
|
95
125
|
let response: Response;
|
|
@@ -98,9 +128,13 @@ export class PlatformPushAdapter implements ChannelAdapter {
|
|
|
98
128
|
method: "POST",
|
|
99
129
|
headers: { "Content-Type": "application/json" },
|
|
100
130
|
body: JSON.stringify(body),
|
|
131
|
+
signal: AbortSignal.timeout(ATTEMPT_TIMEOUT_MS),
|
|
101
132
|
});
|
|
102
133
|
} catch (err) {
|
|
103
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
attempt < RETRY_DELAYS_MS.length &&
|
|
136
|
+
(isAttemptTimeout(err) || isRetryableNetworkError(err))
|
|
137
|
+
) {
|
|
104
138
|
log.warn(
|
|
105
139
|
{
|
|
106
140
|
attempt,
|
|
@@ -117,20 +151,62 @@ export class PlatformPushAdapter implements ChannelAdapter {
|
|
|
117
151
|
{ attempt, sourceEventName: payload.sourceEventName, err },
|
|
118
152
|
"Failed to dispatch platform push notification",
|
|
119
153
|
);
|
|
120
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: message,
|
|
157
|
+
remotePushPlatforms: remotePushPlatforms(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const responseText = await response.text().catch(() => "");
|
|
162
|
+
let responseBody: unknown = null;
|
|
163
|
+
try {
|
|
164
|
+
responseBody = JSON.parse(responseText) as unknown;
|
|
165
|
+
} catch {
|
|
166
|
+
responseBody = null;
|
|
167
|
+
}
|
|
168
|
+
const responsePlatforms = acceptedPlatforms(responseBody);
|
|
169
|
+
if (responsePlatforms) {
|
|
170
|
+
platformsReported = true;
|
|
171
|
+
for (const platform of responsePlatforms) {
|
|
172
|
+
accumulatedPlatforms.add(platform);
|
|
173
|
+
}
|
|
174
|
+
observer?.onRemotePushPlatforms(remotePushPlatforms()!);
|
|
121
175
|
}
|
|
122
176
|
|
|
123
177
|
if (response.ok) {
|
|
178
|
+
// A 2xx does not mean a push went out: the server returns
|
|
179
|
+
// 202 {"skipped": ...} when the feature flag is off or no tokens
|
|
180
|
+
// are registered, and 200 with tokens_sent: 0 on idempotent
|
|
181
|
+
// replays. Only report acceptance when the body confirms at least
|
|
182
|
+
// one device push was dispatched; an unparseable or ambiguous body
|
|
183
|
+
// counts as not accepted (a duplicate client banner beats a lost
|
|
184
|
+
// notification).
|
|
185
|
+
const parsedBody = responseBody as {
|
|
186
|
+
accepted_platforms?: unknown;
|
|
187
|
+
skipped?: unknown;
|
|
188
|
+
tokens_sent?: unknown;
|
|
189
|
+
} | null;
|
|
190
|
+
const remotePushAccepted =
|
|
191
|
+
parsedBody != null &&
|
|
192
|
+
!parsedBody.skipped &&
|
|
193
|
+
typeof parsedBody.tokens_sent === "number" &&
|
|
194
|
+
parsedBody.tokens_sent > 0;
|
|
124
195
|
log.info(
|
|
125
196
|
{
|
|
126
197
|
sourceEventName: payload.sourceEventName,
|
|
127
198
|
title: payload.copy.title,
|
|
128
199
|
guardianScoped: targetGuardianPrincipalId != null,
|
|
129
200
|
status: response.status,
|
|
201
|
+
remotePushAccepted,
|
|
130
202
|
},
|
|
131
203
|
"Platform push dispatched",
|
|
132
204
|
);
|
|
133
|
-
return {
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
remotePushAccepted,
|
|
208
|
+
remotePushPlatforms: remotePushPlatforms(),
|
|
209
|
+
};
|
|
134
210
|
}
|
|
135
211
|
|
|
136
212
|
if (
|
|
@@ -149,18 +225,18 @@ export class PlatformPushAdapter implements ChannelAdapter {
|
|
|
149
225
|
continue;
|
|
150
226
|
}
|
|
151
227
|
|
|
152
|
-
const errorText = await response.text().catch(() => "");
|
|
153
228
|
log.error(
|
|
154
229
|
{
|
|
155
230
|
status: response.status,
|
|
156
231
|
sourceEventName: payload.sourceEventName,
|
|
157
|
-
body:
|
|
232
|
+
body: responseText.slice(0, 256),
|
|
158
233
|
},
|
|
159
234
|
"Non-retryable error from push dispatch endpoint",
|
|
160
235
|
);
|
|
161
236
|
return {
|
|
162
237
|
success: false,
|
|
163
|
-
error: `HTTP ${response.status}: ${
|
|
238
|
+
error: `HTTP ${response.status}: ${responseText.slice(0, 128)}`,
|
|
239
|
+
remotePushPlatforms: remotePushPlatforms(),
|
|
164
240
|
};
|
|
165
241
|
}
|
|
166
242
|
|
|
@@ -49,14 +49,16 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
49
49
|
// Attempt rich delivery with inline keyboard buttons.
|
|
50
50
|
// On failure, fall back to plain text below.
|
|
51
51
|
try {
|
|
52
|
-
await sendTelegramReply(chatId, messageText, approval);
|
|
52
|
+
const sent = await sendTelegramReply(chatId, messageText, approval);
|
|
53
53
|
|
|
54
54
|
log.info(
|
|
55
55
|
{ sourceEventName: payload.sourceEventName, chatId },
|
|
56
56
|
"Telegram approval notification delivered with inline buttons",
|
|
57
57
|
);
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// The message id lets the delivery row address the card later
|
|
60
|
+
// (in-place withdrawal when the request resolves elsewhere).
|
|
61
|
+
return { success: true, messageId: sent.lastMessageId };
|
|
60
62
|
} catch (richErr) {
|
|
61
63
|
log.warn(
|
|
62
64
|
{ err: richErr, sourceEventName: payload.sourceEventName, chatId },
|
|
@@ -73,14 +75,14 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
73
75
|
? `${messageText}\n\n${approval.plainTextFallback}`
|
|
74
76
|
: messageText;
|
|
75
77
|
|
|
76
|
-
await sendTelegramReply(chatId, fallbackText);
|
|
78
|
+
const sent = await sendTelegramReply(chatId, fallbackText);
|
|
77
79
|
|
|
78
80
|
log.info(
|
|
79
81
|
{ sourceEventName: payload.sourceEventName, chatId },
|
|
80
82
|
"Telegram notification delivered",
|
|
81
83
|
);
|
|
82
84
|
|
|
83
|
-
return { success: true };
|
|
85
|
+
return { success: true, messageId: sent.lastMessageId };
|
|
84
86
|
} catch (err) {
|
|
85
87
|
const message = err instanceof Error ? err.message : String(err);
|
|
86
88
|
// A missing bot token means the operator simply hasn't configured
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chat.assistant_reply` producer: an APNs push for a finished reply the user
|
|
3
|
+
* is no longer looking at.
|
|
4
|
+
*
|
|
5
|
+
* Called at the end of a user-initiated turn (see
|
|
6
|
+
* `daemon/conversation-turn-finalize.ts`) and best-effort throughout: every
|
|
7
|
+
* failure is logged and swallowed, because a notification hiccup must never
|
|
8
|
+
* escalate a reply the client already received into a turn-level throw.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type pino from "pino";
|
|
12
|
+
|
|
13
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
14
|
+
import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
|
|
15
|
+
import {
|
|
16
|
+
getAttentionStateByConversationIds,
|
|
17
|
+
hasUnseenLatestAssistantMessage,
|
|
18
|
+
} from "../persistence/conversation-attention-store.js";
|
|
19
|
+
import {
|
|
20
|
+
type ConversationRow,
|
|
21
|
+
getConversation,
|
|
22
|
+
getMessageById,
|
|
23
|
+
parseMessageMetadata,
|
|
24
|
+
} from "../persistence/conversation-crud.js";
|
|
25
|
+
import {
|
|
26
|
+
isReplyPushIneligibleUserMessage,
|
|
27
|
+
resolveConversationKind,
|
|
28
|
+
} from "../persistence/conversation-types.js";
|
|
29
|
+
import { stringifyMessageContent } from "../persistence/message-content.js";
|
|
30
|
+
import { safeParseRecord } from "../util/json.js";
|
|
31
|
+
import { emitNotificationSignal } from "./emit-signal.js";
|
|
32
|
+
import {
|
|
33
|
+
sanitizeMessagePreview,
|
|
34
|
+
sanitizeNotificationTitle,
|
|
35
|
+
} from "./notification-utils.js";
|
|
36
|
+
|
|
37
|
+
/** Kill switch for this producer, on by default. */
|
|
38
|
+
const ASSISTANT_REPLY_PUSH_FLAG = "assistant-reply-push" as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Collapse whitespace runs ahead of the sanitizers' truncation: blank lines and
|
|
42
|
+
* list indentation would otherwise eat into the copy's length budget.
|
|
43
|
+
*/
|
|
44
|
+
function collapseWhitespace(value: string): string {
|
|
45
|
+
return value.replace(/\s+/g, " ").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Body for a reply whose only user-visible output is attachments. Generated
|
|
50
|
+
* files and images are linked to the assistant row by
|
|
51
|
+
* `resolveAssistantAttachments` before the turn's terminal SSE, and are
|
|
52
|
+
* exposed separately from the row's content blocks, so a file-generation reply
|
|
53
|
+
* reaches this producer with no text to preview. Reads attachment metadata
|
|
54
|
+
* only (no base64), and only once the text preview has already come up empty.
|
|
55
|
+
*
|
|
56
|
+
* Returns an empty string when the row has no attachments either, which is the
|
|
57
|
+
* genuinely-empty reply the caller suppresses.
|
|
58
|
+
*/
|
|
59
|
+
function describeAttachmentOnlyReply(assistantMessageId: string): string {
|
|
60
|
+
const attachments = getAttachmentMetadataForMessage(assistantMessageId);
|
|
61
|
+
if (attachments.length === 0) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
if (attachments.length > 1) {
|
|
65
|
+
return `Sent ${attachments.length} attachments`;
|
|
66
|
+
}
|
|
67
|
+
// Filenames are model- and tool-authored, so one is sanitized before it can
|
|
68
|
+
// reach the lock screen. A filename that sanitizes away leaves generic copy
|
|
69
|
+
// rather than a dangling "Sent ".
|
|
70
|
+
const filename = sanitizeMessagePreview(
|
|
71
|
+
collapseWhitespace(attachments[0].originalFilename),
|
|
72
|
+
);
|
|
73
|
+
return filename ? `Sent ${filename}` : "Sent an attachment";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read the markers the eligibility gate below consults off a persisted
|
|
78
|
+
* message's metadata column.
|
|
79
|
+
*
|
|
80
|
+
* `parseMessageMetadata` validates the whole column and yields nothing when
|
|
81
|
+
* any single field fails, so one unrecognized value would otherwise present
|
|
82
|
+
* here as "no metadata" and open every gate at once. The gate is a plain-record
|
|
83
|
+
* predicate, so a permissive read of the same JSON keeps it answering over
|
|
84
|
+
* whichever fields are intact.
|
|
85
|
+
*/
|
|
86
|
+
function readSuppressionMarkers(
|
|
87
|
+
metadataJson: string | null,
|
|
88
|
+
): Record<string, unknown> | undefined {
|
|
89
|
+
const validated = parseMessageMetadata(metadataJson);
|
|
90
|
+
if (validated) {
|
|
91
|
+
return validated;
|
|
92
|
+
}
|
|
93
|
+
return metadataJson ? safeParseRecord(metadataJson) : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function emitAssistantReplyNotification(params: {
|
|
97
|
+
conversationId: string;
|
|
98
|
+
assistantMessageId: string;
|
|
99
|
+
/**
|
|
100
|
+
* The row that opened the turn, threaded from the agent loop. Reading it by
|
|
101
|
+
* id rather than scanning back from the assistant row keeps a hidden or
|
|
102
|
+
* queued user message that landed mid-turn from being mistaken for the
|
|
103
|
+
* prompt this reply answers.
|
|
104
|
+
*/
|
|
105
|
+
userMessageId: string | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* True when this turn's reply streams to the app and nowhere else, so the
|
|
108
|
+
* initiating row's channel and voice markers no longer describe where the
|
|
109
|
+
* reply lands. See {@link isReplyPushIneligibleUserMessage}.
|
|
110
|
+
*/
|
|
111
|
+
replyDeliveredInAppOnly?: boolean;
|
|
112
|
+
rlog: pino.Logger;
|
|
113
|
+
/** Row the caller already holds; re-read when omitted. */
|
|
114
|
+
conversation?: ConversationRow | null;
|
|
115
|
+
}): Promise<void> {
|
|
116
|
+
const { conversationId, assistantMessageId, userMessageId, rlog } = params;
|
|
117
|
+
try {
|
|
118
|
+
if (!isAssistantFeatureFlagEnabled(ASSISTANT_REPLY_PUSH_FLAG)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!userMessageId) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const conversation = params.conversation ?? getConversation(conversationId);
|
|
125
|
+
if (!conversation) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// The other three kinds (memory consolidation, background, scheduled) each
|
|
129
|
+
// already have their own notification producer.
|
|
130
|
+
const kind = resolveConversationKind(
|
|
131
|
+
conversation.source,
|
|
132
|
+
conversation.conversationType,
|
|
133
|
+
);
|
|
134
|
+
if (kind !== "user") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const attention = getAttentionStateByConversationIds([conversationId]).get(
|
|
138
|
+
conversationId,
|
|
139
|
+
);
|
|
140
|
+
if (!hasUnseenLatestAssistantMessage(attention)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const assistantRow = getMessageById(assistantMessageId, conversationId);
|
|
145
|
+
if (!assistantRow) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const initiatingMessage = getMessageById(userMessageId, conversationId);
|
|
150
|
+
if (!initiatingMessage) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const initiatingMetadata = readSuppressionMarkers(
|
|
154
|
+
initiatingMessage.metadata,
|
|
155
|
+
);
|
|
156
|
+
if (
|
|
157
|
+
isReplyPushIneligibleUserMessage(initiatingMetadata, {
|
|
158
|
+
replyDeliveredInAppOnly: params.replyDeliveredInAppOnly,
|
|
159
|
+
})
|
|
160
|
+
) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A reply whose output is entirely attachments has no text to preview, so
|
|
165
|
+
// fall back to naming them rather than suppressing a real reply. A reply
|
|
166
|
+
// with neither text nor attachments stays silent.
|
|
167
|
+
const preview =
|
|
168
|
+
sanitizeMessagePreview(
|
|
169
|
+
collapseWhitespace(stringifyMessageContent(assistantRow.content)),
|
|
170
|
+
) ||
|
|
171
|
+
sanitizeMessagePreview(describeAttachmentOnlyReply(assistantMessageId));
|
|
172
|
+
if (!preview) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Conversation titles are user-controlled and unbounded (renames, imports),
|
|
177
|
+
// so the title gets the same treatment as the body before it reaches the
|
|
178
|
+
// lock screen. Absent `requestedTitle` lets the decision branch derive a
|
|
179
|
+
// title from the body, which reads better than an empty or placeholder
|
|
180
|
+
// conversation title.
|
|
181
|
+
const requestedTitle = sanitizeNotificationTitle(
|
|
182
|
+
collapseWhitespace(conversation.title ?? ""),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
await emitNotificationSignal({
|
|
186
|
+
sourceEventName: "chat.assistant_reply",
|
|
187
|
+
sourceChannel: "vellum",
|
|
188
|
+
// Deep-link target; the broadcaster validates it and merges it into
|
|
189
|
+
// `deepLinkTarget.conversationId`.
|
|
190
|
+
sourceContextId: conversationId,
|
|
191
|
+
attentionHints: {
|
|
192
|
+
requiresAction: false,
|
|
193
|
+
// Load-bearing for the iOS-only scope: `emit-signal.ts` force-adds the
|
|
194
|
+
// vellum channel for high/critical signals, which would widen this into
|
|
195
|
+
// an in-app banner on every client. Raising the urgency is the same as
|
|
196
|
+
// opting into v2.
|
|
197
|
+
urgency: "medium",
|
|
198
|
+
isAsyncBackground: false,
|
|
199
|
+
// The daemon cannot tell "still viewing" from "just left" at turn end.
|
|
200
|
+
// The viewing-on-iPhone case is covered by iOS suppressing remote
|
|
201
|
+
// pushes while the app is foregrounded.
|
|
202
|
+
visibleInSourceNow: false,
|
|
203
|
+
},
|
|
204
|
+
contextPayload: {
|
|
205
|
+
...(requestedTitle ? { requestedTitle } : {}),
|
|
206
|
+
requestedMessage: preview,
|
|
207
|
+
},
|
|
208
|
+
// The pipeline dedupe window is a flat hour keyed on this alone, so it
|
|
209
|
+
// has to carry the message id or a second reply within the hour is
|
|
210
|
+
// silently dropped.
|
|
211
|
+
dedupeKey: `chat.assistant_reply:${conversationId}:${assistantMessageId}`,
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
rlog.warn(
|
|
215
|
+
{ err, conversationId, messageId: assistantMessageId },
|
|
216
|
+
"Failed to emit assistant reply notification (non-fatal)",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|