@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/ARCHITECTURE.md +26 -3
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +38 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +161 -17
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +54 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +9 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +14 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/process-message.ts +5 -0
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/tool-approval-handler.ts +9 -1
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { utimesSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, utimesSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
5
|
+
import { waitFor } from "../../../__tests__/helpers/wait-for.js";
|
|
5
6
|
import {
|
|
6
7
|
sampleConcepts as sharedSampleConcepts,
|
|
7
8
|
sampleConfig,
|
|
@@ -50,11 +51,14 @@ mock.module("../../../persistence/embeddings/embedding-backend.js", () => ({
|
|
|
50
51
|
},
|
|
51
52
|
}));
|
|
52
53
|
|
|
54
|
+
import type { AssistantEventEnvelope } from "../../../api/index.js";
|
|
53
55
|
import { BACKUP_PROFILE_KEYS } from "../../../config/default-profile-names.js";
|
|
54
56
|
import {
|
|
55
57
|
getConfig,
|
|
58
|
+
invalidateConfigCache,
|
|
56
59
|
loadConfig,
|
|
57
60
|
loadRawConfig,
|
|
61
|
+
setNestedValue,
|
|
58
62
|
} from "../../../config/loader.js";
|
|
59
63
|
import { AssistantConfigSchema } from "../../../config/schema.js";
|
|
60
64
|
import { LLMConfigBase } from "../../../config/schemas/llm.js";
|
|
@@ -82,7 +86,8 @@ import {
|
|
|
82
86
|
createConnection,
|
|
83
87
|
getConnection,
|
|
84
88
|
} from "../../../providers/inference/connections.js";
|
|
85
|
-
import {
|
|
89
|
+
import { assistantEventHub } from "../../assistant-event-hub.js";
|
|
90
|
+
import { commitConfigWrite, ROUTES } from "../conversation-query-routes.js";
|
|
86
91
|
|
|
87
92
|
// Local subset: this test only exercises a single concept row.
|
|
88
93
|
const sampleConcepts: MemoryV2ConceptRowRecord[] = sharedSampleConcepts.slice(
|
|
@@ -2047,3 +2052,371 @@ describe("config writes to a per-agent acp entry", () => {
|
|
|
2047
2052
|
expect(loadConfig().acp.agents.claude?.model).toBe("sonnet");
|
|
2048
2053
|
});
|
|
2049
2054
|
});
|
|
2055
|
+
|
|
2056
|
+
describe("persisted chat settings", () => {
|
|
2057
|
+
const getRoute = ROUTES.find((r) => r.operationId === "config_get")!;
|
|
2058
|
+
const patchRoute = ROUTES.find((r) => r.operationId === "config_patch")!;
|
|
2059
|
+
const setRoute = ROUTES.find((r) => r.operationId === "config_set")!;
|
|
2060
|
+
|
|
2061
|
+
beforeEach(() => {
|
|
2062
|
+
initializeProvidersCalls = 0;
|
|
2063
|
+
clearEmbeddingBackendCacheCalls = 0;
|
|
2064
|
+
rawConfigFixture = {
|
|
2065
|
+
conversations: {
|
|
2066
|
+
skipAutoRetitling: true,
|
|
2067
|
+
autoArchive: { enabled: true, afterDays: 14, futureOption: "keep" },
|
|
2068
|
+
},
|
|
2069
|
+
notifications: { newMessageEnabled: false, futureOption: "keep" },
|
|
2070
|
+
maxStepsPerSession: 75,
|
|
2071
|
+
};
|
|
2072
|
+
seedRawConfig();
|
|
2073
|
+
invalidateConfigCache();
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
const writers = ["PATCH", "SET", "canonical"] as const;
|
|
2077
|
+
async function writeSetting(
|
|
2078
|
+
writer: (typeof writers)[number],
|
|
2079
|
+
path: string,
|
|
2080
|
+
value: unknown,
|
|
2081
|
+
): Promise<unknown> {
|
|
2082
|
+
if (writer === "SET") {
|
|
2083
|
+
return setRoute.handler({ body: { path, value } });
|
|
2084
|
+
}
|
|
2085
|
+
const raw = writer === "canonical" ? loadRawConfig() : {};
|
|
2086
|
+
setNestedValue(raw, path, value);
|
|
2087
|
+
return writer === "PATCH"
|
|
2088
|
+
? patchRoute.handler({ body: raw })
|
|
2089
|
+
: commitConfigWrite(raw, "test");
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
for (const writer of writers) {
|
|
2093
|
+
test.each([
|
|
2094
|
+
{ conversations: false },
|
|
2095
|
+
{ conversations: [] },
|
|
2096
|
+
{ conversations: null },
|
|
2097
|
+
{ conversations: { autoArchive: "legacy" } },
|
|
2098
|
+
{ conversations: { autoArchive: [] } },
|
|
2099
|
+
{ conversations: { autoArchive: null } },
|
|
2100
|
+
{ conversations: { autoArchive: { enabled: "true", afterDays: 2 } } },
|
|
2101
|
+
{ conversations: { autoArchive: { enabled: null, afterDays: null } } },
|
|
2102
|
+
{ notifications: "legacy" },
|
|
2103
|
+
{ notifications: [] },
|
|
2104
|
+
{ notifications: null },
|
|
2105
|
+
{ notifications: { newMessageEnabled: "false" } },
|
|
2106
|
+
{ notifications: { newMessageEnabled: null } },
|
|
2107
|
+
])(
|
|
2108
|
+
`${writer} unrelated writes preserve untouched legacy values: %j`,
|
|
2109
|
+
async (legacy) => {
|
|
2110
|
+
rawConfigFixture = { ...legacy, maxStepsPerSession: 75 };
|
|
2111
|
+
seedRawConfig();
|
|
2112
|
+
invalidateConfigCache();
|
|
2113
|
+
|
|
2114
|
+
await writeSetting(writer, "maxStepsPerSession", 100);
|
|
2115
|
+
|
|
2116
|
+
expect(loadRawConfig()).toEqual({
|
|
2117
|
+
...rawConfigFixture,
|
|
2118
|
+
maxStepsPerSession: 100,
|
|
2119
|
+
});
|
|
2120
|
+
expect(initializeProvidersCalls).toBe(1);
|
|
2121
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(1);
|
|
2122
|
+
},
|
|
2123
|
+
);
|
|
2124
|
+
|
|
2125
|
+
test(`${writer} can repair one leaf without changing other legacy values`, async () => {
|
|
2126
|
+
rawConfigFixture = {
|
|
2127
|
+
conversations: { autoArchive: { enabled: "true", afterDays: 2 } },
|
|
2128
|
+
notifications: null,
|
|
2129
|
+
};
|
|
2130
|
+
seedRawConfig();
|
|
2131
|
+
invalidateConfigCache();
|
|
2132
|
+
|
|
2133
|
+
await writeSetting(writer, "conversations.autoArchive.enabled", true);
|
|
2134
|
+
|
|
2135
|
+
expect(loadRawConfig()).toEqual({
|
|
2136
|
+
conversations: { autoArchive: { enabled: true, afterDays: 2 } },
|
|
2137
|
+
notifications: null,
|
|
2138
|
+
});
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
test(`${writer} rejects an invalid leaf change beside an unchanged invalid sibling`, async () => {
|
|
2142
|
+
rawConfigFixture = {
|
|
2143
|
+
conversations: { autoArchive: { enabled: "true", afterDays: 2 } },
|
|
2144
|
+
};
|
|
2145
|
+
seedRawConfig();
|
|
2146
|
+
invalidateConfigCache();
|
|
2147
|
+
const configPath = join(process.env.VELLUM_WORKSPACE_DIR!, "config.json");
|
|
2148
|
+
const originalBytes = readFileSync(configPath, "utf-8");
|
|
2149
|
+
|
|
2150
|
+
await expect(
|
|
2151
|
+
writeSetting(writer, "conversations.autoArchive.afterDays", 3),
|
|
2152
|
+
).rejects.toMatchObject({ statusCode: 400 });
|
|
2153
|
+
|
|
2154
|
+
expect(readFileSync(configPath, "utf-8")).toBe(originalBytes);
|
|
2155
|
+
expect(initializeProvidersCalls).toBe(0);
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
test.each([
|
|
2159
|
+
["conversations", []],
|
|
2160
|
+
["conversations.autoArchive", { enabled: "true" }],
|
|
2161
|
+
["conversations.autoArchive", { afterDays: 2 }],
|
|
2162
|
+
["notifications.newMessageEnabled", "true"],
|
|
2163
|
+
["notifications.newMessageEnabled", 1],
|
|
2164
|
+
])(
|
|
2165
|
+
`${writer} rejects changed invalid settings at %s over legacy config`,
|
|
2166
|
+
async (path, value) => {
|
|
2167
|
+
rawConfigFixture = {
|
|
2168
|
+
conversations: false,
|
|
2169
|
+
notifications: { newMessageEnabled: "false" },
|
|
2170
|
+
maxStepsPerSession: 75,
|
|
2171
|
+
};
|
|
2172
|
+
seedRawConfig();
|
|
2173
|
+
invalidateConfigCache();
|
|
2174
|
+
const configPath = join(
|
|
2175
|
+
process.env.VELLUM_WORKSPACE_DIR!,
|
|
2176
|
+
"config.json",
|
|
2177
|
+
);
|
|
2178
|
+
const originalBytes = readFileSync(configPath, "utf-8");
|
|
2179
|
+
|
|
2180
|
+
await expect(
|
|
2181
|
+
writeSetting(writer, path as string, value),
|
|
2182
|
+
).rejects.toMatchObject({
|
|
2183
|
+
statusCode: 400,
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
expect(readFileSync(configPath, "utf-8")).toBe(originalBytes);
|
|
2187
|
+
expect(initializeProvidersCalls).toBe(0);
|
|
2188
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(0);
|
|
2189
|
+
},
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
test("PATCH resets only the explicitly nulled legacy leaf", async () => {
|
|
2194
|
+
rawConfigFixture = {
|
|
2195
|
+
conversations: { autoArchive: { enabled: null, afterDays: null } },
|
|
2196
|
+
notifications: { newMessageEnabled: null },
|
|
2197
|
+
};
|
|
2198
|
+
seedRawConfig();
|
|
2199
|
+
invalidateConfigCache();
|
|
2200
|
+
|
|
2201
|
+
await patchRoute.handler({
|
|
2202
|
+
body: { conversations: { autoArchive: { enabled: null } } },
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
expect(loadRawConfig()).toEqual({
|
|
2206
|
+
conversations: { autoArchive: { afterDays: null } },
|
|
2207
|
+
notifications: { newMessageEnabled: null },
|
|
2208
|
+
});
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
test("sparse GET exposes effective defaults without materializing unrelated fields", async () => {
|
|
2212
|
+
rawConfigFixture = {};
|
|
2213
|
+
seedRawConfig();
|
|
2214
|
+
invalidateConfigCache();
|
|
2215
|
+
const result = await getRoute.handler({});
|
|
2216
|
+
expect(result).toMatchObject({
|
|
2217
|
+
conversations: { autoArchive: { enabled: false, afterDays: 7 } },
|
|
2218
|
+
notifications: { newMessageEnabled: true },
|
|
2219
|
+
});
|
|
2220
|
+
expect(result).not.toHaveProperty("conversations.skipAutoRetitling");
|
|
2221
|
+
expect(loadRawConfig()).not.toHaveProperty("conversations");
|
|
2222
|
+
expect(loadRawConfig()).not.toHaveProperty("notifications");
|
|
2223
|
+
});
|
|
2224
|
+
|
|
2225
|
+
test("partial PATCH persists, reloads, and publishes config invalidation", async () => {
|
|
2226
|
+
const received: AssistantEventEnvelope[] = [];
|
|
2227
|
+
const subscription = assistantEventHub.subscribe({
|
|
2228
|
+
type: "process",
|
|
2229
|
+
callback: (event) => {
|
|
2230
|
+
received.push(event);
|
|
2231
|
+
},
|
|
2232
|
+
});
|
|
2233
|
+
try {
|
|
2234
|
+
const result = await patchRoute.handler({
|
|
2235
|
+
body: { conversations: { autoArchive: { afterDays: 30 } } },
|
|
2236
|
+
});
|
|
2237
|
+
expect(result).toMatchObject({
|
|
2238
|
+
conversations: {
|
|
2239
|
+
skipAutoRetitling: true,
|
|
2240
|
+
autoArchive: { enabled: true, afterDays: 30, futureOption: "keep" },
|
|
2241
|
+
},
|
|
2242
|
+
notifications: { newMessageEnabled: false, futureOption: "keep" },
|
|
2243
|
+
maxStepsPerSession: 75,
|
|
2244
|
+
});
|
|
2245
|
+
expect(loadRawConfig()).toEqual({
|
|
2246
|
+
...rawConfigFixture,
|
|
2247
|
+
conversations: {
|
|
2248
|
+
skipAutoRetitling: true,
|
|
2249
|
+
autoArchive: { enabled: true, afterDays: 30, futureOption: "keep" },
|
|
2250
|
+
},
|
|
2251
|
+
});
|
|
2252
|
+
invalidateConfigCache();
|
|
2253
|
+
expect(loadConfig().conversations.autoArchive).toEqual({
|
|
2254
|
+
enabled: true,
|
|
2255
|
+
afterDays: 30,
|
|
2256
|
+
});
|
|
2257
|
+
expect(loadConfig().notifications.newMessageEnabled).toBe(false);
|
|
2258
|
+
expect(await getRoute.handler({})).toEqual(result);
|
|
2259
|
+
expect(initializeProvidersCalls).toBe(1);
|
|
2260
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(1);
|
|
2261
|
+
await waitFor(() =>
|
|
2262
|
+
received.some((event) => event.message.type === "sync_changed"),
|
|
2263
|
+
);
|
|
2264
|
+
expect(received.map((event) => event.message)).toContainEqual({
|
|
2265
|
+
type: "sync_changed",
|
|
2266
|
+
tags: ["assistant:self:config"],
|
|
2267
|
+
});
|
|
2268
|
+
} finally {
|
|
2269
|
+
subscription.dispose();
|
|
2270
|
+
}
|
|
2271
|
+
});
|
|
2272
|
+
|
|
2273
|
+
test("PATCH null leaves restore defaults and preserve sibling values", async () => {
|
|
2274
|
+
const result = await patchRoute.handler({
|
|
2275
|
+
body: {
|
|
2276
|
+
conversations: { autoArchive: { enabled: null, afterDays: null } },
|
|
2277
|
+
notifications: { newMessageEnabled: null },
|
|
2278
|
+
},
|
|
2279
|
+
});
|
|
2280
|
+
expect(result).toMatchObject({
|
|
2281
|
+
conversations: {
|
|
2282
|
+
skipAutoRetitling: true,
|
|
2283
|
+
autoArchive: { enabled: false, afterDays: 7, futureOption: "keep" },
|
|
2284
|
+
},
|
|
2285
|
+
notifications: { newMessageEnabled: true, futureOption: "keep" },
|
|
2286
|
+
});
|
|
2287
|
+
expect(loadRawConfig()).toMatchObject({
|
|
2288
|
+
conversations: { autoArchive: { futureOption: "keep" } },
|
|
2289
|
+
notifications: { futureOption: "keep" },
|
|
2290
|
+
});
|
|
2291
|
+
expect(loadRawConfig()).not.toHaveProperty(
|
|
2292
|
+
"conversations.autoArchive.enabled",
|
|
2293
|
+
);
|
|
2294
|
+
expect(loadRawConfig()).not.toHaveProperty(
|
|
2295
|
+
"notifications.newMessageEnabled",
|
|
2296
|
+
);
|
|
2297
|
+
});
|
|
2298
|
+
|
|
2299
|
+
test.each([
|
|
2300
|
+
{ conversations: { autoArchive: null }, notifications: null },
|
|
2301
|
+
{ conversations: null, notifications: null },
|
|
2302
|
+
])("PATCH null subtrees restore defaults: %j", async (body) => {
|
|
2303
|
+
const result = await patchRoute.handler({ body });
|
|
2304
|
+
expect(result).toMatchObject({
|
|
2305
|
+
conversations: { autoArchive: { enabled: false, afterDays: 7 } },
|
|
2306
|
+
notifications: { newMessageEnabled: true },
|
|
2307
|
+
});
|
|
2308
|
+
expect(loadRawConfig()).not.toHaveProperty("conversations.autoArchive");
|
|
2309
|
+
expect(loadRawConfig()).not.toHaveProperty("notifications");
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
test.each([
|
|
2313
|
+
{ conversations: false },
|
|
2314
|
+
{ conversations: { autoArchive: [] } },
|
|
2315
|
+
{ conversations: { autoArchive: { enabled: "true" } } },
|
|
2316
|
+
{ conversations: { autoArchive: { afterDays: 5 } } },
|
|
2317
|
+
{ notifications: "off" },
|
|
2318
|
+
{ notifications: { newMessageEnabled: 0 } },
|
|
2319
|
+
])(
|
|
2320
|
+
"invalid PATCH leaves saved config and runtime unchanged: %j",
|
|
2321
|
+
async (body) => {
|
|
2322
|
+
const original = loadRawConfig();
|
|
2323
|
+
await expect(patchRoute.handler({ body })).rejects.toMatchObject({
|
|
2324
|
+
statusCode: 400,
|
|
2325
|
+
});
|
|
2326
|
+
expect(loadRawConfig()).toEqual(original);
|
|
2327
|
+
expect(initializeProvidersCalls).toBe(0);
|
|
2328
|
+
expect(clearEmbeddingBackendCacheCalls).toBe(0);
|
|
2329
|
+
},
|
|
2330
|
+
);
|
|
2331
|
+
|
|
2332
|
+
test.each([false, "legacy", [], null])(
|
|
2333
|
+
"PATCH null resets a malformed stored autoArchive subtree: %j",
|
|
2334
|
+
async (autoArchive) => {
|
|
2335
|
+
rawConfigFixture = {
|
|
2336
|
+
conversations: { skipAutoRetitling: true, autoArchive },
|
|
2337
|
+
notifications: { newMessageEnabled: false },
|
|
2338
|
+
maxStepsPerSession: 75,
|
|
2339
|
+
};
|
|
2340
|
+
seedRawConfig();
|
|
2341
|
+
invalidateConfigCache();
|
|
2342
|
+
|
|
2343
|
+
const result = await patchRoute.handler({
|
|
2344
|
+
body: { conversations: { autoArchive: null } },
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2347
|
+
expect(result).toMatchObject({
|
|
2348
|
+
conversations: {
|
|
2349
|
+
skipAutoRetitling: true,
|
|
2350
|
+
autoArchive: { enabled: false, afterDays: 7 },
|
|
2351
|
+
},
|
|
2352
|
+
notifications: { newMessageEnabled: false },
|
|
2353
|
+
});
|
|
2354
|
+
expect(loadRawConfig()).toEqual({
|
|
2355
|
+
conversations: { skipAutoRetitling: true },
|
|
2356
|
+
notifications: { newMessageEnabled: false },
|
|
2357
|
+
maxStepsPerSession: 75,
|
|
2358
|
+
});
|
|
2359
|
+
},
|
|
2360
|
+
);
|
|
2361
|
+
|
|
2362
|
+
test.each([false, "legacy", [], null])(
|
|
2363
|
+
"PATCH null resets malformed stored settings sections: %j",
|
|
2364
|
+
async (value) => {
|
|
2365
|
+
rawConfigFixture = {
|
|
2366
|
+
conversations: value,
|
|
2367
|
+
notifications: value,
|
|
2368
|
+
maxStepsPerSession: 75,
|
|
2369
|
+
};
|
|
2370
|
+
seedRawConfig();
|
|
2371
|
+
invalidateConfigCache();
|
|
2372
|
+
|
|
2373
|
+
const result = await patchRoute.handler({
|
|
2374
|
+
body: { conversations: null, notifications: null },
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
expect(result).toMatchObject({
|
|
2378
|
+
conversations: { autoArchive: { enabled: false, afterDays: 7 } },
|
|
2379
|
+
notifications: { newMessageEnabled: true },
|
|
2380
|
+
});
|
|
2381
|
+
expect(loadRawConfig()).toEqual({ maxStepsPerSession: 75 });
|
|
2382
|
+
},
|
|
2383
|
+
);
|
|
2384
|
+
|
|
2385
|
+
test("SET persists valid preferences through the same writer", async () => {
|
|
2386
|
+
await setRoute.handler({
|
|
2387
|
+
body: { path: "notifications.newMessageEnabled", value: true },
|
|
2388
|
+
});
|
|
2389
|
+
expect(loadConfig().notifications.newMessageEnabled).toBe(true);
|
|
2390
|
+
expect(loadRawConfig()).toHaveProperty(
|
|
2391
|
+
"notifications.futureOption",
|
|
2392
|
+
"keep",
|
|
2393
|
+
);
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
test.each([
|
|
2397
|
+
["conversations.autoArchive.afterDays", 2],
|
|
2398
|
+
["conversations.autoArchive.enabled", null],
|
|
2399
|
+
["conversations.autoArchive", []],
|
|
2400
|
+
["notifications.newMessageEnabled", "false"],
|
|
2401
|
+
["notifications", null],
|
|
2402
|
+
])("invalid SET %s is rejected before saving", async (path, value) => {
|
|
2403
|
+
const original = loadRawConfig();
|
|
2404
|
+
await expect(
|
|
2405
|
+
setRoute.handler({ body: { path, value } }),
|
|
2406
|
+
).rejects.toMatchObject({ statusCode: 400 });
|
|
2407
|
+
expect(loadRawConfig()).toEqual(original);
|
|
2408
|
+
expect(initializeProvidersCalls).toBe(0);
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
test("canonical config writes reject invalid settings", async () => {
|
|
2412
|
+
const original = loadRawConfig();
|
|
2413
|
+
await expect(
|
|
2414
|
+
commitConfigWrite(
|
|
2415
|
+
{ ...original, notifications: { newMessageEnabled: null } },
|
|
2416
|
+
"test",
|
|
2417
|
+
),
|
|
2418
|
+
).rejects.toMatchObject({ statusCode: 400 });
|
|
2419
|
+
expect(loadRawConfig()).toEqual(original);
|
|
2420
|
+
expect(initializeProvidersCalls).toBe(0);
|
|
2421
|
+
});
|
|
2422
|
+
});
|
|
@@ -252,7 +252,7 @@ describe("inference_send profile usability", () => {
|
|
|
252
252
|
const err = await rejection({ ...hermes, status: "disabled" });
|
|
253
253
|
expect(err).toBeInstanceOf(BadRequestError);
|
|
254
254
|
expect(err.message).toContain("disabled");
|
|
255
|
-
expect(err.message).toContain("deepseek-
|
|
255
|
+
expect(err.message).toContain("deepseek-v4p1-flash");
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
test("rejects a profile that lost its provider", async () => {
|
|
@@ -106,6 +106,18 @@ describe("path traversal", () => {
|
|
|
106
106
|
);
|
|
107
107
|
expect(res.status).toBe(400);
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
test("rejects backslashes in the route path", async () => {
|
|
111
|
+
// Some URL normalizers treat a backslash as a separator, so a
|
|
112
|
+
// backslash-carrying routePath is refused outright regardless of dots.
|
|
113
|
+
const dispatcher = makeDispatcher();
|
|
114
|
+
for (const routePath of ["..\\config", "foo\\bar", "x\\..\\config"]) {
|
|
115
|
+
const res = await dispatcher.dispatch(routePath, makeRequest("GET"));
|
|
116
|
+
expect(res.status).toBe(400);
|
|
117
|
+
const body = await readErrorBody(res);
|
|
118
|
+
expect(body.error.message).toContain("Path traversal");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
109
121
|
});
|
|
110
122
|
|
|
111
123
|
// ---------------------------------------------------------------------------
|
|
@@ -393,8 +393,8 @@ export async function handleListContacts(queryParams: Record<string, string>) {
|
|
|
393
393
|
export async function handleGetContact(contactId: string) {
|
|
394
394
|
try {
|
|
395
395
|
const result = await ipcCallPersistent("contacts_get_rich", { contactId });
|
|
396
|
-
// The gateway returns null
|
|
397
|
-
if (
|
|
396
|
+
// The gateway returns null on a clean not-found.
|
|
397
|
+
if (result == null) {
|
|
398
398
|
throw new NotFoundError(`Contact "${contactId}" not found`);
|
|
399
399
|
}
|
|
400
400
|
const { contact, assistantMetadata } =
|
|
@@ -150,6 +150,13 @@ const listConversationsResponseSchema = z.object({
|
|
|
150
150
|
nextOffset: z.number(),
|
|
151
151
|
hasMore: z.boolean(),
|
|
152
152
|
groups: z.array(groupSummarySchema).optional(),
|
|
153
|
+
/**
|
|
154
|
+
* The key the rows are ordered on: last activity, the later of a row's
|
|
155
|
+
* last message and the time it was marked done. Stated so a client that
|
|
156
|
+
* pages a list including done rows can tell this order from message
|
|
157
|
+
* recency, which an assistant that omits the field pages by.
|
|
158
|
+
*/
|
|
159
|
+
orderedBy: z.literal("lastActivity"),
|
|
153
160
|
});
|
|
154
161
|
|
|
155
162
|
const conversationDetailResponseSchema = z.object({
|
|
@@ -376,6 +383,7 @@ function handleListConversations({
|
|
|
376
383
|
),
|
|
377
384
|
nextOffset,
|
|
378
385
|
hasMore: nextOffset < totalCount,
|
|
386
|
+
orderedBy: "lastActivity",
|
|
379
387
|
};
|
|
380
388
|
|
|
381
389
|
// Include groups array on first page only
|
|
@@ -632,7 +640,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
632
640
|
type: "string",
|
|
633
641
|
required: false,
|
|
634
642
|
description:
|
|
635
|
-
'Filter by conversation type. Pass "background" to list background and scheduled conversations together (the back-compat umbrella), "scheduled" to list only scheduled conversations, or "all" for the union of the standard listing and that umbrella, so one paginated cursor spans every type. Combine "all" with archiveStatus=all for a whole-history read; rows are
|
|
643
|
+
'Filter by conversation type. Pass "background" to list background and scheduled conversations together (the back-compat umbrella), "scheduled" to list only scheduled conversations, or "all" for the union of the standard listing and that umbrella, so one paginated cursor spans every type. Combine "all" with archiveStatus=all for a whole-history read; rows are ordered by last activity (the later of the last message and the time the row was marked done, descending) like every other list read, and pinned rows are never appended to it.',
|
|
636
644
|
schema: {
|
|
637
645
|
type: "string",
|
|
638
646
|
enum: ["background", "scheduled", "all"],
|
|
@@ -662,7 +670,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
662
670
|
type: "string",
|
|
663
671
|
required: false,
|
|
664
672
|
description:
|
|
665
|
-
'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is
|
|
673
|
+
'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is ordered by last activity like every list read (the later of the last message and the time the row was marked done, descending) and never has pinned rows appended to it. Omit to span every group.',
|
|
666
674
|
},
|
|
667
675
|
{
|
|
668
676
|
name: "needsAttention",
|
|
@@ -30,6 +30,11 @@ import {
|
|
|
30
30
|
LLMRequestLogEntrySchema,
|
|
31
31
|
} from "../../api/responses/llm-request-log-entry.js";
|
|
32
32
|
import { scrubNulledAcpAgentLeaves } from "../../config/acp-agent-write.js";
|
|
33
|
+
import {
|
|
34
|
+
ChatSettingsPatchSchema,
|
|
35
|
+
scrubNulledChatSettings,
|
|
36
|
+
validateChatSettingsWrite,
|
|
37
|
+
} from "../../config/chat-settings.js";
|
|
33
38
|
import {
|
|
34
39
|
catalogEntryFor,
|
|
35
40
|
type InputModalities,
|
|
@@ -54,6 +59,7 @@ import {
|
|
|
54
59
|
} from "../../config/profile-materialization.js";
|
|
55
60
|
import { AssistantConfigSchema } from "../../config/schema.js";
|
|
56
61
|
import { getSchemaAtPath } from "../../config/schema-utils.js";
|
|
62
|
+
import { AutoArchiveConfigSchema } from "../../config/schemas/conversations.js";
|
|
57
63
|
import {
|
|
58
64
|
collectFallbackProfileIssues,
|
|
59
65
|
DefaultProviderSchema,
|
|
@@ -64,6 +70,7 @@ import {
|
|
|
64
70
|
unknownLlmProviderIssue,
|
|
65
71
|
} from "../../config/schemas/llm.js";
|
|
66
72
|
import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
|
|
73
|
+
import { NotificationsConfigSchema } from "../../config/schemas/notifications.js";
|
|
67
74
|
import { ServiceModeSchema } from "../../config/schemas/services.js";
|
|
68
75
|
import { isSidebarDoneEnabled } from "../../config/sidebar-done-gate.js";
|
|
69
76
|
import {
|
|
@@ -746,14 +753,19 @@ const MemoryWireConfigSchema = z
|
|
|
746
753
|
* Response schema for `GET /v1/config`.
|
|
747
754
|
*
|
|
748
755
|
* Describes the wire shape of the raw `settings.json` response after
|
|
749
|
-
* context-default filling and vision-flag enrichment.
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
* passthrough
|
|
756
|
+
* context-default filling and vision-flag enrichment. Chat settings include
|
|
757
|
+
* effective defaults. Fields remain optional for older assistants and sparse
|
|
758
|
+
* on-disk config. Additional top-level sections are preserved via
|
|
759
|
+
* passthrough. This schema types the fields that web/macOS clients consume
|
|
753
760
|
* without restricting the full config surface.
|
|
754
761
|
*/
|
|
755
762
|
const ConfigGetResponseSchema = z
|
|
756
763
|
.object({
|
|
764
|
+
conversations: z
|
|
765
|
+
.object({ autoArchive: AutoArchiveConfigSchema.passthrough() })
|
|
766
|
+
.passthrough()
|
|
767
|
+
.optional(),
|
|
768
|
+
notifications: NotificationsConfigSchema.passthrough().optional(),
|
|
757
769
|
llm: z
|
|
758
770
|
.object({
|
|
759
771
|
default: LLMConfigFragment.extend({
|
|
@@ -859,6 +871,7 @@ const CallSiteOverrideDraftSchema = nullablePartial(
|
|
|
859
871
|
*/
|
|
860
872
|
const ConfigPatchRequestSchema = z
|
|
861
873
|
.object({
|
|
874
|
+
...ChatSettingsPatchSchema.shape,
|
|
862
875
|
llm: z
|
|
863
876
|
.object({
|
|
864
877
|
default: nullablePartial(
|
|
@@ -932,6 +945,7 @@ function handleGetConfig() {
|
|
|
932
945
|
sanitizeMcpTransportHeadersForSettingsRead(config);
|
|
933
946
|
overlayEffectiveProfilesForWire(config);
|
|
934
947
|
enrichProfilesForWire(config);
|
|
948
|
+
overlayChatSettingsForWire(config);
|
|
935
949
|
return config;
|
|
936
950
|
} catch (err) {
|
|
937
951
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -939,6 +953,26 @@ function handleGetConfig() {
|
|
|
939
953
|
}
|
|
940
954
|
}
|
|
941
955
|
|
|
956
|
+
function overlayChatSettingsForWire(config: unknown): void {
|
|
957
|
+
const root = readPlainObject(config);
|
|
958
|
+
if (!root) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const effective = getConfig();
|
|
962
|
+
const conversations = readPlainObject(root.conversations);
|
|
963
|
+
root.conversations = {
|
|
964
|
+
...conversations,
|
|
965
|
+
autoArchive: {
|
|
966
|
+
...readPlainObject(conversations?.autoArchive),
|
|
967
|
+
...effective.conversations.autoArchive,
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
root.notifications = {
|
|
971
|
+
...readPlainObject(root.notifications),
|
|
972
|
+
newMessageEnabled: effective.notifications.newMessageEnabled,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
942
976
|
/**
|
|
943
977
|
* Replace `llm.profiles` in an outgoing config response with the effective
|
|
944
978
|
* profile view (code-catalog default bodies + workspace overlays). Default
|
|
@@ -1513,6 +1547,10 @@ export async function commitConfigWrite(
|
|
|
1513
1547
|
// false diffs. Runs before the watcher-suppress/save sequence so a
|
|
1514
1548
|
// rejection needs no suppress-flag or cache cleanup.
|
|
1515
1549
|
const preWrite = loadRawConfig();
|
|
1550
|
+
const chatSettings = validateChatSettingsWrite(preWrite, raw);
|
|
1551
|
+
if (!chatSettings.success) {
|
|
1552
|
+
throw new BadRequestError(chatSettings.error.message);
|
|
1553
|
+
}
|
|
1516
1554
|
completeChangedCustomProfiles(preWrite, raw);
|
|
1517
1555
|
assertInvariantProfilesPreserved(preWrite, raw);
|
|
1518
1556
|
assertRoutableIdentityEntries(preWrite, raw);
|
|
@@ -1626,6 +1664,10 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
|
|
|
1626
1664
|
) {
|
|
1627
1665
|
throw new BadRequestError("Body must be a non-empty JSON object");
|
|
1628
1666
|
}
|
|
1667
|
+
const chatSettings = ChatSettingsPatchSchema.safeParse(body);
|
|
1668
|
+
if (!chatSettings.success) {
|
|
1669
|
+
throw new BadRequestError(chatSettings.error.message);
|
|
1670
|
+
}
|
|
1629
1671
|
stripWireOnlyProfileKeys(body);
|
|
1630
1672
|
normalizeManagedProfileWrites(body);
|
|
1631
1673
|
rejectManagedProfileDeletion(body as Record<string, unknown>);
|
|
@@ -1641,16 +1683,12 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
|
|
|
1641
1683
|
deepMergeOverwrite(raw, patch);
|
|
1642
1684
|
scrubRemovedServiceModes(raw);
|
|
1643
1685
|
scrubNulledAcpAgentLeaves(raw);
|
|
1686
|
+
scrubNulledChatSettings(raw, patch);
|
|
1644
1687
|
seedSttProviderForSparseBlock(raw);
|
|
1645
1688
|
|
|
1646
1689
|
await commitConfigWrite(raw, "patch");
|
|
1647
1690
|
|
|
1648
|
-
|
|
1649
|
-
overlayWorkspaceMcpForConfigRead(merged);
|
|
1650
|
-
sanitizeMcpTransportHeadersForSettingsRead(merged);
|
|
1651
|
-
overlayEffectiveProfilesForWire(merged);
|
|
1652
|
-
enrichProfilesForWire(merged);
|
|
1653
|
-
return merged;
|
|
1691
|
+
return handleGetConfig();
|
|
1654
1692
|
}
|
|
1655
1693
|
|
|
1656
1694
|
/**
|
|
@@ -101,6 +101,7 @@ import {
|
|
|
101
101
|
writeOnboardingSidecar,
|
|
102
102
|
writeRelationshipState,
|
|
103
103
|
} from "../../home/relationship-state-writer.js";
|
|
104
|
+
import { localeFromAcceptLanguage } from "../../i18n/index.js";
|
|
104
105
|
import { ipcCall } from "../../ipc/gateway-client.js";
|
|
105
106
|
import { buildSlackMessageDeepLinks } from "../../messaging/providers/slack/deep-link.js";
|
|
106
107
|
import {
|
|
@@ -915,7 +916,9 @@ function parseRequestedModeSessionIds(raw: unknown): string[] {
|
|
|
915
916
|
|
|
916
917
|
export async function handleListMessages({
|
|
917
918
|
queryParams,
|
|
919
|
+
headers,
|
|
918
920
|
}: RouteHandlerArgs): Promise<Record<string, unknown>> {
|
|
921
|
+
const locale = localeFromAcceptLanguage(headers?.["accept-language"]);
|
|
919
922
|
const conversationId = queryParams?.conversationId;
|
|
920
923
|
const conversationKey = queryParams?.conversationKey;
|
|
921
924
|
|
|
@@ -1338,6 +1341,7 @@ export async function handleListMessages({
|
|
|
1338
1341
|
attachmentBlocks,
|
|
1339
1342
|
m.id ?? undefined,
|
|
1340
1343
|
m.rowMetadata,
|
|
1344
|
+
m.role === "assistant" ? locale : undefined,
|
|
1341
1345
|
);
|
|
1342
1346
|
|
|
1343
1347
|
const toolCalls = enrichToolCallsWithQuestion(
|
|
@@ -1735,6 +1739,7 @@ export async function handleSendMessage(
|
|
|
1735
1739
|
const principalType = headers?.["x-vellum-principal-type"];
|
|
1736
1740
|
const originClientId = headers?.["x-vellum-client-id"]?.trim() || undefined;
|
|
1737
1741
|
const clientMetadata = readClientMetadataHeaders(headers);
|
|
1742
|
+
const locale = localeFromAcceptLanguage(headers?.["accept-language"]);
|
|
1738
1743
|
|
|
1739
1744
|
const { conversationKey, content, attachmentIds } = body;
|
|
1740
1745
|
const inboundConversationId =
|
|
@@ -2475,6 +2480,7 @@ export async function handleSendMessage(
|
|
|
2475
2480
|
clientMetadata,
|
|
2476
2481
|
),
|
|
2477
2482
|
isInteractive,
|
|
2483
|
+
locale,
|
|
2478
2484
|
sourceActorPrincipalId,
|
|
2479
2485
|
transport,
|
|
2480
2486
|
clientMessageId,
|
|
@@ -3159,6 +3165,7 @@ export async function handleSendMessage(
|
|
|
3159
3165
|
onEvent: broadcastMessage,
|
|
3160
3166
|
isInteractive,
|
|
3161
3167
|
isUserMessage: true,
|
|
3168
|
+
locale,
|
|
3162
3169
|
turnTrustContext,
|
|
3163
3170
|
...(body.hidden === true ? { isHiddenPrompt: true } : {}),
|
|
3164
3171
|
})
|