@vellumai/assistant 0.8.2 → 0.8.3
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 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
1
2
|
import { resolveCallSiteConfig } from "../config/llm-resolver.js";
|
|
3
|
+
import type { AssistantConfig } from "../config/schema.js";
|
|
2
4
|
import { type LLMConfig } from "../config/schemas/llm.js";
|
|
3
5
|
import { getProviderKeyAsync } from "../security/secure-keys.js";
|
|
4
6
|
import { ProviderNotConfiguredError } from "../util/errors.js";
|
|
@@ -26,6 +28,7 @@ const log = getLogger("provider-registry");
|
|
|
26
28
|
|
|
27
29
|
const providers = new Map<string, Provider>();
|
|
28
30
|
const routingSources = new Map<string, "user-key" | "managed-proxy">();
|
|
31
|
+
const OPENAI_COMPATIBLE_ENDPOINTS_FLAG = "openai-compatible-endpoints";
|
|
29
32
|
|
|
30
33
|
/** Per-connection provider cache, keyed by connection name. */
|
|
31
34
|
const connectionProviders = new Map<string, Provider>();
|
|
@@ -69,6 +72,16 @@ export interface ProvidersConfig {
|
|
|
69
72
|
timeouts?: { providerStreamTimeoutSec?: number };
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
function isProviderFeatureFlagEnabled(
|
|
76
|
+
key: string,
|
|
77
|
+
config: ProvidersConfig,
|
|
78
|
+
): boolean {
|
|
79
|
+
return isAssistantFeatureFlagEnabled(
|
|
80
|
+
key,
|
|
81
|
+
config as unknown as AssistantConfig,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
72
85
|
function resolveModel(config: ProvidersConfig, providerName: string): string {
|
|
73
86
|
const resolved = resolveCallSiteConfig("mainAgent", config.llm);
|
|
74
87
|
const inferenceProvider = resolved.provider;
|
|
@@ -130,6 +143,13 @@ export async function initializeProviders(
|
|
|
130
143
|
).provider;
|
|
131
144
|
|
|
132
145
|
for (const entry of PROVIDER_CATALOG) {
|
|
146
|
+
if (
|
|
147
|
+
entry.featureFlag &&
|
|
148
|
+
!isProviderFeatureFlagEnabled(entry.featureFlag, config)
|
|
149
|
+
) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
const isKeyless = entry.setupMode === "keyless";
|
|
134
154
|
|
|
135
155
|
// Credential resolution: user key first, managed proxy second. Keyless
|
|
@@ -202,10 +222,19 @@ export async function resolveProviderFromConnection(
|
|
|
202
222
|
connection: ProviderConnection,
|
|
203
223
|
config: ProvidersConfig,
|
|
204
224
|
): Promise<Provider | null> {
|
|
225
|
+
if (
|
|
226
|
+
connection.provider === "openai-compatible" &&
|
|
227
|
+
!isProviderFeatureFlagEnabled(OPENAI_COMPATIBLE_ENDPOINTS_FLAG, config)
|
|
228
|
+
) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
205
232
|
const cached = connectionProviders.get(connection.name);
|
|
206
233
|
if (cached) return cached;
|
|
207
234
|
|
|
208
|
-
const authResult = await resolveAuth(connection.auth, connection.provider
|
|
235
|
+
const authResult = await resolveAuth(connection.auth, connection.provider, {
|
|
236
|
+
baseUrl: connection.baseUrl,
|
|
237
|
+
});
|
|
209
238
|
if (!authResult.ok) {
|
|
210
239
|
const err = authResult.error;
|
|
211
240
|
if (err.code === "not_implemented") {
|
|
@@ -57,7 +57,11 @@ import {
|
|
|
57
57
|
} from "../daemon/disk-pressure-policy.js";
|
|
58
58
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
59
59
|
import { getConversationOverrideProfile } from "../memory/conversation-crud.js";
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
buildProviderErrorResponsePayload,
|
|
62
|
+
recordRequestLog,
|
|
63
|
+
setAgentLoopExitReasonOnLatestLog,
|
|
64
|
+
} from "../memory/llm-request-log-store.js";
|
|
61
65
|
import type { TurnContext } from "../plugins/types.js";
|
|
62
66
|
import type { Message } from "../providers/types.js";
|
|
63
67
|
import { getLogger } from "../util/logger.js";
|
|
@@ -548,6 +552,12 @@ export async function wakeAgentForOpportunity(
|
|
|
548
552
|
provider?: string;
|
|
549
553
|
};
|
|
550
554
|
const pendingLogs: PendingLog[] = [];
|
|
555
|
+
// Exit reason deferred alongside pendingLogs. Same drop-on-silent-
|
|
556
|
+
// wake guarantee: if the wake never goes live, this stays null and
|
|
557
|
+
// no DB row is touched. Applied after pendingLogs flush in goLive
|
|
558
|
+
// so the latest-row lookup in `setAgentLoopExitReasonOnLatestLog`
|
|
559
|
+
// can see the freshly-persisted final usage row.
|
|
560
|
+
let pendingExitReason: string | null = null;
|
|
551
561
|
const persistLog = (record: PendingLog): void => {
|
|
552
562
|
try {
|
|
553
563
|
recordRequestLog(
|
|
@@ -564,6 +574,16 @@ export async function wakeAgentForOpportunity(
|
|
|
564
574
|
);
|
|
565
575
|
}
|
|
566
576
|
};
|
|
577
|
+
const persistExitReason = (reason: string): void => {
|
|
578
|
+
try {
|
|
579
|
+
setAgentLoopExitReasonOnLatestLog(conversationId, reason);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
log.warn(
|
|
582
|
+
{ err, conversationId, source, reason },
|
|
583
|
+
"agent-wake: failed to persist agent_loop_exit_reason (non-fatal)",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
567
587
|
const safeEmit = (event: AgentEvent): void => {
|
|
568
588
|
try {
|
|
569
589
|
target.emitAgentEvent(event);
|
|
@@ -590,6 +610,38 @@ export async function wakeAgentForOpportunity(
|
|
|
590
610
|
persistLog(record);
|
|
591
611
|
}
|
|
592
612
|
}
|
|
613
|
+
// Mirror the same recording side-effect for provider-rejected calls.
|
|
614
|
+
// `handleProviderError` in the daemon dispatcher persists these on the
|
|
615
|
+
// normal turn path; the wake path owns its own onEvent and bypasses
|
|
616
|
+
// that dispatcher entirely, so we replicate here. Buffering rules
|
|
617
|
+
// match the success path: if the wake never goes live (silent no-op),
|
|
618
|
+
// the rows are dropped so a stale `messageId IS NULL` row doesn't get
|
|
619
|
+
// mis-backfilled onto an unrelated future assistant message.
|
|
620
|
+
if (event.type === "provider_error") {
|
|
621
|
+
const record: PendingLog = {
|
|
622
|
+
rawRequest: event.rawRequest,
|
|
623
|
+
rawResponse: buildProviderErrorResponsePayload(event.error),
|
|
624
|
+
provider: event.actualProvider,
|
|
625
|
+
};
|
|
626
|
+
if (mode === "buffering") {
|
|
627
|
+
pendingLogs.push(record);
|
|
628
|
+
} else {
|
|
629
|
+
persistLog(record);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Replicates the setAgentLoopExitReasonOnLatestLog side-effect that
|
|
633
|
+
// `dispatchAgentEvent` does for the normal path. In live mode the
|
|
634
|
+
// final usage event of the run has already landed its row, so the
|
|
635
|
+
// latest-row lookup hits the right target. In buffering mode the
|
|
636
|
+
// reason is stashed and applied in `goLive` after pendingLogs are
|
|
637
|
+
// persisted, preserving the same ordering guarantee.
|
|
638
|
+
if (event.type === "agent_loop_exit") {
|
|
639
|
+
if (mode === "buffering") {
|
|
640
|
+
pendingExitReason = event.reason;
|
|
641
|
+
} else {
|
|
642
|
+
persistExitReason(event.reason);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
593
645
|
if (mode === "buffering") {
|
|
594
646
|
buffered.push(event);
|
|
595
647
|
return;
|
|
@@ -648,6 +700,14 @@ export async function wakeAgentForOpportunity(
|
|
|
648
700
|
persistLog(record);
|
|
649
701
|
}
|
|
650
702
|
pendingLogs.length = 0;
|
|
703
|
+
// Apply the deferred exit reason after pendingLogs are persisted —
|
|
704
|
+
// the latest-row lookup in `setAgentLoopExitReasonOnLatestLog`
|
|
705
|
+
// needs the final usage row to already exist. Cleared after use so
|
|
706
|
+
// an extremely unlikely double-goLive can't double-stamp.
|
|
707
|
+
if (pendingExitReason !== null) {
|
|
708
|
+
persistExitReason(pendingExitReason);
|
|
709
|
+
pendingExitReason = null;
|
|
710
|
+
}
|
|
651
711
|
mode = "live";
|
|
652
712
|
};
|
|
653
713
|
|
|
@@ -243,6 +243,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
243
243
|
endpoint: "integrations/slack/channel/config:DELETE",
|
|
244
244
|
scopes: ["settings.write"],
|
|
245
245
|
},
|
|
246
|
+
{ endpoint: "integrations/a2a/invite", scopes: ["settings.write"] },
|
|
246
247
|
{ endpoint: "channel-verification-sessions", scopes: ["settings.write"] },
|
|
247
248
|
{
|
|
248
249
|
endpoint: "channel-verification-sessions:DELETE",
|
|
@@ -686,6 +687,18 @@ for (const endpoint of INTERNAL_ENDPOINTS) {
|
|
|
686
687
|
});
|
|
687
688
|
}
|
|
688
689
|
|
|
690
|
+
// A2A invite completion: gateway-only (platform-orchestrated)
|
|
691
|
+
registerPolicy("integrations/a2a/invite/complete", {
|
|
692
|
+
requiredScopes: ["internal.write"],
|
|
693
|
+
allowedPrincipalTypes: ["svc_gateway"],
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// A2A invite redemption: gateway-only (platform-orchestrated)
|
|
697
|
+
registerPolicy("integrations/a2a/invite/redeem", {
|
|
698
|
+
requiredScopes: ["internal.write"],
|
|
699
|
+
allowedPrincipalTypes: ["svc_gateway"],
|
|
700
|
+
});
|
|
701
|
+
|
|
689
702
|
// Admin control-plane endpoints: gateway-only
|
|
690
703
|
registerPolicy("admin/upgrade-broadcast", {
|
|
691
704
|
requiredScopes: ["internal.write"],
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
import { isHttpAuthDisabled } from "../config/env.js";
|
|
29
29
|
import { getIsPlatform } from "../config/env-registry.js";
|
|
30
30
|
import { getConfig } from "../config/loader.js";
|
|
31
|
+
import { createApprovalCopyGenerator } from "../daemon/approval-generators.js";
|
|
32
|
+
import { createGuardianActionCopyGenerator } from "../daemon/guardian-action-generators.js";
|
|
31
33
|
import { processMessage } from "../daemon/process-message.js";
|
|
32
34
|
import { createLiveVoiceSession } from "../live-voice/live-voice-session.js";
|
|
33
35
|
import { LiveVoiceSessionManager } from "../live-voice/live-voice-session-manager.js";
|
|
@@ -96,10 +98,8 @@ import { matchSkillRoute } from "./skill-route-registry.js";
|
|
|
96
98
|
export { isPrivateAddress } from "./middleware/auth.js";
|
|
97
99
|
|
|
98
100
|
import type {
|
|
99
|
-
ApprovalConversationGenerator,
|
|
100
101
|
ApprovalCopyGenerator,
|
|
101
102
|
GuardianActionCopyGenerator,
|
|
102
|
-
GuardianFollowUpConversationGenerator,
|
|
103
103
|
RuntimeHttpServerOptions,
|
|
104
104
|
} from "./http-types.js";
|
|
105
105
|
|
|
@@ -161,10 +161,8 @@ export class RuntimeHttpServer {
|
|
|
161
161
|
private port: number;
|
|
162
162
|
private hostname: string;
|
|
163
163
|
|
|
164
|
-
private approvalCopyGenerator
|
|
165
|
-
private
|
|
166
|
-
private guardianActionCopyGenerator?: GuardianActionCopyGenerator;
|
|
167
|
-
private guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator;
|
|
164
|
+
private readonly approvalCopyGenerator: ApprovalCopyGenerator;
|
|
165
|
+
private readonly guardianActionCopyGenerator: GuardianActionCopyGenerator;
|
|
168
166
|
private retrySweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
169
167
|
private sweepInProgress = false;
|
|
170
168
|
|
|
@@ -175,11 +173,8 @@ export class RuntimeHttpServer {
|
|
|
175
173
|
this.port = options.port ?? DEFAULT_PORT;
|
|
176
174
|
this.hostname = options.hostname ?? DEFAULT_HOSTNAME;
|
|
177
175
|
|
|
178
|
-
this.approvalCopyGenerator =
|
|
179
|
-
this.
|
|
180
|
-
this.guardianActionCopyGenerator = options.guardianActionCopyGenerator;
|
|
181
|
-
this.guardianFollowUpConversationGenerator =
|
|
182
|
-
options.guardianFollowUpConversationGenerator;
|
|
176
|
+
this.approvalCopyGenerator = createApprovalCopyGenerator();
|
|
177
|
+
this.guardianActionCopyGenerator = createGuardianActionCopyGenerator();
|
|
183
178
|
this.liveVoiceSessionManager = new LiveVoiceSessionManager({
|
|
184
179
|
createSession: (context) => createLiveVoiceSession(context),
|
|
185
180
|
});
|
|
@@ -560,11 +555,7 @@ export class RuntimeHttpServer {
|
|
|
560
555
|
const endpoint = url.pathname.slice("/v1/".length).replace(/\/$/, "");
|
|
561
556
|
meta = this.router.findLoggingMetadata(req.method, endpoint) ?? undefined;
|
|
562
557
|
}
|
|
563
|
-
return withRequestLogging(
|
|
564
|
-
req,
|
|
565
|
-
() => this.routeRequest(req, server),
|
|
566
|
-
meta,
|
|
567
|
-
);
|
|
558
|
+
return withRequestLogging(req, () => this.routeRequest(req, server), meta);
|
|
568
559
|
}
|
|
569
560
|
|
|
570
561
|
private async routeRequest(
|
|
@@ -14,10 +14,6 @@ import type {
|
|
|
14
14
|
export type { SlackInboundMessageMetadata };
|
|
15
15
|
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
16
16
|
import type { AssistantEventHub } from "./assistant-event-hub.js";
|
|
17
|
-
import type {
|
|
18
|
-
ApprovalCopyGenerator,
|
|
19
|
-
GuardianActionCopyGenerator,
|
|
20
|
-
} from "./message-composer-types.js";
|
|
21
17
|
|
|
22
18
|
export type {
|
|
23
19
|
ApprovalCopyGenerator,
|
|
@@ -61,41 +57,6 @@ export type ApprovalConversationGenerator = (
|
|
|
61
57
|
context: ApprovalConversationContext,
|
|
62
58
|
) => Promise<ApprovalConversationResult>;
|
|
63
59
|
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Guardian follow-up conversation flow types
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
/** The disposition returned by the guardian follow-up conversation engine. */
|
|
69
|
-
export type GuardianFollowUpDisposition =
|
|
70
|
-
| "call_back"
|
|
71
|
-
| "decline"
|
|
72
|
-
| "keep_pending";
|
|
73
|
-
|
|
74
|
-
/** Structured result from a single turn of the guardian follow-up conversation. */
|
|
75
|
-
export interface GuardianFollowUpTurnResult {
|
|
76
|
-
disposition: GuardianFollowUpDisposition;
|
|
77
|
-
replyText: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Input context for the guardian follow-up conversation engine. */
|
|
81
|
-
export interface GuardianFollowUpConversationContext {
|
|
82
|
-
/** The original question that was asked during the voice call. */
|
|
83
|
-
questionText: string;
|
|
84
|
-
/** The guardian's late answer text that initiated the follow-up. */
|
|
85
|
-
lateAnswerText: string;
|
|
86
|
-
/** The guardian's latest reply in the follow-up conversation. */
|
|
87
|
-
guardianReply: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Daemon-injected function that processes one turn of a guardian follow-up
|
|
92
|
-
* conversation. Classifies the guardian's intent into a structured disposition
|
|
93
|
-
* and produces a natural reply.
|
|
94
|
-
*/
|
|
95
|
-
export type GuardianFollowUpConversationGenerator = (
|
|
96
|
-
context: GuardianFollowUpConversationContext,
|
|
97
|
-
) => Promise<GuardianFollowUpTurnResult>;
|
|
98
|
-
|
|
99
60
|
export interface RuntimeMessageConversationOptions {
|
|
100
61
|
transport?: {
|
|
101
62
|
channelId: ChannelId;
|
|
@@ -173,14 +134,6 @@ export interface RuntimeHttpServerOptions {
|
|
|
173
134
|
port?: number;
|
|
174
135
|
/** Hostname / IP to bind to. Defaults to '127.0.0.1' (loopback-only). */
|
|
175
136
|
hostname?: string;
|
|
176
|
-
/** Daemon-injected generator for approval copy (provider-backed). */
|
|
177
|
-
approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
178
|
-
/** Daemon-injected generator for conversational approval flow (provider-backed). */
|
|
179
|
-
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
180
|
-
/** Daemon-injected generator for guardian action copy (provider-backed). */
|
|
181
|
-
guardianActionCopyGenerator?: GuardianActionCopyGenerator;
|
|
182
|
-
/** Daemon-injected generator for guardian follow-up conversation (provider-backed). */
|
|
183
|
-
guardianFollowUpConversationGenerator?: GuardianFollowUpConversationGenerator;
|
|
184
137
|
}
|
|
185
138
|
|
|
186
139
|
export interface RuntimeAttachmentMetadata {
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asserts `listConsolidationRuns` maps background-conversation rows tagged
|
|
3
|
+
* with `source = MEMORY_V2_CONSOLIDATION_SOURCE` into the heartbeat-runs
|
|
4
|
+
* response shape, derives `status` / `finishedAt` / `durationMs` from
|
|
5
|
+
* **assistant-message presence** (not `lastMessageAt`), and clamps the
|
|
6
|
+
* `limit` query param.
|
|
7
|
+
*
|
|
8
|
+
* Synthetic-field semantics covered here:
|
|
9
|
+
* - `id` and `conversationId` both equal the conversation row's id.
|
|
10
|
+
* - `scheduledFor` and `startedAt` both equal `conversation.createdAt`
|
|
11
|
+
* (no separate schedule timestamp on the row).
|
|
12
|
+
* - `finishedAt` is the `createdAt` of the LATEST assistant message,
|
|
13
|
+
* NOT `conversation.lastMessageAt` — the kickoff user prompt bumps
|
|
14
|
+
* `lastMessageAt` before the agent runs, so it cannot be used as a
|
|
15
|
+
* completion signal.
|
|
16
|
+
* - `durationMs` is `finishedAt − startedAt` when both are present, else
|
|
17
|
+
* null.
|
|
18
|
+
* - `status` is `"ok"` when the conversation has at least one assistant
|
|
19
|
+
* message (positive evidence the agent emitted output) and `"running"`
|
|
20
|
+
* otherwise — including the case where only the kickoff user prompt
|
|
21
|
+
* has been persisted.
|
|
22
|
+
* - `skipReason` and `error` are always null — the conversation row
|
|
23
|
+
* alone cannot distinguish a clean run from a mid-flight crash even
|
|
24
|
+
* once assistant output exists.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
28
|
+
|
|
29
|
+
mock.module("../../../util/logger.js", () => ({
|
|
30
|
+
getLogger: () =>
|
|
31
|
+
new Proxy({} as Record<string, unknown>, {
|
|
32
|
+
get: () => () => {},
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
import { createConversation } from "../../../memory/conversation-crud.js";
|
|
37
|
+
import { getDb } from "../../../memory/db-connection.js";
|
|
38
|
+
import { initializeDb } from "../../../memory/db-init.js";
|
|
39
|
+
import { rawRun } from "../../../memory/raw-query.js";
|
|
40
|
+
import { ROUTES } from "../consolidation-routes.js";
|
|
41
|
+
import type { RouteDefinition } from "../types.js";
|
|
42
|
+
|
|
43
|
+
initializeDb();
|
|
44
|
+
|
|
45
|
+
function resetTables(): void {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
db.run(`DELETE FROM messages`);
|
|
48
|
+
db.run(`DELETE FROM conversations`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findHandler(operationId: string): RouteDefinition["handler"] {
|
|
52
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
53
|
+
if (!route) throw new Error(`Route ${operationId} not found`);
|
|
54
|
+
return route.handler;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function insertMessage(
|
|
58
|
+
conversationId: string,
|
|
59
|
+
role: string,
|
|
60
|
+
createdAt: number,
|
|
61
|
+
): void {
|
|
62
|
+
rawRun(
|
|
63
|
+
"INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
64
|
+
`msg-${conversationId}-${role}-${createdAt}`,
|
|
65
|
+
conversationId,
|
|
66
|
+
role,
|
|
67
|
+
"x",
|
|
68
|
+
createdAt,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface RunRecord {
|
|
73
|
+
id: string;
|
|
74
|
+
scheduledFor: number;
|
|
75
|
+
startedAt: number | null;
|
|
76
|
+
finishedAt: number | null;
|
|
77
|
+
durationMs: number | null;
|
|
78
|
+
status: "ok" | "running";
|
|
79
|
+
skipReason: string | null;
|
|
80
|
+
error: string | null;
|
|
81
|
+
conversationId: string | null;
|
|
82
|
+
createdAt: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ListRunsResponse {
|
|
86
|
+
runs: RunRecord[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("listConsolidationRuns handler", () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
resetTables();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns only conversations sourced from memory_v2_consolidation", async () => {
|
|
95
|
+
createConversation({ title: "c1", source: "memory_v2_consolidation" });
|
|
96
|
+
createConversation({ title: "h1", source: "heartbeat" });
|
|
97
|
+
createConversation({ title: "u1", source: "user" });
|
|
98
|
+
|
|
99
|
+
const handler = findHandler("listConsolidationRuns");
|
|
100
|
+
const result = (await handler({})) as ListRunsResponse;
|
|
101
|
+
|
|
102
|
+
expect(result.runs).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("synthesizes status='ok' with finishedAt from latest assistant message", async () => {
|
|
106
|
+
const conv = createConversation({
|
|
107
|
+
title: "c1",
|
|
108
|
+
source: "memory_v2_consolidation",
|
|
109
|
+
});
|
|
110
|
+
rawRun(
|
|
111
|
+
"UPDATE conversations SET created_at = ? WHERE id = ?",
|
|
112
|
+
1000,
|
|
113
|
+
conv.id,
|
|
114
|
+
);
|
|
115
|
+
// Kickoff user prompt at t=1100 (bumps lastMessageAt — must NOT be
|
|
116
|
+
// mistaken for completion).
|
|
117
|
+
insertMessage(conv.id, "user", 1100);
|
|
118
|
+
// Agent's first assistant turn at t=2000.
|
|
119
|
+
insertMessage(conv.id, "assistant", 2000);
|
|
120
|
+
// Agent's final assistant turn at t=2500.
|
|
121
|
+
insertMessage(conv.id, "assistant", 2500);
|
|
122
|
+
|
|
123
|
+
const handler = findHandler("listConsolidationRuns");
|
|
124
|
+
const result = (await handler({})) as ListRunsResponse;
|
|
125
|
+
|
|
126
|
+
expect(result.runs).toHaveLength(1);
|
|
127
|
+
const run = result.runs[0]!;
|
|
128
|
+
expect(run.id).toBe(conv.id);
|
|
129
|
+
expect(run.conversationId).toBe(conv.id);
|
|
130
|
+
expect(run.status).toBe("ok");
|
|
131
|
+
expect(run.scheduledFor).toBe(1000);
|
|
132
|
+
expect(run.startedAt).toBe(1000);
|
|
133
|
+
// finishedAt = createdAt of LATEST assistant message (2500), NOT
|
|
134
|
+
// the conversation's lastMessageAt (which sqlite triggers may or may
|
|
135
|
+
// not have updated here — irrelevant to this endpoint).
|
|
136
|
+
expect(run.finishedAt).toBe(2500);
|
|
137
|
+
expect(run.durationMs).toBe(1500);
|
|
138
|
+
expect(run.createdAt).toBe(1000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("synthesizes status='running' when conversation has no assistant message", async () => {
|
|
142
|
+
createConversation({ title: "c1", source: "memory_v2_consolidation" });
|
|
143
|
+
|
|
144
|
+
const handler = findHandler("listConsolidationRuns");
|
|
145
|
+
const result = (await handler({})) as ListRunsResponse;
|
|
146
|
+
|
|
147
|
+
expect(result.runs).toHaveLength(1);
|
|
148
|
+
const run = result.runs[0]!;
|
|
149
|
+
expect(run.status).toBe("running");
|
|
150
|
+
expect(run.finishedAt).toBeNull();
|
|
151
|
+
expect(run.durationMs).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("status stays 'running' when only the kickoff user prompt exists (Codex bug regression guard)", async () => {
|
|
155
|
+
// Regression guard for the original `status from lastMessageAt`
|
|
156
|
+
// heuristic. `processMessage` persists the background kickoff prompt as
|
|
157
|
+
// a user message BEFORE the agent runs, which bumps
|
|
158
|
+
// `conversation.lastMessageAt`. A run that timed out / threw before
|
|
159
|
+
// emitting any assistant turn must still report status='running' (or
|
|
160
|
+
// an explicit failure status once one exists) — never 'ok'.
|
|
161
|
+
const conv = createConversation({
|
|
162
|
+
title: "c1",
|
|
163
|
+
source: "memory_v2_consolidation",
|
|
164
|
+
});
|
|
165
|
+
rawRun(
|
|
166
|
+
"UPDATE conversations SET created_at = ?, last_message_at = ? WHERE id = ?",
|
|
167
|
+
1000,
|
|
168
|
+
1100,
|
|
169
|
+
conv.id,
|
|
170
|
+
);
|
|
171
|
+
insertMessage(conv.id, "user", 1100);
|
|
172
|
+
|
|
173
|
+
const handler = findHandler("listConsolidationRuns");
|
|
174
|
+
const result = (await handler({})) as ListRunsResponse;
|
|
175
|
+
|
|
176
|
+
expect(result.runs).toHaveLength(1);
|
|
177
|
+
const run = result.runs[0]!;
|
|
178
|
+
expect(run.status).toBe("running");
|
|
179
|
+
expect(run.finishedAt).toBeNull();
|
|
180
|
+
expect(run.durationMs).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("skipReason and error are always null (not derivable from conversation row)", async () => {
|
|
184
|
+
const conv = createConversation({
|
|
185
|
+
title: "c1",
|
|
186
|
+
source: "memory_v2_consolidation",
|
|
187
|
+
});
|
|
188
|
+
insertMessage(conv.id, "assistant", 2000);
|
|
189
|
+
|
|
190
|
+
const handler = findHandler("listConsolidationRuns");
|
|
191
|
+
const result = (await handler({})) as ListRunsResponse;
|
|
192
|
+
|
|
193
|
+
expect(result.runs[0]!.skipReason).toBeNull();
|
|
194
|
+
expect(result.runs[0]!.error).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("orders runs by createdAt descending", async () => {
|
|
198
|
+
const a = createConversation({
|
|
199
|
+
title: "a",
|
|
200
|
+
source: "memory_v2_consolidation",
|
|
201
|
+
});
|
|
202
|
+
const b = createConversation({
|
|
203
|
+
title: "b",
|
|
204
|
+
source: "memory_v2_consolidation",
|
|
205
|
+
});
|
|
206
|
+
const c = createConversation({
|
|
207
|
+
title: "c",
|
|
208
|
+
source: "memory_v2_consolidation",
|
|
209
|
+
});
|
|
210
|
+
rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 1000, a.id);
|
|
211
|
+
rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 3000, b.id);
|
|
212
|
+
rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 2000, c.id);
|
|
213
|
+
|
|
214
|
+
const handler = findHandler("listConsolidationRuns");
|
|
215
|
+
const result = (await handler({})) as ListRunsResponse;
|
|
216
|
+
|
|
217
|
+
expect(result.runs.map((r) => r.id)).toEqual([b.id, c.id, a.id]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("limit defaults to 20, clamps to [1, 100], and falls back on non-numeric input", async () => {
|
|
221
|
+
for (let i = 0; i < 5; i++) {
|
|
222
|
+
createConversation({
|
|
223
|
+
title: `c${i}`,
|
|
224
|
+
source: "memory_v2_consolidation",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const handler = findHandler("listConsolidationRuns");
|
|
229
|
+
|
|
230
|
+
// Default — all 5 returned (under the 20 default).
|
|
231
|
+
const def = (await handler({})) as ListRunsResponse;
|
|
232
|
+
expect(def.runs).toHaveLength(5);
|
|
233
|
+
|
|
234
|
+
// Explicit limit honored.
|
|
235
|
+
const lim2 = (await handler({
|
|
236
|
+
queryParams: { limit: "2" },
|
|
237
|
+
})) as ListRunsResponse;
|
|
238
|
+
expect(lim2.runs).toHaveLength(2);
|
|
239
|
+
|
|
240
|
+
// Negative clamps to 1.
|
|
241
|
+
const neg = (await handler({
|
|
242
|
+
queryParams: { limit: "-5" },
|
|
243
|
+
})) as ListRunsResponse;
|
|
244
|
+
expect(neg.runs).toHaveLength(1);
|
|
245
|
+
|
|
246
|
+
// Zero clamps to 1.
|
|
247
|
+
const zero = (await handler({
|
|
248
|
+
queryParams: { limit: "0" },
|
|
249
|
+
})) as ListRunsResponse;
|
|
250
|
+
expect(zero.runs).toHaveLength(1);
|
|
251
|
+
|
|
252
|
+
// Non-numeric falls back to the default (20 → all 5 here).
|
|
253
|
+
const bad = (await handler({
|
|
254
|
+
queryParams: { limit: "garbage" },
|
|
255
|
+
})) as ListRunsResponse;
|
|
256
|
+
expect(bad.runs).toHaveLength(5);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -71,6 +71,10 @@ import {
|
|
|
71
71
|
memoryV2ActivationLogs,
|
|
72
72
|
messages,
|
|
73
73
|
} from "../../../memory/schema.js";
|
|
74
|
+
import {
|
|
75
|
+
createConnection,
|
|
76
|
+
getConnection,
|
|
77
|
+
} from "../../../providers/inference/connections.js";
|
|
74
78
|
import { ROUTES } from "../conversation-query-routes.js";
|
|
75
79
|
|
|
76
80
|
// Local subset: this test only exercises a single concept row.
|
|
@@ -427,7 +431,7 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
427
431
|
expect(savedProfile.provider_connection).toBe("personal-openai");
|
|
428
432
|
});
|
|
429
433
|
|
|
430
|
-
test("
|
|
434
|
+
test("auto-derives provider_connection when omitted from body (Any active)", async () => {
|
|
431
435
|
// Seed an existing binding so the test starts from a non-empty state.
|
|
432
436
|
(
|
|
433
437
|
rawConfigFixture.llm as {
|
|
@@ -441,8 +445,37 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
441
445
|
provider: "openai",
|
|
442
446
|
model: "gpt-5.5",
|
|
443
447
|
// provider_connection deliberately omitted — the UI cleared the
|
|
444
|
-
// picker back to "Any active"
|
|
445
|
-
//
|
|
448
|
+
// picker back to "Any active". The route auto-derives an active
|
|
449
|
+
// connection for the provider to prevent stale inheritance during
|
|
450
|
+
// config deep-merge.
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(result).toEqual({ ok: true });
|
|
455
|
+
const savedProfile = (
|
|
456
|
+
savedRawConfig?.llm as {
|
|
457
|
+
profiles: Record<string, Record<string, unknown>>;
|
|
458
|
+
}
|
|
459
|
+
).profiles.custom;
|
|
460
|
+
|
|
461
|
+
// The canonical "openai-managed" connection exists in the test DB;
|
|
462
|
+
// the route auto-derives it when the UI omits provider_connection.
|
|
463
|
+
expect(savedProfile.provider_connection).toBe("openai-managed");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("auto-derives provider_connection for BYOK provider (Any active)", async () => {
|
|
467
|
+
// Seed a fireworks connection in the DB.
|
|
468
|
+
createConnection(getDb(), {
|
|
469
|
+
name: "fireworks",
|
|
470
|
+
provider: "fireworks",
|
|
471
|
+
auth: { type: "api_key", credential: "fireworks:api_key" },
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const result = await replaceProfileRoute.handler({
|
|
475
|
+
pathParams: { name: "custom" },
|
|
476
|
+
body: {
|
|
477
|
+
provider: "fireworks",
|
|
478
|
+
model: "accounts/fireworks/models/llama-v3p1-8b-instruct",
|
|
446
479
|
},
|
|
447
480
|
});
|
|
448
481
|
|
|
@@ -453,7 +486,36 @@ describe("PUT /v1/config/llm/profiles/:name", () => {
|
|
|
453
486
|
}
|
|
454
487
|
).profiles.custom;
|
|
455
488
|
|
|
456
|
-
expect(savedProfile.
|
|
489
|
+
expect(savedProfile.provider).toBe("fireworks");
|
|
490
|
+
expect(savedProfile.provider_connection).toBe("fireworks");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("auto-creates provider_connection when no connection exists for provider", async () => {
|
|
494
|
+
const result = await replaceProfileRoute.handler({
|
|
495
|
+
pathParams: { name: "custom" },
|
|
496
|
+
body: {
|
|
497
|
+
provider: "openrouter",
|
|
498
|
+
model: "anthropic/claude-sonnet-4-6",
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(result).toEqual({ ok: true });
|
|
503
|
+
const savedProfile = (
|
|
504
|
+
savedRawConfig?.llm as {
|
|
505
|
+
profiles: Record<string, Record<string, unknown>>;
|
|
506
|
+
}
|
|
507
|
+
).profiles.custom;
|
|
508
|
+
|
|
509
|
+
expect(savedProfile.provider).toBe("openrouter");
|
|
510
|
+
expect(savedProfile.provider_connection).toBe("openrouter-personal");
|
|
511
|
+
|
|
512
|
+
const conn = getConnection(getDb(), "openrouter-personal");
|
|
513
|
+
expect(conn).not.toBeNull();
|
|
514
|
+
expect(conn!.provider).toBe("openrouter");
|
|
515
|
+
expect(conn!.auth).toEqual({
|
|
516
|
+
type: "api_key",
|
|
517
|
+
credential: "credential/openrouter/api_key",
|
|
518
|
+
});
|
|
457
519
|
});
|
|
458
520
|
|
|
459
521
|
describe("managed profile guard", () => {
|