@vellumai/assistant 0.11.3 → 0.11.4-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -10,7 +10,10 @@ import type {
|
|
|
10
10
|
import { APP_VERSION } from "../version.js";
|
|
11
11
|
import { getDb } from "./db-connection.js";
|
|
12
12
|
import { rawAll } from "./raw-query.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
realUserTurnContentFilter,
|
|
15
|
+
realUserTurnContentFilterSql,
|
|
16
|
+
} from "./real-user-turn-filter.js";
|
|
14
17
|
import {
|
|
15
18
|
buildScheduleAttributionSubquery,
|
|
16
19
|
buildScheduleRunWindowExists,
|
|
@@ -300,6 +303,9 @@ export function queryUnreportedUsageEvents(
|
|
|
300
303
|
// branch from a turn far earlier than the fork's creation time, so
|
|
301
304
|
// they anchor on the fork boundary message instead; child creation is
|
|
302
305
|
// the fallback when the boundary message is absent.
|
|
306
|
+
// The turn-index subqueries below count user-role rows only. User rows are
|
|
307
|
+
// always written finalized (only assistant turns stream), so no completeness
|
|
308
|
+
// predicate is needed; do not copy this shape for assistant-row counts.
|
|
303
309
|
const parentTurnCutoffSql = sql<number>`CASE
|
|
304
310
|
WHEN ${conversations.parentConversationId} IS NOT NULL THEN ${conversations.createdAt}
|
|
305
311
|
ELSE COALESCE(
|
|
@@ -879,8 +885,7 @@ export function getUsageGroupBreakdown(
|
|
|
879
885
|
SELECT COUNT(*) FROM messages AS m2
|
|
880
886
|
WHERE m2.conversation_id = e.conversation_id
|
|
881
887
|
AND m2.role = 'user'
|
|
882
|
-
AND m2
|
|
883
|
-
AND m2.content NOT LIKE '%"type":"web\\_search\\_tool\\_result"%' ESCAPE '\\'
|
|
888
|
+
AND ${realUserTurnContentFilterSql("m2")}
|
|
884
889
|
)
|
|
885
890
|
END AS turn_count
|
|
886
891
|
FROM llm_usage_events e
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { createConversation } from "./conversation-crud.js";
|
|
6
|
+
import { getDb } from "./db-connection.js";
|
|
7
|
+
import { initializeDb } from "./db-init.js";
|
|
8
|
+
import {
|
|
9
|
+
countMessagesByRoleForConversations,
|
|
10
|
+
existingMessageIds,
|
|
11
|
+
latestAssistantMessage,
|
|
12
|
+
latestAssistantMessageBefore,
|
|
13
|
+
latestUserMessageRawContent,
|
|
14
|
+
messageConversationId,
|
|
15
|
+
} from "./message-reads.js";
|
|
16
|
+
import { messages } from "./schema/index.js";
|
|
17
|
+
|
|
18
|
+
await initializeDb();
|
|
19
|
+
|
|
20
|
+
let seq = 0;
|
|
21
|
+
function insertRow(opts: {
|
|
22
|
+
conversationId: string;
|
|
23
|
+
role: string;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
content?: string;
|
|
26
|
+
finalized?: 0 | 1;
|
|
27
|
+
}): string {
|
|
28
|
+
const id = `msg-${String(++seq).padStart(4, "0")}`;
|
|
29
|
+
getDb()
|
|
30
|
+
.insert(messages)
|
|
31
|
+
.values({
|
|
32
|
+
id,
|
|
33
|
+
conversationId: opts.conversationId,
|
|
34
|
+
role: opts.role,
|
|
35
|
+
content: opts.content ?? JSON.stringify([{ type: "text", text: "hi" }]),
|
|
36
|
+
createdAt: opts.createdAt,
|
|
37
|
+
metadata: null,
|
|
38
|
+
finalized: opts.finalized ?? 1,
|
|
39
|
+
})
|
|
40
|
+
.run();
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function seedConversation(): string {
|
|
45
|
+
return createConversation("Message reads test").id;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("message-reads", () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
db.run("DELETE FROM messages");
|
|
52
|
+
db.run("DELETE FROM conversations");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("latestAssistantMessage returns the newest finalized assistant row", () => {
|
|
56
|
+
const conv = seedConversation();
|
|
57
|
+
insertRow({ conversationId: conv, role: "user", createdAt: 1_000 });
|
|
58
|
+
const older = insertRow({
|
|
59
|
+
conversationId: conv,
|
|
60
|
+
role: "assistant",
|
|
61
|
+
createdAt: 2_000,
|
|
62
|
+
});
|
|
63
|
+
const newer = insertRow({
|
|
64
|
+
conversationId: conv,
|
|
65
|
+
role: "assistant",
|
|
66
|
+
createdAt: 3_000,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(latestAssistantMessage(conv)?.id).toBe(newer);
|
|
70
|
+
expect(latestAssistantMessage(conv)?.id).not.toBe(older);
|
|
71
|
+
expect(latestAssistantMessage("missing-conv")).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("latestAssistantMessage never anchors on a streaming row", () => {
|
|
75
|
+
const conv = seedConversation();
|
|
76
|
+
const finalizedReply = insertRow({
|
|
77
|
+
conversationId: conv,
|
|
78
|
+
role: "assistant",
|
|
79
|
+
createdAt: 1_000,
|
|
80
|
+
});
|
|
81
|
+
insertRow({
|
|
82
|
+
conversationId: conv,
|
|
83
|
+
role: "assistant",
|
|
84
|
+
createdAt: 2_000,
|
|
85
|
+
finalized: 0,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The streaming row is newest but is not a reply yet; the watermark must
|
|
89
|
+
// sit on the completed one until the turn finishes.
|
|
90
|
+
expect(latestAssistantMessage(conv)?.id).toBe(finalizedReply);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("a streaming row qualifies on its own once finalized", () => {
|
|
94
|
+
const conv = seedConversation();
|
|
95
|
+
insertRow({ conversationId: conv, role: "assistant", createdAt: 1_000 });
|
|
96
|
+
const streaming = insertRow({
|
|
97
|
+
conversationId: conv,
|
|
98
|
+
role: "assistant",
|
|
99
|
+
createdAt: 2_000,
|
|
100
|
+
finalized: 0,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
getDb()
|
|
104
|
+
.update(messages)
|
|
105
|
+
.set({ finalized: 1 })
|
|
106
|
+
.where(eq(messages.id, streaming))
|
|
107
|
+
.run();
|
|
108
|
+
|
|
109
|
+
expect(latestAssistantMessage(conv)?.id).toBe(streaming);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("latestAssistantMessageBefore is strict and skips streaming rows", () => {
|
|
113
|
+
const conv = seedConversation();
|
|
114
|
+
const first = insertRow({
|
|
115
|
+
conversationId: conv,
|
|
116
|
+
role: "assistant",
|
|
117
|
+
createdAt: 1_000,
|
|
118
|
+
});
|
|
119
|
+
insertRow({
|
|
120
|
+
conversationId: conv,
|
|
121
|
+
role: "assistant",
|
|
122
|
+
createdAt: 2_000,
|
|
123
|
+
finalized: 0,
|
|
124
|
+
});
|
|
125
|
+
insertRow({ conversationId: conv, role: "assistant", createdAt: 3_000 });
|
|
126
|
+
|
|
127
|
+
// Strictly before 3000: the streaming row at 2000 is passed over in
|
|
128
|
+
// favour of the completed row at 1000.
|
|
129
|
+
expect(latestAssistantMessageBefore(conv, 3_000)?.id).toBe(first);
|
|
130
|
+
// Strictly before 1000: nothing.
|
|
131
|
+
expect(latestAssistantMessageBefore(conv, 1_000)).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("latestUserMessageRawContent resolves same-timestamp rows by insertion order", () => {
|
|
135
|
+
const conv = seedConversation();
|
|
136
|
+
insertRow({
|
|
137
|
+
conversationId: conv,
|
|
138
|
+
role: "user",
|
|
139
|
+
createdAt: 1_000,
|
|
140
|
+
content: "first",
|
|
141
|
+
});
|
|
142
|
+
insertRow({
|
|
143
|
+
conversationId: conv,
|
|
144
|
+
role: "user",
|
|
145
|
+
createdAt: 1_000,
|
|
146
|
+
content: "second",
|
|
147
|
+
});
|
|
148
|
+
insertRow({ conversationId: conv, role: "assistant", createdAt: 2_000 });
|
|
149
|
+
|
|
150
|
+
expect(latestUserMessageRawContent(conv)).toBe("second");
|
|
151
|
+
expect(latestUserMessageRawContent("missing-conv")).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("countMessagesByRoleForConversations aggregates per conversation", () => {
|
|
155
|
+
const convA = seedConversation();
|
|
156
|
+
const convB = seedConversation();
|
|
157
|
+
insertRow({ conversationId: convA, role: "assistant", createdAt: 1_000 });
|
|
158
|
+
insertRow({ conversationId: convA, role: "assistant", createdAt: 2_000 });
|
|
159
|
+
insertRow({ conversationId: convA, role: "user", createdAt: 3_000 });
|
|
160
|
+
insertRow({ conversationId: convB, role: "assistant", createdAt: 4_000 });
|
|
161
|
+
|
|
162
|
+
const stats = countMessagesByRoleForConversations(
|
|
163
|
+
[convA, convB],
|
|
164
|
+
"assistant",
|
|
165
|
+
);
|
|
166
|
+
expect(stats.get(convA)).toEqual({ count: 2, lastAt: 2_000 });
|
|
167
|
+
expect(stats.get(convB)).toEqual({ count: 1, lastAt: 4_000 });
|
|
168
|
+
expect(countMessagesByRoleForConversations([], "assistant").size).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("existingMessageIds returns only ids that exist", () => {
|
|
172
|
+
const conv = seedConversation();
|
|
173
|
+
const present = insertRow({
|
|
174
|
+
conversationId: conv,
|
|
175
|
+
role: "user",
|
|
176
|
+
createdAt: 1_000,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const existing = existingMessageIds([present, "msg-not-there"]);
|
|
180
|
+
expect(existing.has(present)).toBe(true);
|
|
181
|
+
expect(existing.has("msg-not-there")).toBe(false);
|
|
182
|
+
expect(existingMessageIds([]).size).toBe(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("messageConversationId resolves ownership in any state", () => {
|
|
186
|
+
const conv = seedConversation();
|
|
187
|
+
const streaming = insertRow({
|
|
188
|
+
conversationId: conv,
|
|
189
|
+
role: "assistant",
|
|
190
|
+
createdAt: 1_000,
|
|
191
|
+
finalized: 0,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(messageConversationId(streaming)).toBe(conv);
|
|
195
|
+
expect(messageConversationId("msg-not-there")).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named read accessors for the `messages` table.
|
|
3
|
+
*
|
|
4
|
+
* Every row in `messages` is conversation content in some state of
|
|
5
|
+
* completeness: `finalized = 1` rows are immutable history, `finalized = 0`
|
|
6
|
+
* rows are still being written and their `content` may be a `{ ref }` into an
|
|
7
|
+
* in-flight delta file that disappears when the turn completes. Ad-hoc queries
|
|
8
|
+
* tend to encode a visibility decision without stating it; each accessor here
|
|
9
|
+
* states its decision in its name and contract, so a caller (and a reviewer)
|
|
10
|
+
* chooses between "completed history" and "any row" deliberately.
|
|
11
|
+
*
|
|
12
|
+
* Conversation-history reads (transcripts, lineage, pagination) stay with
|
|
13
|
+
* `getMessages` / `getMessagesAfter` / `getMessagesPaginated` in
|
|
14
|
+
* `conversation-crud.ts`, which also own fork-lineage resolution. This module
|
|
15
|
+
* is for the narrower point lookups and aggregates that other persistence
|
|
16
|
+
* modules need.
|
|
17
|
+
*
|
|
18
|
+
* Correlated subqueries embedded inside larger SQL (usage accounting's
|
|
19
|
+
* turn-index arithmetic, search candidate joins, `message_count` projections)
|
|
20
|
+
* are out of scope: extracting them would restructure their host queries. They
|
|
21
|
+
* carry their visibility decision as a comment at the site instead.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { and, desc, eq, inArray, lt, sql } from "drizzle-orm";
|
|
25
|
+
|
|
26
|
+
import { type DrizzleDb, getDb } from "./db-connection.js";
|
|
27
|
+
import { messages } from "./schema/index.js";
|
|
28
|
+
|
|
29
|
+
/** `(id, createdAt)` of a message, the shape cursor and watermark logic needs. */
|
|
30
|
+
export interface MessageRef {
|
|
31
|
+
id: string;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Structural read handle: anything that can `select` like the shared
|
|
37
|
+
* connection, which includes the connection itself and an open transaction.
|
|
38
|
+
* Every accessor takes `opts.db` with this type and defaults to the shared
|
|
39
|
+
* connection, so a caller holding its own handle (a transaction, an injected
|
|
40
|
+
* or in-memory database) reads the same database it writes.
|
|
41
|
+
*/
|
|
42
|
+
export type MessageReadHandle = Pick<DrizzleDb, "select">;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The newest completed assistant message in a conversation, or null when it
|
|
46
|
+
* has none.
|
|
47
|
+
*
|
|
48
|
+
* Finalized-only: an assistant row that is still streaming is not a reply yet,
|
|
49
|
+
* so consumers tracking "the latest reply" (read/unread watermarks, previews)
|
|
50
|
+
* must not anchor on it. The row qualifies on its own once the turn completes.
|
|
51
|
+
*
|
|
52
|
+
* `db` lets a caller inside a transaction read through its own handle.
|
|
53
|
+
*/
|
|
54
|
+
export function latestAssistantMessage(
|
|
55
|
+
conversationId: string,
|
|
56
|
+
opts?: { db?: MessageReadHandle },
|
|
57
|
+
): MessageRef | null {
|
|
58
|
+
return newestFinalizedAssistantRow(conversationId, opts?.db ?? getDb());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The newest completed assistant message strictly before `createdAt`, or null.
|
|
63
|
+
* Strict comparison: rewind logic that landed on a same-timestamp sibling
|
|
64
|
+
* would classify the latest reply as already seen.
|
|
65
|
+
*/
|
|
66
|
+
export function latestAssistantMessageBefore(
|
|
67
|
+
conversationId: string,
|
|
68
|
+
createdAt: number,
|
|
69
|
+
opts?: { db?: MessageReadHandle },
|
|
70
|
+
): MessageRef | null {
|
|
71
|
+
return newestFinalizedAssistantRow(
|
|
72
|
+
conversationId,
|
|
73
|
+
opts?.db ?? getDb(),
|
|
74
|
+
createdAt,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function newestFinalizedAssistantRow(
|
|
79
|
+
conversationId: string,
|
|
80
|
+
db: MessageReadHandle,
|
|
81
|
+
strictlyBefore?: number,
|
|
82
|
+
): MessageRef | null {
|
|
83
|
+
const row = db
|
|
84
|
+
.select({ id: messages.id, createdAt: messages.createdAt })
|
|
85
|
+
.from(messages)
|
|
86
|
+
.where(
|
|
87
|
+
and(
|
|
88
|
+
eq(messages.conversationId, conversationId),
|
|
89
|
+
eq(messages.role, "assistant"),
|
|
90
|
+
eq(messages.finalized, 1),
|
|
91
|
+
...(strictlyBefore === undefined
|
|
92
|
+
? []
|
|
93
|
+
: [lt(messages.createdAt, strictlyBefore)]),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
.orderBy(desc(messages.createdAt), desc(messages.id))
|
|
97
|
+
.limit(1)
|
|
98
|
+
.get();
|
|
99
|
+
return row ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Raw `content` of the last user-role row in a conversation, in insertion
|
|
104
|
+
* order (`rowid DESC`, not `createdAt`), or null when there is none.
|
|
105
|
+
*
|
|
106
|
+
* Insertion order is load-bearing for the undo path this serves: same-
|
|
107
|
+
* millisecond rows must resolve to the one written last. User rows are always
|
|
108
|
+
* written finalized, so no completeness predicate applies; only assistant
|
|
109
|
+
* turns stream.
|
|
110
|
+
*/
|
|
111
|
+
export function latestUserMessageRawContent(
|
|
112
|
+
conversationId: string,
|
|
113
|
+
opts?: { db?: MessageReadHandle },
|
|
114
|
+
): string | null {
|
|
115
|
+
const row = (opts?.db ?? getDb())
|
|
116
|
+
.select({ content: messages.content })
|
|
117
|
+
.from(messages)
|
|
118
|
+
.where(
|
|
119
|
+
and(
|
|
120
|
+
eq(messages.conversationId, conversationId),
|
|
121
|
+
eq(messages.role, "user"),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
.orderBy(sql`rowid DESC`)
|
|
125
|
+
.limit(1)
|
|
126
|
+
.get();
|
|
127
|
+
return row?.content ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Per-conversation count and latest timestamp of rows with `role`, for the
|
|
132
|
+
* given conversations.
|
|
133
|
+
*
|
|
134
|
+
* Any-state deliberately: these aggregates feed list ordering and previews,
|
|
135
|
+
* where a streaming reply still counts as activity. A caller needing
|
|
136
|
+
* completed-only aggregates should say so here rather than post-filtering.
|
|
137
|
+
*/
|
|
138
|
+
export function countMessagesByRoleForConversations(
|
|
139
|
+
conversationIds: string[],
|
|
140
|
+
role: string,
|
|
141
|
+
opts?: { db?: MessageReadHandle },
|
|
142
|
+
): Map<string, { count: number; lastAt: number }> {
|
|
143
|
+
if (conversationIds.length === 0) {
|
|
144
|
+
return new Map();
|
|
145
|
+
}
|
|
146
|
+
const rows = (opts?.db ?? getDb())
|
|
147
|
+
.select({
|
|
148
|
+
conversationId: messages.conversationId,
|
|
149
|
+
count: sql<number>`COUNT(*)`.as("count"),
|
|
150
|
+
lastAt: sql<number>`MAX(${messages.createdAt})`.as("last_at"),
|
|
151
|
+
})
|
|
152
|
+
.from(messages)
|
|
153
|
+
.where(
|
|
154
|
+
and(
|
|
155
|
+
inArray(messages.conversationId, conversationIds),
|
|
156
|
+
eq(messages.role, role),
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
.groupBy(messages.conversationId)
|
|
160
|
+
.all();
|
|
161
|
+
return new Map(
|
|
162
|
+
rows.map((r) => [
|
|
163
|
+
r.conversationId,
|
|
164
|
+
{ count: Number(r.count), lastAt: Number(r.lastAt) },
|
|
165
|
+
]),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Which of `messageIds` exist as rows, in any state.
|
|
171
|
+
*
|
|
172
|
+
* Any-state deliberately: existence is the question (orphan detection for
|
|
173
|
+
* rows referencing messages), and a streaming row exists. An empty input
|
|
174
|
+
* returns an empty set without querying.
|
|
175
|
+
*/
|
|
176
|
+
export function existingMessageIds(
|
|
177
|
+
messageIds: string[],
|
|
178
|
+
opts?: { db?: MessageReadHandle },
|
|
179
|
+
): Set<string> {
|
|
180
|
+
if (messageIds.length === 0) {
|
|
181
|
+
return new Set();
|
|
182
|
+
}
|
|
183
|
+
return new Set(
|
|
184
|
+
(opts?.db ?? getDb())
|
|
185
|
+
.select({ id: messages.id })
|
|
186
|
+
.from(messages)
|
|
187
|
+
.where(inArray(messages.id, messageIds))
|
|
188
|
+
.all()
|
|
189
|
+
.map((r) => r.id),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The conversation that owns `messageId`, in any state, or null when the
|
|
195
|
+
* message does not exist.
|
|
196
|
+
*
|
|
197
|
+
* Any-state deliberately: callers hold an id the user acted on (a bookmark on
|
|
198
|
+
* a rendered row, an attachment link), and a rendered row may still be
|
|
199
|
+
* streaming.
|
|
200
|
+
*/
|
|
201
|
+
export function messageConversationId(
|
|
202
|
+
messageId: string,
|
|
203
|
+
opts?: { db?: MessageReadHandle },
|
|
204
|
+
): string | null {
|
|
205
|
+
const row = (opts?.db ?? getDb())
|
|
206
|
+
.select({ conversationId: messages.conversationId })
|
|
207
|
+
.from(messages)
|
|
208
|
+
.where(eq(messages.id, messageId))
|
|
209
|
+
.get();
|
|
210
|
+
return row?.conversationId ?? null;
|
|
211
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import * as schema from "../schema.js";
|
|
7
|
+
import { migrateChatgptSubscriptionRowIdentity } from "./366-chatgpt-subscription-row-identity.js";
|
|
8
|
+
|
|
9
|
+
function createTestDb() {
|
|
10
|
+
const sqlite = new Database(":memory:");
|
|
11
|
+
sqlite.exec(/*sql*/ `
|
|
12
|
+
CREATE TABLE provider_connections (
|
|
13
|
+
name TEXT PRIMARY KEY,
|
|
14
|
+
provider TEXT NOT NULL,
|
|
15
|
+
auth TEXT NOT NULL,
|
|
16
|
+
label TEXT,
|
|
17
|
+
base_url TEXT,
|
|
18
|
+
models TEXT,
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
updated_at INTEGER NOT NULL
|
|
21
|
+
);
|
|
22
|
+
`);
|
|
23
|
+
return { sqlite, db: drizzle(sqlite, { schema }) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function insertRow(
|
|
27
|
+
sqlite: Database,
|
|
28
|
+
name: string,
|
|
29
|
+
provider: string,
|
|
30
|
+
auth: string,
|
|
31
|
+
): void {
|
|
32
|
+
sqlite
|
|
33
|
+
.query(
|
|
34
|
+
/*sql*/ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at)
|
|
35
|
+
VALUES (?, ?, ?, 1, 1)`,
|
|
36
|
+
)
|
|
37
|
+
.run(name, provider, auth);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readProvider(sqlite: Database, name: string): string | undefined {
|
|
41
|
+
const row = sqlite
|
|
42
|
+
.query(`SELECT provider FROM provider_connections WHERE name = ?`)
|
|
43
|
+
.get(name) as { provider: string } | null;
|
|
44
|
+
return row?.provider;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("migration 366: chatgpt-subscription row identity", () => {
|
|
48
|
+
test("flips the subscription row to provider chatgpt", () => {
|
|
49
|
+
const { sqlite, db } = createTestDb();
|
|
50
|
+
insertRow(
|
|
51
|
+
sqlite,
|
|
52
|
+
"chatgpt-subscription",
|
|
53
|
+
"openai",
|
|
54
|
+
'{"type":"oauth_subscription","credential":"credential/chatgpt/access_token"}',
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
migrateChatgptSubscriptionRowIdentity(db);
|
|
58
|
+
|
|
59
|
+
expect(readProvider(sqlite, "chatgpt-subscription")).toBe("chatgpt");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("leaves a claiming row with key auth untouched", () => {
|
|
63
|
+
const { sqlite, db } = createTestDb();
|
|
64
|
+
insertRow(
|
|
65
|
+
sqlite,
|
|
66
|
+
"chatgpt-subscription",
|
|
67
|
+
"openai",
|
|
68
|
+
'{"type":"api_key","credential":"vault/openai"}',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
migrateChatgptSubscriptionRowIdentity(db);
|
|
72
|
+
|
|
73
|
+
expect(readProvider(sqlite, "chatgpt-subscription")).toBe("openai");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("leaves other rows untouched", () => {
|
|
77
|
+
const { sqlite, db } = createTestDb();
|
|
78
|
+
insertRow(sqlite, "openai-key", "openai", '{"type":"api_key"}');
|
|
79
|
+
insertRow(sqlite, "vellum", "vellum", '{"type":"platform"}');
|
|
80
|
+
|
|
81
|
+
migrateChatgptSubscriptionRowIdentity(db);
|
|
82
|
+
|
|
83
|
+
expect(readProvider(sqlite, "openai-key")).toBe("openai");
|
|
84
|
+
expect(readProvider(sqlite, "vellum")).toBe("vellum");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("leaves unparseable auth untouched", () => {
|
|
88
|
+
const { sqlite, db } = createTestDb();
|
|
89
|
+
insertRow(sqlite, "chatgpt-subscription", "openai", "not json");
|
|
90
|
+
|
|
91
|
+
migrateChatgptSubscriptionRowIdentity(db);
|
|
92
|
+
|
|
93
|
+
expect(readProvider(sqlite, "chatgpt-subscription")).toBe("openai");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("is idempotent", () => {
|
|
97
|
+
const { sqlite, db } = createTestDb();
|
|
98
|
+
insertRow(
|
|
99
|
+
sqlite,
|
|
100
|
+
"chatgpt-subscription",
|
|
101
|
+
"openai",
|
|
102
|
+
'{"type":"oauth_subscription"}',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
migrateChatgptSubscriptionRowIdentity(db);
|
|
106
|
+
migrateChatgptSubscriptionRowIdentity(db);
|
|
107
|
+
|
|
108
|
+
expect(readProvider(sqlite, "chatgpt-subscription")).toBe("chatgpt");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("no-ops without the table or the row", () => {
|
|
112
|
+
const bare = new Database(":memory:");
|
|
113
|
+
expect(() =>
|
|
114
|
+
migrateChatgptSubscriptionRowIdentity(drizzle(bare, { schema })),
|
|
115
|
+
).not.toThrow();
|
|
116
|
+
|
|
117
|
+
const { db } = createTestDb();
|
|
118
|
+
expect(() => migrateChatgptSubscriptionRowIdentity(db)).not.toThrow();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stamp the ChatGPT-subscription row with its own provider identity.
|
|
5
|
+
*
|
|
6
|
+
* The row is the subscription route by definition (auth modality = provider
|
|
7
|
+
* identity), but it historically stored `provider: "openai"` and was found
|
|
8
|
+
* by name, which left provider "openai" ambiguous on disk: a BYOK api_key
|
|
9
|
+
* row and the subscription row read identically at the provider column.
|
|
10
|
+
* With the row carrying `provider: "chatgpt"`, auth type is derivable from
|
|
11
|
+
* the provider for every row, and provider-keyed scans for "openai" no
|
|
12
|
+
* longer surface the subscription row.
|
|
13
|
+
*
|
|
14
|
+
* Scoped tightly: only the canonical row name with oauth_subscription auth
|
|
15
|
+
* is flipped. A claiming row under the same name with key auth is an
|
|
16
|
+
* ordinary connection and keeps its provider. Idempotent: a flipped row no
|
|
17
|
+
* longer matches, and the table-exists guard covers databases from before
|
|
18
|
+
* migration 243.
|
|
19
|
+
*/
|
|
20
|
+
export function migrateChatgptSubscriptionRowIdentity(
|
|
21
|
+
database: DrizzleDb,
|
|
22
|
+
): void {
|
|
23
|
+
const raw = getSqliteFrom(database);
|
|
24
|
+
|
|
25
|
+
const tableExists = raw
|
|
26
|
+
.query(
|
|
27
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'provider_connections'`,
|
|
28
|
+
)
|
|
29
|
+
.get();
|
|
30
|
+
if (!tableExists) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const row = raw
|
|
35
|
+
.query(
|
|
36
|
+
`SELECT provider, auth FROM provider_connections WHERE name = 'chatgpt-subscription'`,
|
|
37
|
+
)
|
|
38
|
+
.get() as { provider: string; auth: string } | null;
|
|
39
|
+
if (!row || row.provider !== "openai") {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let authType: unknown;
|
|
44
|
+
try {
|
|
45
|
+
const parsed: unknown = JSON.parse(row.auth);
|
|
46
|
+
authType =
|
|
47
|
+
parsed !== null && typeof parsed === "object"
|
|
48
|
+
? (parsed as { type?: unknown }).type
|
|
49
|
+
: undefined;
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (authType !== "oauth_subscription") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
raw
|
|
58
|
+
.prepare(
|
|
59
|
+
`UPDATE provider_connections SET provider = 'chatgpt', updated_at = ? WHERE name = 'chatgpt-subscription'`,
|
|
60
|
+
)
|
|
61
|
+
.run(Date.now());
|
|
62
|
+
}
|
|
@@ -19,8 +19,32 @@ import { sql } from "drizzle-orm";
|
|
|
19
19
|
export function realUserTurnContentFilter(
|
|
20
20
|
alias: string,
|
|
21
21
|
): ReturnType<typeof sql> {
|
|
22
|
-
return sql.raw(
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
return sql.raw(realUserTurnContentFilterSql(alias));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Raw-string form of {@link realUserTurnContentFilter}, for queries built as
|
|
27
|
+
* plain SQL template strings (`rawAll` sites). Same fragment, one source.
|
|
28
|
+
*
|
|
29
|
+
* `finalized = 1`: the grouped tool-result row is a USER-role row reserved
|
|
30
|
+
* in-flight (`ensureToolResultRowReserved`), and while it streams its content
|
|
31
|
+
* is a `{ ref }` pointer that does not contain the tool_result text the
|
|
32
|
+
* exclusions match. Without this predicate it counts as a real user turn
|
|
33
|
+
* mid-tool-execution and stops counting once the content folds inline.
|
|
34
|
+
*
|
|
35
|
+
* Bare `[]` content is deliberately NOT excluded: a hidden send persists its
|
|
36
|
+
* user row with empty display content (`processMessage` with
|
|
37
|
+
* `displayContent: ""`) and still runs a turn, so `[]` rows are legitimate
|
|
38
|
+
* turns. The cost is a narrow pre-existing window in the writer-creation
|
|
39
|
+
* fallback, where the tool-result row is born finalized with a `[]`
|
|
40
|
+
* placeholder and counts until the first content write lands; that window is
|
|
41
|
+
* rare (writer creation failure only) and transient, and closing it needs a
|
|
42
|
+
* signal that distinguishes the placeholder from a real empty-display turn.
|
|
43
|
+
*/
|
|
44
|
+
export function realUserTurnContentFilterSql(alias: string): string {
|
|
45
|
+
return (
|
|
46
|
+
`${alias}.finalized = 1 ` +
|
|
47
|
+
`AND ${alias}.content NOT LIKE '%"type":"tool\\_result"%' ESCAPE '\\' ` +
|
|
48
|
+
`AND ${alias}.content NOT LIKE '%"type":"web\\_search\\_tool\\_result"%' ESCAPE '\\'`
|
|
25
49
|
);
|
|
26
50
|
}
|
package/src/persistence/steps.ts
CHANGED
|
@@ -473,6 +473,7 @@ import { migrateAddConversationSubagentKind } from "./migrations/362-add-convers
|
|
|
473
473
|
import { migrateBackfillScheduleInferenceProfile } from "./migrations/363-backfill-schedule-inference-profile.js";
|
|
474
474
|
import { migrateAddScheduleSourceKey } from "./migrations/364-add-schedule-source-key.js";
|
|
475
475
|
import { migrateAddConversationForkStrategy } from "./migrations/365-add-conversation-fork-strategy.js";
|
|
476
|
+
import { migrateChatgptSubscriptionRowIdentity } from "./migrations/366-chatgpt-subscription-row-identity.js";
|
|
476
477
|
import type { MigrationStep } from "./migrations/run-migrations.js";
|
|
477
478
|
|
|
478
479
|
export const migrationSteps: MigrationStep[] = [
|
|
@@ -1573,4 +1574,12 @@ export const migrationSteps: MigrationStep[] = [
|
|
|
1573
1574
|
},
|
|
1574
1575
|
migrateAddScheduleSourceKey,
|
|
1575
1576
|
migrateAddConversationForkStrategy,
|
|
1577
|
+
{
|
|
1578
|
+
name: "migrateChatgptSubscriptionRowIdentity",
|
|
1579
|
+
run: migrateChatgptSubscriptionRowIdentity,
|
|
1580
|
+
// The table-exists guard treats a missing table as nothing-to-do, so the
|
|
1581
|
+
// creating migration must be checkpointed first or a repair flow could
|
|
1582
|
+
// permanently checkpoint the no-op.
|
|
1583
|
+
dependsOn: ["migrateCreateProviderConnections"],
|
|
1584
|
+
},
|
|
1576
1585
|
];
|