@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
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const INDEX = "idx_channel_inbound_events_message_id";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Index the inbound-event to message link.
|
|
8
|
+
*
|
|
9
|
+
* Three queries filter on `message_id`: the sibling lookups on the
|
|
10
|
+
* redelivery path (`getSiblingStreamedReplyTs`,
|
|
11
|
+
* `isDeduplicatedDeliveryOwnedBySibling`) and the NOT EXISTS prefilter in
|
|
12
|
+
* `findMessageByProviderMessageId` that skips rows the inbound-event
|
|
13
|
+
* index already resolves. Without this index each is a full table scan.
|
|
14
|
+
*
|
|
15
|
+
* Partial, on linked rows only: unlinked events (crash-window orphans,
|
|
16
|
+
* reactions before linking) contribute nothing to it.
|
|
17
|
+
*
|
|
18
|
+
* Idempotent via IF NOT EXISTS.
|
|
19
|
+
*/
|
|
20
|
+
export function migrateChannelInboundMessageIdIndex(database: DrizzleDb): void {
|
|
21
|
+
getSqliteFrom(database).exec(
|
|
22
|
+
`CREATE INDEX IF NOT EXISTS ${INDEX}
|
|
23
|
+
ON channel_inbound_events (message_id)
|
|
24
|
+
WHERE message_id IS NOT NULL`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
const TABLE = "channel_outbound_posts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create `channel_outbound_posts`: the provider-id index for messages the
|
|
8
|
+
* assistant itself posted, the outbound counterpart of
|
|
9
|
+
* `channel_inbound_events`' provider-id resolution.
|
|
10
|
+
*
|
|
11
|
+
* Its own table rather than more envelope fields on `messages`, because one
|
|
12
|
+
* stored reply fans out to many provider posts (a reply split at tool
|
|
13
|
+
* boundaries or length limits) and resolution by id must be exact: reactions
|
|
14
|
+
* and deletions name a provider post, and a JSON-array scan over recent rows
|
|
15
|
+
* is a recency heuristic, not a resolution contract. The row's
|
|
16
|
+
* `providerMeta` envelope stays the self-description every reader consumes;
|
|
17
|
+
* this table is the derived index over its id facts, written by the same
|
|
18
|
+
* post-send reconciliation that writes them.
|
|
19
|
+
*
|
|
20
|
+
* Composite primary key: a provider post id is unique within its channel's
|
|
21
|
+
* chat, and the triple is exactly how an inbound reaction or delete
|
|
22
|
+
* addresses it. Secondary indexes on the two FK columns because SQLite does
|
|
23
|
+
* not index FK columns automatically and both parents cascade deletes.
|
|
24
|
+
*
|
|
25
|
+
* No backfill: rows delivered before this table resolve through the
|
|
26
|
+
* transitional envelope-scan fallback in `findMessageByProviderMessageId`
|
|
27
|
+
* until they age out of relevance.
|
|
28
|
+
*
|
|
29
|
+
* Idempotent via IF NOT EXISTS.
|
|
30
|
+
*/
|
|
31
|
+
export function migrateCreateChannelOutboundPosts(database: DrizzleDb): void {
|
|
32
|
+
const sqlite = getSqliteFrom(database);
|
|
33
|
+
sqlite.exec(
|
|
34
|
+
`CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
35
|
+
source_channel TEXT NOT NULL,
|
|
36
|
+
external_chat_id TEXT NOT NULL,
|
|
37
|
+
provider_message_id TEXT NOT NULL,
|
|
38
|
+
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
39
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
40
|
+
created_at INTEGER NOT NULL,
|
|
41
|
+
PRIMARY KEY (source_channel, external_chat_id, provider_message_id)
|
|
42
|
+
)`,
|
|
43
|
+
);
|
|
44
|
+
sqlite.exec(
|
|
45
|
+
`CREATE INDEX IF NOT EXISTS idx_channel_outbound_posts_message_id
|
|
46
|
+
ON ${TABLE} (message_id)`,
|
|
47
|
+
);
|
|
48
|
+
sqlite.exec(
|
|
49
|
+
`CREATE INDEX IF NOT EXISTS idx_channel_outbound_posts_conversation_id
|
|
50
|
+
ON ${TABLE} (conversation_id)`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 375 creates `channel_outbound_posts`, the provider-id resolution
|
|
3
|
+
* index for messages the assistant itself posted: the outbound counterpart
|
|
4
|
+
* of `channel_inbound_events`. Keyed by the (channel, chat, provider id)
|
|
5
|
+
* triple, which is exactly how an inbound reaction or delete addresses a
|
|
6
|
+
* post.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Database } from "bun:sqlite";
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
13
|
+
|
|
14
|
+
import { getSqliteFrom } from "../../db-connection.js";
|
|
15
|
+
import * as schema from "../../schema.js";
|
|
16
|
+
import { migrateCreateChannelOutboundPosts } from "../375-create-channel-outbound-posts.js";
|
|
17
|
+
|
|
18
|
+
function createTestDb() {
|
|
19
|
+
const sqlite = new Database(":memory:");
|
|
20
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
21
|
+
return drizzle(sqlite, { schema });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tableNames(db: ReturnType<typeof createTestDb>): string[] {
|
|
25
|
+
const rows = getSqliteFrom(db)
|
|
26
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
27
|
+
.all() as { name: string }[];
|
|
28
|
+
return rows.map((row) => row.name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function indexNames(db: ReturnType<typeof createTestDb>): string[] {
|
|
32
|
+
const rows = getSqliteFrom(db)
|
|
33
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'index'")
|
|
34
|
+
.all() as { name: string }[];
|
|
35
|
+
return rows.map((row) => row.name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("migrateCreateChannelOutboundPosts", () => {
|
|
39
|
+
test("creates the table and its FK indexes", () => {
|
|
40
|
+
const db = createTestDb();
|
|
41
|
+
|
|
42
|
+
migrateCreateChannelOutboundPosts(db);
|
|
43
|
+
|
|
44
|
+
expect(tableNames(db)).toContain("channel_outbound_posts");
|
|
45
|
+
// SQLite does not index FK columns automatically, and both parents
|
|
46
|
+
// cascade deletes through them.
|
|
47
|
+
expect(indexNames(db)).toContain("idx_channel_outbound_posts_message_id");
|
|
48
|
+
expect(indexNames(db)).toContain(
|
|
49
|
+
"idx_channel_outbound_posts_conversation_id",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("re-running is a no-op", () => {
|
|
54
|
+
const db = createTestDb();
|
|
55
|
+
|
|
56
|
+
migrateCreateChannelOutboundPosts(db);
|
|
57
|
+
migrateCreateChannelOutboundPosts(db);
|
|
58
|
+
|
|
59
|
+
expect(tableNames(db)).toContain("channel_outbound_posts");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("the triple is the primary key: one row per provider post", () => {
|
|
63
|
+
const db = createTestDb();
|
|
64
|
+
migrateCreateChannelOutboundPosts(db);
|
|
65
|
+
const sqlite = getSqliteFrom(db);
|
|
66
|
+
const insert = () =>
|
|
67
|
+
sqlite
|
|
68
|
+
.prepare(
|
|
69
|
+
`INSERT OR IGNORE INTO channel_outbound_posts
|
|
70
|
+
(source_channel, external_chat_id, provider_message_id,
|
|
71
|
+
message_id, conversation_id, created_at)
|
|
72
|
+
VALUES ('discord', 'chat-1', 'post-1', 'row-1', 'conv-1', 1)`,
|
|
73
|
+
)
|
|
74
|
+
.run();
|
|
75
|
+
|
|
76
|
+
insert();
|
|
77
|
+
insert();
|
|
78
|
+
|
|
79
|
+
const count = sqlite
|
|
80
|
+
.prepare("SELECT COUNT(*) AS n FROM channel_outbound_posts")
|
|
81
|
+
.get() as { n: number };
|
|
82
|
+
expect(count.n).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -2,6 +2,7 @@ import { sql } from "drizzle-orm";
|
|
|
2
2
|
import {
|
|
3
3
|
index,
|
|
4
4
|
integer,
|
|
5
|
+
primaryKey,
|
|
5
6
|
real,
|
|
6
7
|
sqliteTable,
|
|
7
8
|
text,
|
|
@@ -282,6 +283,30 @@ export const conversationCompactionEvents = sqliteTable(
|
|
|
282
283
|
],
|
|
283
284
|
);
|
|
284
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Lifecycle of a channel inbound event's reply delivery. `pending` is the only
|
|
288
|
+
* non-terminal value, so every path that finishes a turn has to move the row
|
|
289
|
+
* off it, including the paths that finish WITHOUT delivering: recovery reads a
|
|
290
|
+
* lingering `pending` as a reply still owed. See `runtime/AGENTS.md`.
|
|
291
|
+
*/
|
|
292
|
+
export const CHANNEL_DELIVERY_STATUSES = [
|
|
293
|
+
"pending",
|
|
294
|
+
"delivered",
|
|
295
|
+
"failed",
|
|
296
|
+
"dead_letter",
|
|
297
|
+
] as const;
|
|
298
|
+
export type ChannelDeliveryStatus = (typeof CHANNEL_DELIVERY_STATUSES)[number];
|
|
299
|
+
|
|
300
|
+
/** Lifecycle of a channel inbound event's agent-turn processing. */
|
|
301
|
+
export const CHANNEL_PROCESSING_STATUSES = [
|
|
302
|
+
"pending",
|
|
303
|
+
"processed",
|
|
304
|
+
"failed",
|
|
305
|
+
"dead_letter",
|
|
306
|
+
] as const;
|
|
307
|
+
export type ChannelProcessingStatus =
|
|
308
|
+
(typeof CHANNEL_PROCESSING_STATUSES)[number];
|
|
309
|
+
|
|
285
310
|
export const channelInboundEvents = sqliteTable("channel_inbound_events", {
|
|
286
311
|
id: text("id").primaryKey(),
|
|
287
312
|
sourceChannel: text("source_channel").notNull(),
|
|
@@ -294,8 +319,14 @@ export const channelInboundEvents = sqliteTable("channel_inbound_events", {
|
|
|
294
319
|
messageId: text("message_id").references(() => messages.id, {
|
|
295
320
|
onDelete: "cascade",
|
|
296
321
|
}),
|
|
297
|
-
deliveryStatus: text("delivery_status")
|
|
298
|
-
|
|
322
|
+
deliveryStatus: text("delivery_status", { enum: CHANNEL_DELIVERY_STATUSES })
|
|
323
|
+
.notNull()
|
|
324
|
+
.default("pending"),
|
|
325
|
+
processingStatus: text("processing_status", {
|
|
326
|
+
enum: CHANNEL_PROCESSING_STATUSES,
|
|
327
|
+
})
|
|
328
|
+
.notNull()
|
|
329
|
+
.default("pending"),
|
|
299
330
|
processingAttempts: integer("processing_attempts").notNull().default(0),
|
|
300
331
|
deliveryAttempts: integer("delivery_attempts").notNull().default(0),
|
|
301
332
|
lastProcessingError: text("last_processing_error"),
|
|
@@ -307,3 +338,41 @@ export const channelInboundEvents = sqliteTable("channel_inbound_events", {
|
|
|
307
338
|
createdAt: integer("created_at").notNull(),
|
|
308
339
|
updatedAt: integer("updated_at").notNull(),
|
|
309
340
|
});
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Provider-id index for messages the assistant itself posted: the outbound
|
|
344
|
+
* counterpart of `channelInboundEvents`' provider-id resolution. One stored
|
|
345
|
+
* reply fans out to many provider posts, and a reaction or deletion names a
|
|
346
|
+
* post, so resolution is keyed by the (channel, chat, provider id) triple.
|
|
347
|
+
* Written by the post-send reconciliation alongside the row's `providerMeta`
|
|
348
|
+
* envelope, which remains the row's self-description; this table is the
|
|
349
|
+
* derived index over its id facts.
|
|
350
|
+
*/
|
|
351
|
+
export const channelOutboundPosts = sqliteTable(
|
|
352
|
+
"channel_outbound_posts",
|
|
353
|
+
{
|
|
354
|
+
sourceChannel: text("source_channel").notNull(),
|
|
355
|
+
externalChatId: text("external_chat_id").notNull(),
|
|
356
|
+
providerMessageId: text("provider_message_id").notNull(),
|
|
357
|
+
messageId: text("message_id")
|
|
358
|
+
.notNull()
|
|
359
|
+
.references(() => messages.id, { onDelete: "cascade" }),
|
|
360
|
+
conversationId: text("conversation_id")
|
|
361
|
+
.notNull()
|
|
362
|
+
.references(() => conversations.id, { onDelete: "cascade" }),
|
|
363
|
+
createdAt: integer("created_at").notNull(),
|
|
364
|
+
},
|
|
365
|
+
(table) => [
|
|
366
|
+
primaryKey({
|
|
367
|
+
columns: [
|
|
368
|
+
table.sourceChannel,
|
|
369
|
+
table.externalChatId,
|
|
370
|
+
table.providerMessageId,
|
|
371
|
+
],
|
|
372
|
+
}),
|
|
373
|
+
index("idx_channel_outbound_posts_message_id").on(table.messageId),
|
|
374
|
+
index("idx_channel_outbound_posts_conversation_id").on(
|
|
375
|
+
table.conversationId,
|
|
376
|
+
),
|
|
377
|
+
],
|
|
378
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
diffDrizzleSchemaContract,
|
|
5
|
+
listDrizzleTables,
|
|
6
|
+
schemaContractIsClean,
|
|
7
|
+
} from "./schema-contract.js";
|
|
8
|
+
|
|
9
|
+
describe("listDrizzleTables", () => {
|
|
10
|
+
test("includes conversations.fork_strategy from the source catalog", () => {
|
|
11
|
+
const conversations = listDrizzleTables().find(
|
|
12
|
+
(table) => table.name === "conversations",
|
|
13
|
+
);
|
|
14
|
+
expect(conversations).toBeDefined();
|
|
15
|
+
expect(conversations!.columns).toContain("fork_strategy");
|
|
16
|
+
expect(conversations!.columns).toContain("id");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("de-duplicates aliased table exports", () => {
|
|
20
|
+
const names = listDrizzleTables().map((table) => table.name);
|
|
21
|
+
expect(names.filter((name) => name === "cron_jobs")).toEqual(["cron_jobs"]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("diffDrizzleSchemaContract", () => {
|
|
26
|
+
test("reports a missing column on a table that exists", () => {
|
|
27
|
+
const conversations = listDrizzleTables().find(
|
|
28
|
+
(table) => table.name === "conversations",
|
|
29
|
+
);
|
|
30
|
+
expect(conversations).toBeDefined();
|
|
31
|
+
const liveColumns = new Set(
|
|
32
|
+
conversations!.columns.filter((column) => column !== "fork_strategy"),
|
|
33
|
+
);
|
|
34
|
+
const live = new Map<string, Set<string>>(
|
|
35
|
+
listDrizzleTables().map((table) => [
|
|
36
|
+
table.name,
|
|
37
|
+
table.name === "conversations" ? liveColumns : new Set(table.columns),
|
|
38
|
+
]),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const report = diffDrizzleSchemaContract(live);
|
|
42
|
+
expect(schemaContractIsClean(report)).toBe(false);
|
|
43
|
+
expect(report.missingTables).toEqual([]);
|
|
44
|
+
expect(report.missingColumns).toEqual([
|
|
45
|
+
{ table: "conversations", column: "fork_strategy" },
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("ignores extra SQLite columns that Drizzle does not own", () => {
|
|
50
|
+
const live = new Map<string, Set<string>>(
|
|
51
|
+
listDrizzleTables().map((table) => {
|
|
52
|
+
const columns = new Set(table.columns);
|
|
53
|
+
if (table.name === "conversations") {
|
|
54
|
+
columns.add("group_id");
|
|
55
|
+
}
|
|
56
|
+
return [table.name, columns];
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const report = diffDrizzleSchemaContract(live);
|
|
61
|
+
expect(schemaContractIsClean(report)).toBe(true);
|
|
62
|
+
expect(report.missingColumns).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("reports a drizzle table that is absent from every live file", () => {
|
|
66
|
+
const live = new Map<string, Set<string>>(
|
|
67
|
+
listDrizzleTables()
|
|
68
|
+
.filter((table) => table.name !== "conversations")
|
|
69
|
+
.map((table) => [table.name, new Set(table.columns)]),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const report = diffDrizzleSchemaContract(live);
|
|
73
|
+
expect(report.missingTables).toEqual(["conversations"]);
|
|
74
|
+
expect(
|
|
75
|
+
report.missingColumns.some((miss) => miss.table === "conversations"),
|
|
76
|
+
).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare the Drizzle table/column catalog in source to a live SQLite schema.
|
|
3
|
+
*
|
|
4
|
+
* Used by `assistant db status` to surface a binary that selects a
|
|
5
|
+
* column the live database never received. Extra SQLite tables or
|
|
6
|
+
* columns are ignored. Those are often raw-SQL fields (`group_id`) that
|
|
7
|
+
* Drizzle does not own.
|
|
8
|
+
*
|
|
9
|
+
* The catalog is the union of every `sqliteTable` exported from
|
|
10
|
+
* `schema/index.ts`. Tables live across the main, logs, memory, and
|
|
11
|
+
* telemetry files; callers merge those files before calling
|
|
12
|
+
* {@link diffDrizzleSchemaContract}.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getTableColumns, getTableName, is, Table } from "drizzle-orm";
|
|
16
|
+
|
|
17
|
+
import * as schema from "./schema/index.js";
|
|
18
|
+
|
|
19
|
+
export interface SchemaColumnMiss {
|
|
20
|
+
table: string;
|
|
21
|
+
column: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SchemaContractReport {
|
|
25
|
+
/** Drizzle tables with no matching SQLite table in the live files. */
|
|
26
|
+
missingTables: string[];
|
|
27
|
+
/** Drizzle columns whose table exists in SQLite but the column does not. */
|
|
28
|
+
missingColumns: SchemaColumnMiss[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DrizzleTableColumns {
|
|
32
|
+
name: string;
|
|
33
|
+
columns: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Every Drizzle table name and its SQL column names, de-duplicated. */
|
|
37
|
+
export function listDrizzleTables(): DrizzleTableColumns[] {
|
|
38
|
+
const byName = new Map<string, string[]>();
|
|
39
|
+
for (const value of Object.values(schema)) {
|
|
40
|
+
if (!is(value, Table)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const name = getTableName(value);
|
|
44
|
+
if (!name) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const columns = Object.values(getTableColumns(value)).map(
|
|
48
|
+
(column) => column.name,
|
|
49
|
+
);
|
|
50
|
+
byName.set(name, columns);
|
|
51
|
+
}
|
|
52
|
+
return [...byName.entries()]
|
|
53
|
+
.map(([name, columns]) => ({ name, columns: [...columns].sort() }))
|
|
54
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Diff Drizzle's catalog against a merged live schema (table name → columns).
|
|
59
|
+
* Tables and columns present only in SQLite are not reported.
|
|
60
|
+
*/
|
|
61
|
+
export function diffDrizzleSchemaContract(
|
|
62
|
+
liveColumnsByTable: Map<string, ReadonlySet<string>>,
|
|
63
|
+
): SchemaContractReport {
|
|
64
|
+
const missingTables: string[] = [];
|
|
65
|
+
const missingColumns: SchemaColumnMiss[] = [];
|
|
66
|
+
|
|
67
|
+
for (const table of listDrizzleTables()) {
|
|
68
|
+
const live = liveColumnsByTable.get(table.name);
|
|
69
|
+
if (!live) {
|
|
70
|
+
missingTables.push(table.name);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
for (const column of table.columns) {
|
|
74
|
+
if (!live.has(column)) {
|
|
75
|
+
missingColumns.push({ table: table.name, column });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
missingTables.sort();
|
|
81
|
+
missingColumns.sort((a, b) => {
|
|
82
|
+
const tableCmp = a.table.localeCompare(b.table);
|
|
83
|
+
if (tableCmp !== 0) {
|
|
84
|
+
return tableCmp;
|
|
85
|
+
}
|
|
86
|
+
return a.column.localeCompare(b.column);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return { missingTables, missingColumns };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function schemaContractIsClean(report: SchemaContractReport): boolean {
|
|
93
|
+
return (
|
|
94
|
+
report.missingTables.length === 0 && report.missingColumns.length === 0
|
|
95
|
+
);
|
|
96
|
+
}
|
package/src/persistence/steps.ts
CHANGED
|
@@ -481,6 +481,8 @@ import { migrateAcpSessionHistoryAuthErrorCode } from "./migrations/370-acp-sess
|
|
|
481
481
|
import { migrateAcpSessionHistoryAuthErrorCredential } from "./migrations/371-acp-session-history-auth-error-credential.js";
|
|
482
482
|
import { migrateCreateAcpRefusedCredentials } from "./migrations/372-create-acp-refused-credentials.js";
|
|
483
483
|
import { migrateAcpAuthMarkerIndex } from "./migrations/373-acp-auth-marker-index.js";
|
|
484
|
+
import { migrateChannelInboundMessageIdIndex } from "./migrations/374-channel-inbound-message-id-index.js";
|
|
485
|
+
import { migrateCreateChannelOutboundPosts } from "./migrations/375-create-channel-outbound-posts.js";
|
|
484
486
|
import type { MigrationStep } from "./migrations/run-migrations.js";
|
|
485
487
|
|
|
486
488
|
export const migrationSteps: MigrationStep[] = [
|
|
@@ -1596,4 +1598,6 @@ export const migrationSteps: MigrationStep[] = [
|
|
|
1596
1598
|
migrateAcpSessionHistoryAuthErrorCredential,
|
|
1597
1599
|
migrateCreateAcpRefusedCredentials,
|
|
1598
1600
|
migrateAcpAuthMarkerIndex,
|
|
1601
|
+
migrateChannelInboundMessageIdIndex,
|
|
1602
|
+
migrateCreateChannelOutboundPosts,
|
|
1599
1603
|
];
|
package/src/platform/client.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* authenticated fetch for all platform API calls.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { PlatformCredentialVerificationStatus } from "@vellumai/service-contracts/platform-credential";
|
|
9
|
+
|
|
8
10
|
import { getPlatformAssistantId } from "../config/env.js";
|
|
9
11
|
import { resolveManagedProxyContext } from "../providers/platform-proxy/context.js";
|
|
10
12
|
import { credentialKey } from "../security/credential-key.js";
|
|
@@ -106,6 +108,9 @@ async function resolvePlatformClientConfig(): Promise<PlatformClientConfig | nul
|
|
|
106
108
|
// credential backend must answer from cache instead of stalling the banner.
|
|
107
109
|
const CONFIGURED_PROBE_DEADLINE_MS = 500;
|
|
108
110
|
|
|
111
|
+
/** Bound for the credential check; the heartbeat waits on it. */
|
|
112
|
+
const CREDENTIAL_VERIFY_TIMEOUT_MS = 5_000;
|
|
113
|
+
|
|
109
114
|
let lastKnownConfigured: boolean | null = null;
|
|
110
115
|
// Single-flight slot with rotation: concurrent probes share one resolution,
|
|
111
116
|
// while a flight some caller already gave up on is replaced so a hung
|
|
@@ -308,6 +313,56 @@ export class VellumPlatformClient {
|
|
|
308
313
|
}
|
|
309
314
|
}
|
|
310
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Ask the platform whether this client's assistant API key still
|
|
318
|
+
* authenticates.
|
|
319
|
+
*
|
|
320
|
+
* `"rejected"` is a settled negative: the platform answered, and the answer
|
|
321
|
+
* was that the credential is not accepted (401/403, which is how
|
|
322
|
+
* `AssistantAPIKeyAuthentication` reports a revoked, expired, or unknown
|
|
323
|
+
* key). Only that verdict may drive a rotation, because rotating on a
|
|
324
|
+
* network blip would replace a working credential for no reason.
|
|
325
|
+
* `"unknown"` covers every unsettled case (no assistant id, transport
|
|
326
|
+
* failure, server error) and means "ask again later", never "broken".
|
|
327
|
+
*
|
|
328
|
+
* The carrier is the owner-consent endpoint. Nothing here reads consent:
|
|
329
|
+
* the request is a side-effect-free authenticated GET this client already
|
|
330
|
+
* makes, and the only thing inspected is whether authentication succeeded.
|
|
331
|
+
* The platform exposes no dedicated credential-verification endpoint, so
|
|
332
|
+
* the carrier is named here rather than left implicit at call sites.
|
|
333
|
+
*/
|
|
334
|
+
async verifyCredential(): Promise<PlatformCredentialVerificationStatus> {
|
|
335
|
+
if (!this.assistantId) {
|
|
336
|
+
return "unknown";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const res = await this.fetch(
|
|
341
|
+
`/v1/assistants/${this.assistantId}/owner-consent/`,
|
|
342
|
+
// The heartbeat awaits this check, so an unanswered request must
|
|
343
|
+
// settle as unknown rather than hold the credential pass open.
|
|
344
|
+
{ signal: AbortSignal.timeout(CREDENTIAL_VERIFY_TIMEOUT_MS) },
|
|
345
|
+
);
|
|
346
|
+
if (res.ok) {
|
|
347
|
+
return "valid";
|
|
348
|
+
}
|
|
349
|
+
if (res.status === 401 || res.status === 403) {
|
|
350
|
+
return "rejected";
|
|
351
|
+
}
|
|
352
|
+
log.debug(
|
|
353
|
+
{ status: res.status },
|
|
354
|
+
"credential verification returned an unsettled status",
|
|
355
|
+
);
|
|
356
|
+
return "unknown";
|
|
357
|
+
} catch (err) {
|
|
358
|
+
log.debug(
|
|
359
|
+
{ err },
|
|
360
|
+
"credential verification could not reach the platform",
|
|
361
|
+
);
|
|
362
|
+
return "unknown";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
311
366
|
get baseUrl(): string {
|
|
312
367
|
return this.platformBaseUrl;
|
|
313
368
|
}
|
|
@@ -137,6 +137,52 @@ describe("readPluginMcpServers", () => {
|
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
test("defaults an omitted type to streamable-http", () => {
|
|
141
|
+
const dir = makePluginsDir({
|
|
142
|
+
openseo: {
|
|
143
|
+
mcpJson: JSON.stringify({
|
|
144
|
+
mcpServers: {
|
|
145
|
+
openseo: { url: "https://app.openseo.so/mcp" },
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const { servers, issues } = readPluginMcpServers({
|
|
151
|
+
workspacePluginsDir: dir,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(issues).toEqual([]);
|
|
155
|
+
expect(servers).toHaveLength(1);
|
|
156
|
+
expect(servers[0].config.transport).toEqual({
|
|
157
|
+
type: "streamable-http",
|
|
158
|
+
url: "https://app.openseo.so/mcp",
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("treats the Claude-style http alias as streamable-http", () => {
|
|
163
|
+
const dir = makePluginsDir({
|
|
164
|
+
openseo: {
|
|
165
|
+
mcpJson: JSON.stringify({
|
|
166
|
+
mcpServers: {
|
|
167
|
+
openseo: {
|
|
168
|
+
type: "http",
|
|
169
|
+
url: "https://app.openseo.so/mcp",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const { servers, issues } = readPluginMcpServers({
|
|
176
|
+
workspacePluginsDir: dir,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(issues).toEqual([]);
|
|
180
|
+
expect(servers[0].config.transport).toEqual({
|
|
181
|
+
type: "streamable-http",
|
|
182
|
+
url: "https://app.openseo.so/mcp",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
140
186
|
test("defaults plugin servers to low risk", () => {
|
|
141
187
|
// mcp.json has no risk field, so a host default applies. It is `low`
|
|
142
188
|
// because the review happens at install time — curated marketplace,
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
*
|
|
35
35
|
* Gaps between slots leave room for future injectors to slot in at granular
|
|
36
36
|
* positions (e.g. `25` between unified-turn-context and pkb) without
|
|
37
|
-
* renumbering. The memory-v3 injectors sit far above this range (1000
|
|
37
|
+
* renumbering. The memory-v3 injectors sit far above this range (1000 cards
|
|
38
|
+
* / 1001 outbound spotlight).
|
|
38
39
|
*/
|
|
39
40
|
export const DEFAULT_INJECTOR_ORDER = {
|
|
40
41
|
diskPressureWarning: 5,
|
|
@@ -44,8 +44,8 @@ const SIGHT_FRAME_CONTENT = JSON.stringify([
|
|
|
44
44
|
]);
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* What `
|
|
48
|
-
* spoke it, so `scripted` rides along with the tag.
|
|
47
|
+
* What `persistAmbientSightFrame` writes on the voice surface: the gate
|
|
48
|
+
* sampled this, nobody spoke it, so `scripted` rides along with the tag.
|
|
49
49
|
*/
|
|
50
50
|
function sightFrameMetadata(attachmentId: string): string {
|
|
51
51
|
return JSON.stringify({
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
searchMessageIdsLexical,
|
|
5
5
|
} from "@vellumai/plugin-api";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { readSlackMetadataFromMessageMetadata } from "../../../../../messaging/providers/slack/message-metadata.js";
|
|
8
8
|
import { AUTO_ANALYSIS_SOURCE } from "../../../../../persistence/auto-analysis-constants.js";
|
|
9
9
|
import { isLexicalBackfillComplete } from "../../../../../persistence/checkpoints.js";
|
|
10
10
|
import { rawAll } from "../../../../../persistence/raw-query.js";
|
|
@@ -296,25 +296,23 @@ function parseSlackRecallMetadata(rawMetadata: string | null): {
|
|
|
296
296
|
return null;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
const slackMeta = readSlackMetadataFromMessageMetadata(rawMetadata);
|
|
300
|
+
if (!slackMeta) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
// The trust class sits beside the envelope, on the row's top-level
|
|
304
|
+
// metadata. Parsed here rather than through the host's JSON helper, which
|
|
305
|
+
// is outside the plugin's import boundary.
|
|
299
306
|
let parsed: unknown;
|
|
300
307
|
try {
|
|
301
308
|
parsed = JSON.parse(rawMetadata);
|
|
302
309
|
} catch {
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const metadata = parsed as Record<string, unknown>;
|
|
311
|
-
if (typeof metadata.slackMeta !== "string") {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
const slackMeta = readSlackMetadata(metadata.slackMeta);
|
|
315
|
-
if (!slackMeta) {
|
|
316
|
-
return null;
|
|
310
|
+
parsed = null;
|
|
317
311
|
}
|
|
312
|
+
const metadata =
|
|
313
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
314
|
+
? (parsed as Record<string, unknown>)
|
|
315
|
+
: {};
|
|
318
316
|
|
|
319
317
|
return {
|
|
320
318
|
...(slackMeta.displayName ? { displayName: slackMeta.displayName } : {}),
|