@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
|
@@ -107,6 +107,7 @@ import { faviconUrlForDomain } from "../util/favicon.js";
|
|
|
107
107
|
import { redactLogString } from "../util/log-redact.js";
|
|
108
108
|
import { getLogger } from "../util/logger.js";
|
|
109
109
|
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
110
|
+
import { safeStringSlice } from "../util/unicode.js";
|
|
110
111
|
import type { DirectiveRequest } from "./assistant-attachments.js";
|
|
111
112
|
import {
|
|
112
113
|
cleanAssistantContent,
|
|
@@ -640,6 +641,8 @@ export interface EventHandlerState {
|
|
|
640
641
|
* rather than on every delta.
|
|
641
642
|
*/
|
|
642
643
|
readonly surfacePendingScannedToolUseIds: Set<string>;
|
|
644
|
+
/** App previews whose cumulative JSON reached the client payload cap. */
|
|
645
|
+
readonly cappedAppPreviewToolUseIds: Set<string>;
|
|
643
646
|
/**
|
|
644
647
|
* In-flight priming of {@link liveRevealGuardEntries}. The dispatcher
|
|
645
648
|
* awaits this before processing a `text_delta` (and before the
|
|
@@ -777,6 +780,7 @@ export function createEventHandlerState(): EventHandlerState {
|
|
|
777
780
|
forChatMintWatermark: currentForChatMintWatermark(),
|
|
778
781
|
stagedRevealIdentities: new Set(),
|
|
779
782
|
surfacePendingScannedToolUseIds: new Set(),
|
|
783
|
+
cappedAppPreviewToolUseIds: new Set(),
|
|
780
784
|
liveRevealGuardPriming: undefined,
|
|
781
785
|
};
|
|
782
786
|
}
|
|
@@ -1411,6 +1415,7 @@ function schedulePartialFlush(
|
|
|
1411
1415
|
// tools the client discards it (extractCodePreview only handles app tools),
|
|
1412
1416
|
// so we skip forwarding entirely to avoid transport/decode overhead.
|
|
1413
1417
|
const APP_TOOL_NAMES = new Set(["app_create"]);
|
|
1418
|
+
const APP_INPUT_PREVIEW_CHAR_LIMIT = 256_000;
|
|
1414
1419
|
|
|
1415
1420
|
// ── Surface Placeholder Detection ────────────────────────────────────
|
|
1416
1421
|
// A `visual` ui_show streams the longest tool input the model produces, and
|
|
@@ -2111,13 +2116,24 @@ export function handleInputJsonDelta(
|
|
|
2111
2116
|
// Only forward input deltas for app tools — the client only uses this
|
|
2112
2117
|
// stream for app_create code previews. Non-app tools would send large
|
|
2113
2118
|
// cumulative JSON on every delta with no benefit.
|
|
2114
|
-
if (
|
|
2119
|
+
if (
|
|
2120
|
+
!APP_TOOL_NAMES.has(event.toolName) ||
|
|
2121
|
+
state.cappedAppPreviewToolUseIds.has(event.toolUseId)
|
|
2122
|
+
) {
|
|
2115
2123
|
return;
|
|
2116
2124
|
}
|
|
2125
|
+
const content = safeStringSlice(
|
|
2126
|
+
event.accumulatedJson,
|
|
2127
|
+
0,
|
|
2128
|
+
APP_INPUT_PREVIEW_CHAR_LIMIT,
|
|
2129
|
+
);
|
|
2130
|
+
if (content.length < event.accumulatedJson.length) {
|
|
2131
|
+
state.cappedAppPreviewToolUseIds.add(event.toolUseId);
|
|
2132
|
+
}
|
|
2117
2133
|
deps.onEvent({
|
|
2118
2134
|
type: "tool_input_delta",
|
|
2119
2135
|
toolName: event.toolName,
|
|
2120
|
-
content
|
|
2136
|
+
content,
|
|
2121
2137
|
conversationId: deps.ctx.conversationId,
|
|
2122
2138
|
toolUseId: event.toolUseId,
|
|
2123
2139
|
messageId: state.lastAssistantMessageId,
|
|
@@ -44,6 +44,7 @@ import { isSendUserMessageActiveForTurn } from "../config/send-user-message-gate
|
|
|
44
44
|
import { desktopAutomationLease } from "../desktop/desktop-automation-lease.js";
|
|
45
45
|
import { writeRelationshipState } from "../home/relationship-state-writer.js";
|
|
46
46
|
import type { UserPromptSubmitInputContext } from "../hooks/types.js";
|
|
47
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
47
48
|
import {
|
|
48
49
|
addMessage,
|
|
49
50
|
deleteMessageById,
|
|
@@ -500,6 +501,7 @@ export async function runAgentLoopImpl(
|
|
|
500
501
|
* loop defaults to `'mainAgent'` for user-initiated turns.
|
|
501
502
|
*/
|
|
502
503
|
callSite?: LLMCallSite;
|
|
504
|
+
locale?: SupportedLocale;
|
|
503
505
|
/** Skip fresh retrieval while keeping resident memory and static context. */
|
|
504
506
|
skipMemoryRetrieval?: boolean;
|
|
505
507
|
/**
|
|
@@ -1719,10 +1721,12 @@ export async function runAgentLoopImpl(
|
|
|
1719
1721
|
onCheckpoint,
|
|
1720
1722
|
callSite: turnCallSite,
|
|
1721
1723
|
inferenceCallSite,
|
|
1724
|
+
locale: options?.locale,
|
|
1722
1725
|
suppressAssistantText: sendUserMessageActive,
|
|
1723
1726
|
supportsDynamicUi: conversationSupportsDynamicUi(ctx),
|
|
1724
1727
|
trust: loopTrust,
|
|
1725
1728
|
overrideProfile: turnOverrideProfile,
|
|
1729
|
+
...(autoRoute ? { overrideProfileOrigin: "auto" as const } : {}),
|
|
1726
1730
|
...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
|
|
1727
1731
|
resolveOverrideProfile: refreshCurrentProfileState,
|
|
1728
1732
|
...(onModelCallPrepared !== undefined ? { onModelCallPrepared } : {}),
|
|
@@ -2147,6 +2151,9 @@ export async function runAgentLoopImpl(
|
|
|
2147
2151
|
// path to the terminal SSE that re-enables the composer.
|
|
2148
2152
|
ctx.messages = restoredHistory;
|
|
2149
2153
|
|
|
2154
|
+
// The row's override is whichever profile served the last call.
|
|
2155
|
+
const mainRowOverrideProfile =
|
|
2156
|
+
state.exchangeInferenceProfile ?? refreshCurrentProfileState() ?? null;
|
|
2150
2157
|
emitUsage(
|
|
2151
2158
|
ctx,
|
|
2152
2159
|
state.exchangeInputTokens,
|
|
@@ -2171,16 +2178,17 @@ export async function runAgentLoopImpl(
|
|
|
2171
2178
|
// backup (via `state.exchangeProviderName` / `state.model`), so
|
|
2172
2179
|
// attributing the profile from the primary would write a row that
|
|
2173
2180
|
// contradicts itself. `forceOverrideProfile` floats it above the
|
|
2174
|
-
// call-site profile exactly as the fallback dispatch did.
|
|
2181
|
+
// call-site profile exactly as the fallback dispatch did. The Auto
|
|
2182
|
+
// origin applies only while the row's override is still the router's
|
|
2183
|
+
// pick: a profile switched in mid-turn is the user's.
|
|
2175
2184
|
{
|
|
2176
2185
|
callSite: inferenceCallSite,
|
|
2177
|
-
overrideProfile:
|
|
2178
|
-
state.exchangeInferenceProfile ??
|
|
2179
|
-
refreshCurrentProfileState() ??
|
|
2180
|
-
null,
|
|
2186
|
+
overrideProfile: mainRowOverrideProfile,
|
|
2181
2187
|
...(state.exchangeInferenceProfile !== undefined
|
|
2182
2188
|
? { forceOverrideProfile: true }
|
|
2183
|
-
:
|
|
2189
|
+
: autoRoute && mainRowOverrideProfile === autoRoute.profile
|
|
2190
|
+
? { overrideProfileOrigin: "auto" as const }
|
|
2191
|
+
: {}),
|
|
2184
2192
|
},
|
|
2185
2193
|
turnCronRunId,
|
|
2186
2194
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSubagentManager } from "../subagent/index.js";
|
|
2
|
+
import { getContainerMemoryLimitBytes } from "../util/cgroup-memory.js";
|
|
2
3
|
import { getLogger } from "../util/logger.js";
|
|
3
4
|
import { getConversationMap } from "./conversation-registry.js";
|
|
4
5
|
|
|
@@ -15,7 +16,7 @@ export interface EvictorOptions {
|
|
|
15
16
|
ttlMs?: number;
|
|
16
17
|
/** Max number of in-memory conversations before LRU eviction kicks in. Default: 100. */
|
|
17
18
|
maxConversations?: number;
|
|
18
|
-
/** RSS threshold (bytes) above which idle conversations are aggressively evicted. Default:
|
|
19
|
+
/** RSS threshold (bytes) above which idle conversations are aggressively evicted. Default: {@link defaultMemoryThresholdBytes}. */
|
|
19
20
|
memoryThresholdBytes?: number;
|
|
20
21
|
/** Interval between periodic sweeps (ms). Default: 60 s. */
|
|
21
22
|
sweepIntervalMs?: number;
|
|
@@ -34,13 +35,34 @@ export interface EvictionResult {
|
|
|
34
35
|
|
|
35
36
|
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
36
37
|
const DEFAULT_MAX_CONVERSATIONS = 100;
|
|
37
|
-
|
|
38
|
+
/** Threshold when no container limit is known (local hosts). */
|
|
39
|
+
const FALLBACK_MEMORY_THRESHOLD_BYTES = 3072 * 1024 * 1024; // 3 GiB
|
|
40
|
+
/**
|
|
41
|
+
* Share of the container memory limit the daemon's own RSS may reach before
|
|
42
|
+
* idle conversations are evicted. The rest of the limit is Qdrant, the
|
|
43
|
+
* workers, and tool children, so the daemon cannot have it all.
|
|
44
|
+
*/
|
|
45
|
+
const MEMORY_THRESHOLD_LIMIT_FRACTION = 0.5;
|
|
38
46
|
const DEFAULT_SWEEP_INTERVAL_MS = 60 * 1000; // 60 seconds
|
|
47
|
+
/**
|
|
48
|
+
* Idle conversations evicted per sweep under memory pressure. RSS cannot fall
|
|
49
|
+
* until the disposed heap is collected after the sweep returns, so the sweep
|
|
50
|
+
* evicts a bounded batch and the next sweep re-measures.
|
|
51
|
+
*/
|
|
52
|
+
const MEMORY_EVICTION_BATCH = 10;
|
|
53
|
+
|
|
54
|
+
/** Default memory-pressure threshold: half the container limit, or 3 GiB when there is none. */
|
|
55
|
+
export function defaultMemoryThresholdBytes(limitBytes: number | null): number {
|
|
56
|
+
return limitBytes != null
|
|
57
|
+
? Math.floor(limitBytes * MEMORY_THRESHOLD_LIMIT_FRACTION)
|
|
58
|
+
: FALLBACK_MEMORY_THRESHOLD_BYTES;
|
|
59
|
+
}
|
|
39
60
|
|
|
40
61
|
export class ConversationEvictor {
|
|
41
62
|
private readonly ttlMs: number;
|
|
42
63
|
private readonly maxConversations: number;
|
|
43
|
-
|
|
64
|
+
/** Explicit threshold, or null to derive it from the container limit on first use. */
|
|
65
|
+
private memoryThresholdBytes: number | null;
|
|
44
66
|
private readonly sweepIntervalMs: number;
|
|
45
67
|
|
|
46
68
|
/** Tracks last access time per conversation ID. */
|
|
@@ -57,8 +79,7 @@ export class ConversationEvictor {
|
|
|
57
79
|
this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
|
|
58
80
|
this.maxConversations =
|
|
59
81
|
options?.maxConversations ?? DEFAULT_MAX_CONVERSATIONS;
|
|
60
|
-
this.memoryThresholdBytes =
|
|
61
|
-
options?.memoryThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD_BYTES;
|
|
82
|
+
this.memoryThresholdBytes = options?.memoryThresholdBytes ?? null;
|
|
62
83
|
this.sweepIntervalMs =
|
|
63
84
|
options?.sweepIntervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
|
|
64
85
|
}
|
|
@@ -155,25 +176,26 @@ export class ConversationEvictor {
|
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
178
|
|
|
158
|
-
// Phase 3: Memory pressure — if RSS exceeds threshold, evict
|
|
159
|
-
// starting from least-recently-used
|
|
160
|
-
// no more idle conversations remain.
|
|
179
|
+
// Phase 3: Memory pressure — if RSS exceeds threshold, evict a batch of
|
|
180
|
+
// idle conversations starting from least-recently-used.
|
|
161
181
|
const rss = process.memoryUsage.rss();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
182
|
+
const thresholdBytes = this.resolveMemoryThresholdBytes();
|
|
183
|
+
if (rss > thresholdBytes) {
|
|
184
|
+
const batch = this.idleConversationsByLru().slice(
|
|
185
|
+
0,
|
|
186
|
+
MEMORY_EVICTION_BATCH,
|
|
187
|
+
);
|
|
188
|
+
if (batch.length > 0) {
|
|
165
189
|
log.warn(
|
|
166
190
|
{
|
|
167
191
|
rssBytes: rss,
|
|
168
|
-
thresholdBytes
|
|
192
|
+
thresholdBytes,
|
|
169
193
|
conversationCount: this.conversations.size,
|
|
194
|
+
evicting: batch.length,
|
|
170
195
|
},
|
|
171
196
|
"Memory pressure detected, evicting idle conversations",
|
|
172
197
|
);
|
|
173
|
-
for (const [id, conversation] of
|
|
174
|
-
if (process.memoryUsage.rss() <= this.memoryThresholdBytes) {
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
198
|
+
for (const [id, conversation] of batch) {
|
|
177
199
|
this.evict(id, conversation);
|
|
178
200
|
result.memoryEvicted++;
|
|
179
201
|
}
|
|
@@ -198,6 +220,18 @@ export class ConversationEvictor {
|
|
|
198
220
|
|
|
199
221
|
// ── Internals ──────────────────────────────────────────────────────
|
|
200
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Resolved on first sweep rather than at construction: the module-level
|
|
225
|
+
* singleton is built at import, before the daemon has loaded its dotenv
|
|
226
|
+
* file, and VELLUM_MEMORY_LIMIT may come from there.
|
|
227
|
+
*/
|
|
228
|
+
private resolveMemoryThresholdBytes(): number {
|
|
229
|
+
this.memoryThresholdBytes ??= defaultMemoryThresholdBytes(
|
|
230
|
+
getContainerMemoryLimitBytes(),
|
|
231
|
+
);
|
|
232
|
+
return this.memoryThresholdBytes;
|
|
233
|
+
}
|
|
234
|
+
|
|
201
235
|
private evict(id: string, conversation: EvictableConversation): void {
|
|
202
236
|
conversation.dispose();
|
|
203
237
|
this.conversations.delete(id);
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
parseInterfaceId,
|
|
27
27
|
} from "../channels/types.js";
|
|
28
28
|
import { parseImageDimensions } from "../context/image-dimensions.js";
|
|
29
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
29
30
|
import {
|
|
30
31
|
type ProviderMessageMetadata,
|
|
31
32
|
providerMessageMetadataSchema,
|
|
@@ -824,6 +825,7 @@ export interface EnqueueMessageOptions {
|
|
|
824
825
|
currentPage?: string;
|
|
825
826
|
metadata?: Record<string, unknown>;
|
|
826
827
|
isInteractive?: boolean;
|
|
828
|
+
locale?: SupportedLocale;
|
|
827
829
|
displayContent?: string;
|
|
828
830
|
transport?: ConversationTransportMetadata;
|
|
829
831
|
clientMessageId?: string;
|
|
@@ -873,6 +875,7 @@ export function enqueueMessage(
|
|
|
873
875
|
currentPage,
|
|
874
876
|
metadata,
|
|
875
877
|
isInteractive,
|
|
878
|
+
locale,
|
|
876
879
|
displayContent,
|
|
877
880
|
transport,
|
|
878
881
|
clientMessageId,
|
|
@@ -912,6 +915,7 @@ export function enqueueMessage(
|
|
|
912
915
|
turnChannelContext,
|
|
913
916
|
turnInterfaceContext,
|
|
914
917
|
isInteractive,
|
|
918
|
+
locale,
|
|
915
919
|
sourceActorPrincipalId,
|
|
916
920
|
authContext: queuedAuthContext,
|
|
917
921
|
trustContext: queuedTrustContext,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TurnInterfaceContext,
|
|
21
21
|
} from "../channels/types.js";
|
|
22
22
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
23
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
23
24
|
import { extractPreferences } from "../notifications/preference-extractor.js";
|
|
24
25
|
import { createPreference } from "../notifications/preferences-store.js";
|
|
25
26
|
import {
|
|
@@ -1278,6 +1279,7 @@ async function drainSingleMessage(
|
|
|
1278
1279
|
titleText?: string;
|
|
1279
1280
|
isHiddenPrompt?: boolean;
|
|
1280
1281
|
turnTrustContext?: TrustContext;
|
|
1282
|
+
locale?: SupportedLocale;
|
|
1281
1283
|
cronRunId?: string | null;
|
|
1282
1284
|
} = {
|
|
1283
1285
|
isUserMessage: true,
|
|
@@ -1289,6 +1291,9 @@ async function drainSingleMessage(
|
|
|
1289
1291
|
if (next.isInteractive !== undefined) {
|
|
1290
1292
|
drainLoopOptions.isInteractive = next.isInteractive;
|
|
1291
1293
|
}
|
|
1294
|
+
if (next.locale !== undefined) {
|
|
1295
|
+
drainLoopOptions.locale = next.locale;
|
|
1296
|
+
}
|
|
1292
1297
|
if (agentLoopContent !== resolvedContent) {
|
|
1293
1298
|
drainLoopOptions.titleText = resolvedContent;
|
|
1294
1299
|
}
|
|
@@ -1797,6 +1802,7 @@ async function drainBatch(
|
|
|
1797
1802
|
isHiddenPrompt?: boolean;
|
|
1798
1803
|
notifyUserMessageId?: string;
|
|
1799
1804
|
turnTrustContext?: TrustContext;
|
|
1805
|
+
locale?: SupportedLocale;
|
|
1800
1806
|
cronRunId?: string | null;
|
|
1801
1807
|
} = {
|
|
1802
1808
|
isUserMessage: true,
|
|
@@ -1816,6 +1822,9 @@ async function drainBatch(
|
|
|
1816
1822
|
if (lastSuccessfulBatchEntry?.isInteractive !== undefined) {
|
|
1817
1823
|
drainLoopOptions.isInteractive = lastSuccessfulBatchEntry.isInteractive;
|
|
1818
1824
|
}
|
|
1825
|
+
if (lastSuccessfulBatchEntry?.locale !== undefined) {
|
|
1826
|
+
drainLoopOptions.locale = lastSuccessfulBatchEntry.locale;
|
|
1827
|
+
}
|
|
1819
1828
|
// A batch counts as a hidden turn only when every message in it is a
|
|
1820
1829
|
// hidden machine signal — one genuine user prompt justifies the
|
|
1821
1830
|
// prompt-as-user-speech consumers (title generation).
|
|
@@ -1871,6 +1880,7 @@ export interface ProcessMessageOptions {
|
|
|
1871
1880
|
currentPage?: string;
|
|
1872
1881
|
isInteractive?: boolean;
|
|
1873
1882
|
callSite?: LLMCallSite;
|
|
1883
|
+
locale?: SupportedLocale;
|
|
1874
1884
|
/**
|
|
1875
1885
|
* Optional ad-hoc inference-profile override applied to every LLM call
|
|
1876
1886
|
* this turn issues (e.g. a schedule's pinned profile). Forwarded to
|
|
@@ -1928,6 +1938,7 @@ export async function processMessage(
|
|
|
1928
1938
|
currentPage,
|
|
1929
1939
|
isInteractive,
|
|
1930
1940
|
callSite,
|
|
1941
|
+
locale,
|
|
1931
1942
|
overrideProfile,
|
|
1932
1943
|
displayContent,
|
|
1933
1944
|
sourceActorPrincipalId,
|
|
@@ -2451,6 +2462,7 @@ export async function processMessage(
|
|
|
2451
2462
|
isUserMessage?: boolean;
|
|
2452
2463
|
titleText?: string;
|
|
2453
2464
|
callSite?: LLMCallSite;
|
|
2465
|
+
locale?: SupportedLocale;
|
|
2454
2466
|
overrideProfile?: string;
|
|
2455
2467
|
turnTrustContext?: TrustContext;
|
|
2456
2468
|
} = {
|
|
@@ -2471,6 +2483,9 @@ export async function processMessage(
|
|
|
2471
2483
|
if (callSite !== undefined) {
|
|
2472
2484
|
loopOptions.callSite = callSite;
|
|
2473
2485
|
}
|
|
2486
|
+
if (locale !== undefined) {
|
|
2487
|
+
loopOptions.locale = locale;
|
|
2488
|
+
}
|
|
2474
2489
|
if (overrideProfile !== undefined) {
|
|
2475
2490
|
loopOptions.overrideProfile = overrideProfile;
|
|
2476
2491
|
}
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
TurnChannelContext,
|
|
11
11
|
TurnInterfaceContext,
|
|
12
12
|
} from "../channels/types.js";
|
|
13
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
13
14
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
14
15
|
import { getLogger } from "../util/logger.js";
|
|
15
16
|
import type { UserMessageAttachment } from "./message-protocol.js";
|
|
@@ -30,6 +31,7 @@ export interface QueuedMessage {
|
|
|
30
31
|
turnInterfaceContext?: TurnInterfaceContext;
|
|
31
32
|
/** When false, the turn has no interactive user and should skip clarification prompts. */
|
|
32
33
|
isInteractive?: boolean;
|
|
34
|
+
locale?: SupportedLocale;
|
|
33
35
|
/** Requester identity captured from the verified auth context at enqueue time. */
|
|
34
36
|
sourceActorPrincipalId?: string;
|
|
35
37
|
/** Full auth snapshot captured at enqueue time for turn-scoped authorization decisions. */
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
derefToolResultReReads,
|
|
45
45
|
postTurnTruncateToolResults,
|
|
46
46
|
} from "../context/post-turn-tool-result-truncation.js";
|
|
47
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
47
48
|
import { readProviderMetadata } from "../messaging/read-provider-metadata.js";
|
|
48
49
|
import { isGuardianCardRow } from "../notifications/approval-card-data.js";
|
|
49
50
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
@@ -3528,6 +3529,7 @@ export class Conversation {
|
|
|
3528
3529
|
*/
|
|
3529
3530
|
replyDeliveredInAppOnly?: boolean;
|
|
3530
3531
|
callSite?: LLMCallSite;
|
|
3532
|
+
locale?: SupportedLocale;
|
|
3531
3533
|
/** Skip fresh retrieval while keeping resident memory and static context. */
|
|
3532
3534
|
skipMemoryRetrieval?: boolean;
|
|
3533
3535
|
/** Provider configuration source when distinct from turn semantics. */
|
|
@@ -78,7 +78,7 @@ function updateDoordashSteps(
|
|
|
78
78
|
return undefined;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
return steps.map((s, i) => {
|
|
82
82
|
if (i < stepIndex) {
|
|
83
83
|
// Steps before current should be completed
|
|
84
84
|
return s.status === "completed" ? s : { ...s, status: "completed" };
|
|
@@ -96,8 +96,6 @@ function updateDoordashSteps(
|
|
|
96
96
|
}
|
|
97
97
|
return s;
|
|
98
98
|
});
|
|
99
|
-
|
|
100
|
-
return updated;
|
|
101
99
|
}
|
|
102
100
|
|
|
103
101
|
// ── Helpers for reading/writing the task_progress surface ─────────────
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
} from "../../../contacts/contact-store.js";
|
|
22
22
|
import { getSqlite } from "../../../persistence/db-connection.js";
|
|
23
23
|
import { initializeDb } from "../../../persistence/db-init.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
acceptA2AInvite,
|
|
26
|
+
assertSafeSenderGatewayUrl,
|
|
27
|
+
createA2AInvite,
|
|
28
|
+
} from "../config-a2a.js";
|
|
25
29
|
|
|
26
30
|
await initializeDb();
|
|
27
31
|
|
|
@@ -487,4 +491,42 @@ describe("acceptA2AInvite", () => {
|
|
|
487
491
|
const contacts = searchContacts({ channelType: "a2a" });
|
|
488
492
|
expect(contacts).toHaveLength(0);
|
|
489
493
|
});
|
|
494
|
+
|
|
495
|
+
test("rejects http, credentials, and loopback or metadata hosts", async () => {
|
|
496
|
+
const originalFetch = globalThis.fetch;
|
|
497
|
+
let fetchCalls = 0;
|
|
498
|
+
globalThis.fetch = (async () => {
|
|
499
|
+
fetchCalls += 1;
|
|
500
|
+
throw new Error("fetch should not run");
|
|
501
|
+
}) as unknown as typeof fetch;
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
for (const url of [
|
|
505
|
+
"http://sender.example.com",
|
|
506
|
+
"https://user:pass@sender.example.com",
|
|
507
|
+
"https://127.0.0.1",
|
|
508
|
+
"https://localhost",
|
|
509
|
+
"https://169.254.169.254",
|
|
510
|
+
"https://metadata.google.internal",
|
|
511
|
+
]) {
|
|
512
|
+
const result = await acceptA2AInvite({
|
|
513
|
+
senderGatewayUrl: url,
|
|
514
|
+
senderAssistantId: SENDER_ASSISTANT_ID,
|
|
515
|
+
token: "any-token",
|
|
516
|
+
});
|
|
517
|
+
expect(result.success).toBe(false);
|
|
518
|
+
expect(result.errorCode).toBe("invalid_sender_url");
|
|
519
|
+
}
|
|
520
|
+
expect(fetchCalls).toBe(0);
|
|
521
|
+
} finally {
|
|
522
|
+
globalThis.fetch = originalFetch;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("allows https LAN peers used by self-hosted accept", () => {
|
|
527
|
+
expect(assertSafeSenderGatewayUrl("https://192.168.1.20:8787").ok).toBe(
|
|
528
|
+
true,
|
|
529
|
+
);
|
|
530
|
+
expect(assertSafeSenderGatewayUrl("https://peer.ts.net").ok).toBe(true);
|
|
531
|
+
});
|
|
490
532
|
});
|
|
@@ -303,6 +303,54 @@ export function redeemA2AInvite(params: {
|
|
|
303
303
|
// ── Self-hosted broker ──────────────────────────────────────────────
|
|
304
304
|
|
|
305
305
|
const ACCEPT_TIMEOUT_MS = 15_000;
|
|
306
|
+
const ACCEPT_MAX_BODY_BYTES = 64_000;
|
|
307
|
+
|
|
308
|
+
const BLOCKED_SENDER_HOSTS = new Set([
|
|
309
|
+
"localhost",
|
|
310
|
+
"localhost.localdomain",
|
|
311
|
+
"metadata.google.internal",
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Self-hosted accept brokers an outbound POST to the peer's invite/complete
|
|
316
|
+
* URL. Require https, reject credentials, and refuse loopback/metadata
|
|
317
|
+
* literals so a settings.write caller cannot steer the daemon at link-local
|
|
318
|
+
* or cloud-metadata endpoints. RFC1918 and Tailscale hostnames stay allowed
|
|
319
|
+
* because that is how self-hosted peers reach each other on a LAN.
|
|
320
|
+
*/
|
|
321
|
+
export function assertSafeSenderGatewayUrl(
|
|
322
|
+
raw: string,
|
|
323
|
+
): { ok: true; origin: string } | { ok: false; error: string } {
|
|
324
|
+
let parsed: URL;
|
|
325
|
+
try {
|
|
326
|
+
parsed = new URL(raw);
|
|
327
|
+
} catch {
|
|
328
|
+
return { ok: false, error: "senderGatewayUrl must be a valid URL" };
|
|
329
|
+
}
|
|
330
|
+
if (parsed.protocol !== "https:") {
|
|
331
|
+
return { ok: false, error: "senderGatewayUrl must use https" };
|
|
332
|
+
}
|
|
333
|
+
if (parsed.username || parsed.password) {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
error: "senderGatewayUrl must not include credentials",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
340
|
+
if (!host) {
|
|
341
|
+
return { ok: false, error: "senderGatewayUrl must include a host" };
|
|
342
|
+
}
|
|
343
|
+
if (BLOCKED_SENDER_HOSTS.has(host) || host.endsWith(".localhost")) {
|
|
344
|
+
return { ok: false, error: "senderGatewayUrl host is not allowed" };
|
|
345
|
+
}
|
|
346
|
+
if (host === "127.0.0.1" || host === "::1" || host === "0.0.0.0") {
|
|
347
|
+
return { ok: false, error: "senderGatewayUrl host is not allowed" };
|
|
348
|
+
}
|
|
349
|
+
if (host.startsWith("169.254.") || host === "169.254.169.254") {
|
|
350
|
+
return { ok: false, error: "senderGatewayUrl host is not allowed" };
|
|
351
|
+
}
|
|
352
|
+
return { ok: true, origin: parsed.origin };
|
|
353
|
+
}
|
|
306
354
|
|
|
307
355
|
/**
|
|
308
356
|
* Extract a human-readable error message from a daemon HTTP error
|
|
@@ -339,7 +387,15 @@ export async function acceptA2AInvite(params: {
|
|
|
339
387
|
senderAssistantId: string;
|
|
340
388
|
token: string;
|
|
341
389
|
}): Promise<AcceptA2AInviteResult> {
|
|
342
|
-
const
|
|
390
|
+
const safety = assertSafeSenderGatewayUrl(params.senderGatewayUrl);
|
|
391
|
+
if (!safety.ok) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
error: safety.error,
|
|
395
|
+
errorCode: "invalid_sender_url",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const senderGatewayUrl = safety.origin;
|
|
343
399
|
|
|
344
400
|
// 1. Validate local config
|
|
345
401
|
const displayName = getAssistantName() ?? "Vellum Assistant";
|
|
@@ -380,10 +436,27 @@ export async function acceptA2AInvite(params: {
|
|
|
380
436
|
method: "POST",
|
|
381
437
|
headers: { "Content-Type": "application/json" },
|
|
382
438
|
body: JSON.stringify(completeBody),
|
|
439
|
+
redirect: "error",
|
|
383
440
|
signal: AbortSignal.timeout(ACCEPT_TIMEOUT_MS),
|
|
384
441
|
});
|
|
385
442
|
|
|
386
|
-
|
|
443
|
+
const rawBody = await response.text();
|
|
444
|
+
if (new TextEncoder().encode(rawBody).byteLength > ACCEPT_MAX_BODY_BYTES) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: "Sender invite/complete response exceeded size limit",
|
|
448
|
+
errorCode: "complete_failed",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
completeData = JSON.parse(rawBody) as Record<string, unknown>;
|
|
453
|
+
} catch {
|
|
454
|
+
return {
|
|
455
|
+
success: false,
|
|
456
|
+
error: "Sender invite/complete returned invalid JSON",
|
|
457
|
+
errorCode: "complete_failed",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
387
460
|
|
|
388
461
|
if (!response.ok) {
|
|
389
462
|
const error =
|
|
@@ -19,6 +19,10 @@ import type {
|
|
|
19
19
|
import { ConfirmationDecisionSchema } from "../../api/responses/conversation-message.js";
|
|
20
20
|
import { getConfig } from "../../config/loader.js";
|
|
21
21
|
import type { LLMCallSite, Speed } from "../../config/schemas/llm.js";
|
|
22
|
+
import {
|
|
23
|
+
resolveStoredMessageText,
|
|
24
|
+
type SupportedLocale,
|
|
25
|
+
} from "../../i18n/index.js";
|
|
22
26
|
import { ipcCall as gatewayIpcCall } from "../../ipc/gateway-client.js";
|
|
23
27
|
import type { ProviderMessageMetadata } from "../../messaging/provider-message-metadata.js";
|
|
24
28
|
import type { SecretPromptResult } from "../../permissions/secret-prompt-types.js";
|
|
@@ -409,6 +413,7 @@ export function renderHistoryContent(
|
|
|
409
413
|
>,
|
|
410
414
|
messageId?: string,
|
|
411
415
|
metadata?: unknown,
|
|
416
|
+
locale?: SupportedLocale,
|
|
412
417
|
): RenderedHistoryContent {
|
|
413
418
|
// A row whose turn routed its reply through `send_user_message` carries
|
|
414
419
|
// private working notes, so every consumer of this render (web history,
|
|
@@ -575,8 +580,14 @@ export function renderHistoryContent(
|
|
|
575
580
|
const rawText =
|
|
576
581
|
typeof block._redactionVersion === "number" &&
|
|
577
582
|
block._redactionVersion >= SENTINEL_REDACTION_VERSION
|
|
578
|
-
?
|
|
579
|
-
|
|
583
|
+
? locale
|
|
584
|
+
? resolveStoredMessageText(block.text, locale)
|
|
585
|
+
: block.text
|
|
586
|
+
: neutralizeRedactedSentinels(
|
|
587
|
+
locale
|
|
588
|
+
? resolveStoredMessageText(block.text, locale)
|
|
589
|
+
: block.text,
|
|
590
|
+
);
|
|
580
591
|
const displayText = unwrapExternalContentForDisplay(rawText);
|
|
581
592
|
// Skip empty/whitespace-only text blocks. During streaming the client
|
|
582
593
|
// discards empty text deltas (guard !text.isEmpty), so including them
|
|
@@ -28,6 +28,7 @@ import { POINT_AT_PROXY_TOOL } from "../tools/computer-use/skill-proxy-bridge.js
|
|
|
28
28
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
29
29
|
import { AssistantError, ErrorCode } from "../util/errors.js";
|
|
30
30
|
import { getLogger } from "../util/logger.js";
|
|
31
|
+
import { prepareComputerUseObservation } from "./computer-use-observation.js";
|
|
31
32
|
import { resolveHostCuTarget } from "./host-cu-target.js";
|
|
32
33
|
import { bestEffortModeSessionTracking } from "./mode-session-tracking.js";
|
|
33
34
|
|
|
@@ -615,7 +616,7 @@ export class HostCuProxy {
|
|
|
615
616
|
this.recordAction(toolName, input, reasoning);
|
|
616
617
|
this._previousAXTree = undefined;
|
|
617
618
|
this._consecutiveUnchangedSteps = 0;
|
|
618
|
-
const observation = await execute();
|
|
619
|
+
const observation = await prepareComputerUseObservation(await execute());
|
|
619
620
|
return this.formatObservation(observation, undefined, false, {
|
|
620
621
|
toolName,
|
|
621
622
|
input,
|
|
@@ -791,7 +792,7 @@ export class HostCuProxy {
|
|
|
791
792
|
);
|
|
792
793
|
}
|
|
793
794
|
|
|
794
|
-
const screenshotMeta = this.formatScreenshotMetadata(obs);
|
|
795
|
+
const screenshotMeta = this.formatScreenshotMetadata(obs, action);
|
|
795
796
|
if (screenshotMeta.length > 0) {
|
|
796
797
|
parts.push("");
|
|
797
798
|
parts.push(...screenshotMeta);
|
|
@@ -898,7 +899,10 @@ export class HostCuProxy {
|
|
|
898
899
|
}
|
|
899
900
|
}
|
|
900
901
|
|
|
901
|
-
private formatScreenshotMetadata(
|
|
902
|
+
private formatScreenshotMetadata(
|
|
903
|
+
obs: CuObservationResult,
|
|
904
|
+
action: ActionIdentity | undefined,
|
|
905
|
+
): string[] {
|
|
902
906
|
if (!obs.screenshot) {
|
|
903
907
|
return [];
|
|
904
908
|
}
|
|
@@ -914,6 +918,33 @@ export class HostCuProxy {
|
|
|
914
918
|
`Screen metadata: ${obs.screenWidthPt}x${obs.screenHeightPt} pt`,
|
|
915
919
|
);
|
|
916
920
|
}
|
|
921
|
+
if (action && hasCaptureTarget(action.input)) {
|
|
922
|
+
lines.push(
|
|
923
|
+
"This screenshot is capture-relative. Full-screen dimensions do not map its pixels to action coordinates. Use accessibility element IDs, or request an unscoped desktop observation before using coordinates.",
|
|
924
|
+
);
|
|
925
|
+
} else if (
|
|
926
|
+
[
|
|
927
|
+
obs.screenshotWidthPx,
|
|
928
|
+
obs.screenshotHeightPx,
|
|
929
|
+
obs.screenWidthPt,
|
|
930
|
+
obs.screenHeightPt,
|
|
931
|
+
].every(
|
|
932
|
+
(dimension) =>
|
|
933
|
+
typeof dimension === "number" &&
|
|
934
|
+
Number.isFinite(dimension) &&
|
|
935
|
+
dimension > 0,
|
|
936
|
+
)
|
|
937
|
+
) {
|
|
938
|
+
const scaleX = obs.screenWidthPt! / obs.screenshotWidthPx!;
|
|
939
|
+
const scaleY = obs.screenHeightPt! / obs.screenshotHeightPx!;
|
|
940
|
+
lines.push(
|
|
941
|
+
`Action coordinates are screen points, not screenshot pixels. Convert this image's pixels: x = round(image_x * ${scaleX.toFixed(6)}), y = round(image_y * ${scaleY.toFixed(6)}). Prefer accessibility element IDs when available.`,
|
|
942
|
+
);
|
|
943
|
+
} else {
|
|
944
|
+
lines.push(
|
|
945
|
+
"Screenshot-to-screen coordinate mapping is unavailable. Use accessibility element IDs or obtain an observation with both screenshot and screen dimensions before using coordinates.",
|
|
946
|
+
);
|
|
947
|
+
}
|
|
917
948
|
return lines;
|
|
918
949
|
}
|
|
919
950
|
}
|