@vellumai/assistant 0.4.48 → 0.4.49

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.
Files changed (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -1,4 +1,5 @@
1
1
  import type { ContentBlock, Message } from "../providers/types.js";
2
+ import { parseImageDimensions } from "./image-dimensions.js";
2
3
 
3
4
  const CHARS_PER_TOKEN = 4;
4
5
  const MESSAGE_OVERHEAD_TOKENS = 4;
@@ -12,6 +13,22 @@ const OTHER_BLOCK_TOKENS = 16;
12
13
  const SYSTEM_PROMPT_OVERHEAD_TOKENS = 8;
13
14
  const GEMINI_INLINE_FILE_MIME_TYPES = new Set(["application/pdf"]);
14
15
 
16
+ // Anthropic scales images to fit within 1568x1568 maintaining aspect ratio,
17
+ // then charges ~(width * height) / 750 tokens.
18
+ const ANTHROPIC_IMAGE_MAX_DIMENSION = 1568;
19
+ const ANTHROPIC_IMAGE_TOKENS_PER_PIXEL = 1 / 750;
20
+ const ANTHROPIC_IMAGE_MAX_TOKENS = Math.ceil(
21
+ ANTHROPIC_IMAGE_MAX_DIMENSION *
22
+ ANTHROPIC_IMAGE_MAX_DIMENSION *
23
+ ANTHROPIC_IMAGE_TOKENS_PER_PIXEL,
24
+ ); // ~3,277 tokens
25
+
26
+ // Anthropic renders each PDF page as an image (~1,568 tokens at standard
27
+ // resolution) plus any extracted text. Typical PDF pages are 50-150 KB.
28
+ // Using ~100 KB/page and ~1,600 tokens/page gives ~0.016 tokens/byte.
29
+ const ANTHROPIC_PDF_TOKENS_PER_BYTE = 0.016;
30
+ const ANTHROPIC_PDF_MIN_TOKENS = 1600; // At least one page
31
+
15
32
  export interface TokenEstimatorOptions {
16
33
  providerName?: string;
17
34
  }
@@ -21,21 +38,69 @@ export function estimateTextTokens(text: string): number {
21
38
  return Math.ceil(text.length / CHARS_PER_TOKEN);
22
39
  }
23
40
 
24
- function shouldCountFileSourceData(
41
+ function estimateAnthropicPdfTokens(base64Data: string): number {
42
+ const rawBytes = Math.ceil((base64Data.length * 3) / 4);
43
+ return Math.max(
44
+ ANTHROPIC_PDF_MIN_TOKENS,
45
+ Math.ceil(rawBytes * ANTHROPIC_PDF_TOKENS_PER_BYTE),
46
+ );
47
+ }
48
+
49
+ function estimateFileDataTokens(
25
50
  block: Extract<ContentBlock, { type: "file" }>,
26
51
  options?: TokenEstimatorOptions,
27
- ): boolean {
28
- if (options?.providerName !== "gemini") {
29
- return false;
52
+ ): number {
53
+ const providerName = options?.providerName;
54
+
55
+ // Anthropic sends PDFs as native document blocks and renders each page as an image
56
+ if (
57
+ providerName === "anthropic" &&
58
+ block.source.media_type === "application/pdf"
59
+ ) {
60
+ return estimateAnthropicPdfTokens(block.source.data);
61
+ }
62
+
63
+ // Gemini sends certain file types inline as base64
64
+ if (
65
+ providerName === "gemini" &&
66
+ GEMINI_INLINE_FILE_MIME_TYPES.has(block.source.media_type)
67
+ ) {
68
+ return estimateTextTokens(block.source.data);
30
69
  }
31
- return GEMINI_INLINE_FILE_MIME_TYPES.has(block.source.media_type);
70
+
71
+ return 0;
72
+ }
73
+
74
+ function estimateAnthropicImageTokens(width: number, height: number): number {
75
+ // Scale down to fit within 1568x1568 bounding box, maintaining aspect ratio
76
+ const scale = Math.min(
77
+ 1,
78
+ ANTHROPIC_IMAGE_MAX_DIMENSION / Math.max(width, height),
79
+ );
80
+ const scaledWidth = Math.round(width * scale);
81
+ const scaledHeight = Math.round(height * scale);
82
+ return Math.max(
83
+ IMAGE_BLOCK_TOKENS, // minimum 1024
84
+ Math.ceil(scaledWidth * scaledHeight * ANTHROPIC_IMAGE_TOKENS_PER_PIXEL),
85
+ );
32
86
  }
33
87
 
34
- function estimateImageSourceDataTokens(
88
+ function estimateImageTokens(
35
89
  block: Extract<ContentBlock, { type: "image" }>,
90
+ options?: TokenEstimatorOptions,
36
91
  ): number {
37
- // Image payloads are carried inline as base64 for all currently supported
38
- // providers, so estimator must scale with payload size (not fixed per image).
92
+ if (options?.providerName === "anthropic") {
93
+ const dims = parseImageDimensions(
94
+ block.source.data,
95
+ block.source.media_type,
96
+ );
97
+ if (dims) {
98
+ return estimateAnthropicImageTokens(dims.width, dims.height);
99
+ }
100
+ // Fallback: if dimensions can't be parsed, use Anthropic's max
101
+ return ANTHROPIC_IMAGE_MAX_TOKENS;
102
+ }
103
+ // Non-Anthropic: keep existing base64-size heuristic
39
104
  return estimateTextTokens(block.source.data);
40
105
  }
41
106
 
@@ -69,16 +134,14 @@ export function estimateContentBlockTokens(
69
134
  IMAGE_BLOCK_TOKENS,
70
135
  IMAGE_BLOCK_OVERHEAD_TOKENS +
71
136
  estimateTextTokens(block.source.media_type) +
72
- estimateImageSourceDataTokens(block),
137
+ estimateImageTokens(block, options),
73
138
  );
74
139
  case "file":
75
140
  return (
76
141
  FILE_BLOCK_OVERHEAD_TOKENS +
77
142
  estimateTextTokens(block.source.filename) +
78
143
  estimateTextTokens(block.source.media_type) +
79
- (shouldCountFileSourceData(block, options)
80
- ? estimateTextTokens(block.source.data)
81
- : 0) +
144
+ estimateFileDataTokens(block, options) +
82
145
  estimateTextTokens(block.extracted_text ?? "")
83
146
  );
84
147
  case "thinking":
@@ -83,21 +83,44 @@ export interface ContextWindowCompactOptions {
83
83
 
84
84
  export interface ContextWindowManagerOptions {
85
85
  provider: Provider;
86
- systemPrompt: string;
86
+ systemPrompt: string | (() => string);
87
87
  config: ContextWindowConfig;
88
88
  }
89
89
 
90
90
  export class ContextWindowManager {
91
91
  private readonly provider: Provider;
92
- private readonly systemPrompt: string;
92
+ private readonly _systemPrompt: string | (() => string);
93
93
  private readonly config: ContextWindowConfig;
94
+ /**
95
+ * Cached resolved system prompt. Lazily populated on first access via the
96
+ * `systemPrompt` getter and cleared after each compaction pass so the next
97
+ * pass picks up any prompt changes.
98
+ */
99
+ private _resolvedSystemPrompt: string | undefined;
94
100
 
95
101
  constructor(options: ContextWindowManagerOptions) {
96
102
  this.provider = options.provider;
97
- this.systemPrompt = options.systemPrompt;
103
+ this._systemPrompt = options.systemPrompt;
98
104
  this.config = options.config;
99
105
  }
100
106
 
107
+ /** Lazily resolve and cache the system prompt for the duration of a compaction pass. */
108
+ private get systemPrompt(): string {
109
+ if (this._resolvedSystemPrompt !== undefined) {
110
+ return this._resolvedSystemPrompt;
111
+ }
112
+ const resolved =
113
+ typeof this._systemPrompt === "function"
114
+ ? this._systemPrompt()
115
+ : this._systemPrompt;
116
+ this._resolvedSystemPrompt = resolved;
117
+ return resolved;
118
+ }
119
+
120
+ private clearSystemPromptCache(): void {
121
+ this._resolvedSystemPrompt = undefined;
122
+ }
123
+
101
124
  /**
102
125
  * Cheap pre-check: returns whether the estimated token count exceeds
103
126
  * the compaction threshold, along with the estimated token count so
@@ -106,19 +129,35 @@ export class ContextWindowManager {
106
129
  */
107
130
  shouldCompact(messages: Message[]): ShouldCompactResult {
108
131
  if (!this.config.enabled) return { needed: false, estimatedTokens: 0 };
109
- const estimated = estimatePromptTokens(messages, this.systemPrompt, {
110
- providerName: this.provider.name,
111
- });
112
- const threshold = Math.floor(
113
- this.config.maxInputTokens * this.config.compactThreshold,
114
- );
115
- return { needed: estimated >= threshold, estimatedTokens: estimated };
132
+ try {
133
+ const estimated = estimatePromptTokens(messages, this.systemPrompt, {
134
+ providerName: this.provider.name,
135
+ });
136
+ const threshold = Math.floor(
137
+ this.config.maxInputTokens * this.config.compactThreshold,
138
+ );
139
+ return { needed: estimated >= threshold, estimatedTokens: estimated };
140
+ } finally {
141
+ this.clearSystemPromptCache();
142
+ }
116
143
  }
117
144
 
118
145
  async maybeCompact(
119
146
  messages: Message[],
120
147
  signal?: AbortSignal,
121
148
  options?: ContextWindowCompactOptions,
149
+ ): Promise<ContextWindowResult> {
150
+ try {
151
+ return await this._maybeCompact(messages, signal, options);
152
+ } finally {
153
+ this.clearSystemPromptCache();
154
+ }
155
+ }
156
+
157
+ private async _maybeCompact(
158
+ messages: Message[],
159
+ signal?: AbortSignal,
160
+ options?: ContextWindowCompactOptions,
122
161
  ): Promise<ContextWindowResult> {
123
162
  const previousEstimatedInputTokens =
124
163
  options?.precomputedEstimate ??
@@ -17,9 +17,6 @@ import {
17
17
  // Constants
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- /** Maximum number of attachments the assistant may emit per turn. */
21
- export const MAX_ASSISTANT_ATTACHMENTS = 5;
22
-
23
20
  /** Maximum size in bytes for a single assistant attachment (20 MB). */
24
21
  export const MAX_ASSISTANT_ATTACHMENT_BYTES = 20 * 1024 * 1024;
25
22
 
@@ -122,10 +119,9 @@ export interface ValidatedDrafts {
122
119
  }
123
120
 
124
121
  /**
125
- * Enforce per-turn attachment caps.
122
+ * Enforce per-attachment size cap.
126
123
  *
127
124
  * - Rejects individual drafts that exceed `MAX_ASSISTANT_ATTACHMENT_BYTES`.
128
- * - Truncates the list at `MAX_ASSISTANT_ATTACHMENTS`.
129
125
  */
130
126
  export function validateDrafts(
131
127
  drafts: AssistantAttachmentDraft[],
@@ -144,14 +140,6 @@ export function validateDrafts(
144
140
  continue;
145
141
  }
146
142
 
147
- if (accepted.length >= MAX_ASSISTANT_ATTACHMENTS) {
148
- warnings.push(
149
- `Skipped attachment "${draft.filename}": ` +
150
- `exceeded maximum of ${MAX_ASSISTANT_ATTACHMENTS} attachments per turn.`,
151
- );
152
- continue;
153
- }
154
-
155
143
  accepted.push(draft);
156
144
  }
157
145
 
@@ -5,7 +5,6 @@ import {
5
5
  } from "../../calls/twilio-rest.js";
6
6
  import {
7
7
  getGatewayInternalBaseUrl,
8
- getIngressPublicBaseUrl,
9
8
  setIngressPublicBaseUrl,
10
9
  } from "../../config/env.js";
11
10
  import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
@@ -26,20 +25,6 @@ import {
26
25
  log,
27
26
  } from "./shared.js";
28
27
 
29
- // Lazily capture the env-provided INGRESS_PUBLIC_BASE_URL on first access
30
- // rather than at module load time. The daemon loads ~/.vellum/.env inside
31
- // runDaemon() (see lifecycle.ts), which runs AFTER static ES module imports
32
- // resolve. A module-level snapshot would miss dotenv-provided values.
33
- let _originalIngressEnvCaptured = false;
34
- let _originalIngressEnv: string | undefined;
35
- function getOriginalIngressEnv(): string | undefined {
36
- if (!_originalIngressEnvCaptured) {
37
- _originalIngressEnv = getIngressPublicBaseUrl();
38
- _originalIngressEnvCaptured = true;
39
- }
40
- return _originalIngressEnv;
41
- }
42
-
43
28
  export function computeGatewayTarget(): string {
44
29
  return getGatewayInternalBaseUrl();
45
30
  }
@@ -108,13 +93,11 @@ export async function handleIngressConfig(
108
93
  });
109
94
  } else if (msg.action === "set") {
110
95
  const value = (msg.publicBaseUrl ?? "").trim().replace(/\/+$/, "");
111
- // Ensure we capture the original env value before any mutation below
112
- getOriginalIngressEnv();
113
96
  const raw = loadRawConfig();
114
97
 
115
98
  // Update ingress.publicBaseUrl — this is the single source of truth for
116
- // the canonical public ingress URL. The gateway receives this value via
117
- // the INGRESS_PUBLIC_BASE_URL env var at spawn time (see hatch.ts).
99
+ // the canonical public ingress URL. The gateway reads this value from
100
+ // the workspace config file via ConfigFileCache.
118
101
  // The gateway also validates Twilio signatures against forwarded public
119
102
  // URL headers, so local tunnel updates generally apply without restarts.
120
103
  const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
@@ -139,24 +122,17 @@ export async function handleIngressConfig(
139
122
  CONFIG_RELOAD_DEBOUNCE_MS,
140
123
  );
141
124
 
142
- // Propagate to the gateway's process environment so it picks up the
143
- // new URL when it is restarted. For the local-deployment path the
144
- // gateway runs as a child process that inherited the assistant's env,
145
- // so updating process.env here ensures the value is visible when the
146
- // gateway is restarted (e.g. by the self-upgrade skill or a manual
147
- // `pkill -f gateway`).
148
- // Only export the URL when ingress is enabled; clearing it when
125
+ // Propagate to module-level state so the assistant's in-process URL
126
+ // resolution stays in sync. The gateway reads from the workspace config
127
+ // file directly via ConfigFileCache, so no env var propagation is needed.
128
+ // Only set the URL when ingress is enabled; clearing it when
149
129
  // disabled ensures the gateway stops accepting inbound webhooks.
150
130
  const isEnabled = (ingress.enabled as boolean | undefined) ?? false;
151
131
  if (value && isEnabled) {
152
132
  setIngressPublicBaseUrl(value);
153
- } else if (isEnabled && getOriginalIngressEnv() !== undefined) {
154
- // Ingress is enabled but the user cleared the URL — fall back to the
155
- // env var that was present when the process started.
156
- setIngressPublicBaseUrl(getOriginalIngressEnv()!);
157
133
  } else {
158
- // Ingress is disabled or no URL is configured and no startup env var
159
- // exists — remove the env var so the gateway stops accepting webhooks.
134
+ // Ingress is disabled or no URL is configured clear the module-level
135
+ // URL so the gateway stops accepting webhooks.
160
136
  setIngressPublicBaseUrl(undefined);
161
137
  }
162
138
 
@@ -250,4 +226,3 @@ export async function handleIngressConfig(
250
226
  });
251
227
  }
252
228
  }
253
-
@@ -5,6 +5,11 @@ import {
5
5
  saveRawConfig,
6
6
  setNestedValue,
7
7
  } from "../../config/loader.js";
8
+ import {
9
+ ensureManualTokenConnection,
10
+ removeManualTokenConnection,
11
+ } from "../../oauth/manual-token-connection.js";
12
+ import { getConnectionByProvider } from "../../oauth/oauth-store.js";
8
13
  import { credentialKey } from "../../security/credential-key.js";
9
14
  import {
10
15
  deleteSecureKeyAsync,
@@ -41,12 +46,15 @@ export function getSlackChannelConfig(): SlackChannelConfigResult {
41
46
  const hasAppToken = !!getSecureKey(
42
47
  credentialKey("slack_channel", "app_token"),
43
48
  );
49
+ const conn = getConnectionByProvider("slack_channel");
50
+ const connected =
51
+ !!(conn && conn.status === "active") && hasBotToken && hasAppToken;
44
52
  const { teamId, teamName, botUserId, botUsername } = getConfig().slack;
45
53
  return {
46
54
  success: true,
47
55
  hasBotToken,
48
56
  hasAppToken,
49
- connected: hasBotToken && hasAppToken,
57
+ connected,
50
58
  ...(teamId ? { teamId } : {}),
51
59
  ...(teamName ? { teamName } : {}),
52
60
  ...(botUserId ? { botUserId } : {}),
@@ -83,17 +91,13 @@ export async function setSlackChannelConfig(
83
91
  user?: string;
84
92
  };
85
93
  if (!data.ok) {
86
- const storedBotToken = !!getSecureKey(
87
- credentialKey("slack_channel", "bot_token"),
88
- );
89
- const storedAppToken = !!getSecureKey(
90
- credentialKey("slack_channel", "app_token"),
91
- );
94
+ const errConn = getConnectionByProvider("slack_channel");
95
+ const errConnected = !!(errConn && errConn.status === "active");
92
96
  return {
93
97
  success: false,
94
- hasBotToken: storedBotToken,
95
- hasAppToken: storedAppToken,
96
- connected: storedBotToken && storedAppToken,
98
+ hasBotToken: errConnected,
99
+ hasAppToken: errConnected,
100
+ connected: errConnected,
97
101
  error: `Slack API validation failed: ${
98
102
  data.error ?? "unknown error"
99
103
  }`,
@@ -107,17 +111,13 @@ export async function setSlackChannelConfig(
107
111
  };
108
112
  } catch (err) {
109
113
  const message = err instanceof Error ? err.message : String(err);
110
- const storedBotToken = !!getSecureKey(
111
- credentialKey("slack_channel", "bot_token"),
112
- );
113
- const storedAppToken = !!getSecureKey(
114
- credentialKey("slack_channel", "app_token"),
115
- );
114
+ const errConn = getConnectionByProvider("slack_channel");
115
+ const errConnected = !!(errConn && errConn.status === "active");
116
116
  return {
117
117
  success: false,
118
- hasBotToken: storedBotToken,
119
- hasAppToken: storedAppToken,
120
- connected: storedBotToken && storedAppToken,
118
+ hasBotToken: errConnected,
119
+ hasAppToken: errConnected,
120
+ connected: errConnected,
121
121
  error: `Failed to validate bot token: ${message}`,
122
122
  };
123
123
  }
@@ -127,17 +127,13 @@ export async function setSlackChannelConfig(
127
127
  botToken,
128
128
  );
129
129
  if (!stored) {
130
- const storedBotToken = !!getSecureKey(
131
- credentialKey("slack_channel", "bot_token"),
132
- );
133
- const storedAppToken = !!getSecureKey(
134
- credentialKey("slack_channel", "app_token"),
135
- );
130
+ const errConn = getConnectionByProvider("slack_channel");
131
+ const errConnected = !!(errConn && errConn.status === "active");
136
132
  return {
137
133
  success: false,
138
- hasBotToken: storedBotToken,
139
- hasAppToken: storedAppToken,
140
- connected: storedBotToken && storedAppToken,
134
+ hasBotToken: errConnected,
135
+ hasAppToken: errConnected,
136
+ connected: errConnected,
141
137
  error: "Failed to store bot token in secure storage",
142
138
  };
143
139
  }
@@ -165,17 +161,13 @@ export async function setSlackChannelConfig(
165
161
  // Validate and store app token
166
162
  if (appToken) {
167
163
  if (!appToken.startsWith("xapp-")) {
168
- const storedBotToken = !!getSecureKey(
169
- credentialKey("slack_channel", "bot_token"),
170
- );
171
- const storedAppToken = !!getSecureKey(
172
- credentialKey("slack_channel", "app_token"),
173
- );
164
+ const errConn = getConnectionByProvider("slack_channel");
165
+ const errConnected = !!(errConn && errConn.status === "active");
174
166
  return {
175
167
  success: false,
176
- hasBotToken: storedBotToken,
177
- hasAppToken: storedAppToken,
178
- connected: storedBotToken && storedAppToken,
168
+ hasBotToken: errConnected,
169
+ hasAppToken: errConnected,
170
+ connected: errConnected,
179
171
  error: 'Invalid app token: must start with "xapp-"',
180
172
  };
181
173
  }
@@ -185,17 +177,13 @@ export async function setSlackChannelConfig(
185
177
  appToken,
186
178
  );
187
179
  if (!stored) {
188
- const storedBotToken = !!getSecureKey(
189
- credentialKey("slack_channel", "bot_token"),
190
- );
191
- const storedAppToken = !!getSecureKey(
192
- credentialKey("slack_channel", "app_token"),
193
- );
180
+ const errConn = getConnectionByProvider("slack_channel");
181
+ const errConnected = !!(errConn && errConn.status === "active");
194
182
  return {
195
183
  success: false,
196
- hasBotToken: storedBotToken,
197
- hasAppToken: storedAppToken,
198
- connected: storedBotToken && storedAppToken,
184
+ hasBotToken: errConnected,
185
+ hasAppToken: errConnected,
186
+ connected: errConnected,
199
187
  error: "Failed to store app token in secure storage",
200
188
  };
201
189
  }
@@ -218,6 +206,17 @@ export async function setSlackChannelConfig(
218
206
  "App token stored but bot token is missing — connection incomplete.";
219
207
  }
220
208
 
209
+ // Sync oauth_connection record so getConnectionByProvider("slack_channel")
210
+ // reflects the current credential state.
211
+ if (hasBotToken && hasAppToken) {
212
+ const accountInfo = metadata.teamName
213
+ ? `${metadata.teamName}${metadata.botUsername ? ` (@${metadata.botUsername})` : ""}`
214
+ : undefined;
215
+ await ensureManualTokenConnection("slack_channel", accountInfo);
216
+ } else {
217
+ removeManualTokenConnection("slack_channel");
218
+ }
219
+
221
220
  return {
222
221
  success: true,
223
222
  hasBotToken,
@@ -237,6 +236,7 @@ export async function clearSlackChannelConfig(): Promise<SlackChannelConfigResul
237
236
  );
238
237
 
239
238
  if (r1 === "error" || r2 === "error") {
239
+ // Check each key individually so partial deletions report accurate status.
240
240
  const hasBotToken = !!getSecureKey(
241
241
  credentialKey("slack_channel", "bot_token"),
242
242
  );
@@ -255,6 +255,9 @@ export async function clearSlackChannelConfig(): Promise<SlackChannelConfigResul
255
255
  deleteCredentialMetadata("slack_channel", "bot_token");
256
256
  deleteCredentialMetadata("slack_channel", "app_token");
257
257
 
258
+ // Remove the oauth_connection row so getConnectionByProvider returns undefined.
259
+ removeManualTokenConnection("slack_channel");
260
+
258
261
  const raw = loadRawConfig();
259
262
  setNestedValue(raw, "slack.teamId", "");
260
263
  setNestedValue(raw, "slack.teamName", "");
@@ -8,6 +8,11 @@ import {
8
8
  registerCallbackRoute,
9
9
  shouldUsePlatformCallbacks,
10
10
  } from "../../inbound/platform-callback-registration.js";
11
+ import {
12
+ ensureManualTokenConnection,
13
+ removeManualTokenConnection,
14
+ } from "../../oauth/manual-token-connection.js";
15
+ import { getConnectionByProvider } from "../../oauth/oauth-store.js";
11
16
  import { credentialKey } from "../../security/credential-key.js";
12
17
  import {
13
18
  deleteSecureKeyAsync,
@@ -65,12 +70,14 @@ export function getTelegramConfig(): TelegramConfigResult {
65
70
  const hasWebhookSecret = !!getSecureKey(
66
71
  credentialKey("telegram", "webhook_secret"),
67
72
  );
73
+ const conn = getConnectionByProvider("telegram");
74
+ const connected = !!(conn && conn.status === "active");
68
75
  const botUsername = getTelegramBotUsername();
69
76
  return {
70
77
  success: true,
71
78
  hasBotToken,
72
79
  botUsername,
73
- connected: hasBotToken && hasWebhookSecret,
80
+ connected: connected && hasBotToken && hasWebhookSecret,
74
81
  hasWebhookSecret,
75
82
  };
76
83
  }
@@ -200,6 +207,13 @@ export async function setTelegramConfig(
200
207
  upsertCredentialMetadata("telegram", "webhook_secret", {});
201
208
  }
202
209
 
210
+ // Sync oauth_connection record so getConnectionByProvider("telegram")
211
+ // reflects the current credential state.
212
+ await ensureManualTokenConnection(
213
+ "telegram",
214
+ botUsername ? `@${botUsername}` : undefined,
215
+ );
216
+
203
217
  const result: TelegramConfigResult = {
204
218
  success: true,
205
219
  hasBotToken: true,
@@ -245,6 +259,7 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
245
259
  );
246
260
 
247
261
  if (r1 === "error" || r2 === "error") {
262
+ // Check each key individually so partial deletions report accurate status.
248
263
  const hasBotToken = !!getSecureKey(credentialKey("telegram", "bot_token"));
249
264
  const hasWebhookSecret = !!getSecureKey(
250
265
  credentialKey("telegram", "webhook_secret"),
@@ -261,6 +276,9 @@ export async function clearTelegramConfig(): Promise<TelegramConfigResult> {
261
276
  deleteCredentialMetadata("telegram", "bot_token");
262
277
  deleteCredentialMetadata("telegram", "webhook_secret");
263
278
 
279
+ // Remove the oauth_connection row so getConnectionByProvider returns undefined.
280
+ removeManualTokenConnection("telegram");
281
+
264
282
  // Clear bot username from config so getTelegramBotUsername() doesn't
265
283
  // return a stale value after disconnect.
266
284
  const raw = loadRawConfig();
@@ -306,38 +324,36 @@ export async function setTelegramCommands(
306
324
  );
307
325
  if (!res.ok) {
308
326
  const body = await res.text();
327
+ const cmdConn = getConnectionByProvider("telegram");
328
+ const cmdConnected = !!(cmdConn && cmdConn.status === "active");
309
329
  return {
310
330
  success: false,
311
331
  hasBotToken: true,
312
- connected: !!getSecureKey(credentialKey("telegram", "webhook_secret")),
313
- hasWebhookSecret: !!getSecureKey(
314
- credentialKey("telegram", "webhook_secret"),
315
- ),
332
+ connected: cmdConnected,
333
+ hasWebhookSecret: cmdConnected,
316
334
  error: `Failed to set bot commands: ${body}`,
317
335
  };
318
336
  }
319
337
  } catch (err) {
320
338
  const message = summarizeTelegramError(err);
339
+ const cmdConn = getConnectionByProvider("telegram");
340
+ const cmdConnected = !!(cmdConn && cmdConn.status === "active");
321
341
  return {
322
342
  success: false,
323
343
  hasBotToken: true,
324
- connected: !!getSecureKey(credentialKey("telegram", "webhook_secret")),
325
- hasWebhookSecret: !!getSecureKey(
326
- credentialKey("telegram", "webhook_secret"),
327
- ),
344
+ connected: cmdConnected,
345
+ hasWebhookSecret: cmdConnected,
328
346
  error: `Failed to set bot commands: ${message}`,
329
347
  };
330
348
  }
331
349
 
332
- const hasBotToken = !!getSecureKey(credentialKey("telegram", "bot_token"));
333
- const hasWebhookSecret = !!getSecureKey(
334
- credentialKey("telegram", "webhook_secret"),
335
- );
350
+ const cmdConn = getConnectionByProvider("telegram");
351
+ const cmdConnected = !!(cmdConn && cmdConn.status === "active");
336
352
  return {
337
353
  success: true,
338
- hasBotToken,
339
- connected: hasBotToken && hasWebhookSecret,
340
- hasWebhookSecret,
354
+ hasBotToken: true,
355
+ connected: cmdConnected,
356
+ hasWebhookSecret: cmdConnected,
341
357
  commandsRegistered: resolvedCommands.map((c) => c.command),
342
358
  };
343
359
  }