vellum 0.2.7 → 0.2.8

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 (42) hide show
  1. package/bun.lock +2 -2
  2. package/package.json +2 -2
  3. package/src/__tests__/asset-materialize-tool.test.ts +2 -2
  4. package/src/__tests__/checker.test.ts +104 -0
  5. package/src/__tests__/gateway-only-enforcement.test.ts +458 -0
  6. package/src/__tests__/ipc-snapshot.test.ts +11 -0
  7. package/src/__tests__/oauth-callback-registry.test.ts +85 -0
  8. package/src/__tests__/oauth2-gateway-transport.test.ts +298 -0
  9. package/src/__tests__/provider-commit-message-generator.test.ts +51 -12
  10. package/src/__tests__/public-ingress-urls.test.ts +206 -0
  11. package/src/__tests__/tool-executor.test.ts +88 -0
  12. package/src/__tests__/turn-commit.test.ts +64 -0
  13. package/src/calls/twilio-config.ts +17 -1
  14. package/src/calls/twilio-routes.ts +10 -2
  15. package/src/calls/twilio-webhook-urls.ts +18 -21
  16. package/src/config/defaults.ts +4 -0
  17. package/src/config/schema.ts +30 -2
  18. package/src/config/system-prompt.ts +1 -1
  19. package/src/config/types.ts +1 -0
  20. package/src/daemon/computer-use-session.ts +2 -1
  21. package/src/daemon/handlers/config.ts +51 -2
  22. package/src/daemon/handlers/sessions.ts +2 -2
  23. package/src/daemon/handlers/work-items.ts +1 -1
  24. package/src/daemon/ipc-contract-inventory.json +4 -0
  25. package/src/daemon/ipc-contract.ts +16 -1
  26. package/src/daemon/session-tool-setup.ts +7 -0
  27. package/src/inbound/public-ingress-urls.ts +106 -0
  28. package/src/memory/attachments-store.ts +0 -1
  29. package/src/memory/channel-delivery-store.ts +0 -1
  30. package/src/memory/conversation-key-store.ts +0 -1
  31. package/src/memory/db.ts +346 -149
  32. package/src/memory/runs-store.ts +0 -3
  33. package/src/memory/schema.ts +0 -4
  34. package/src/runtime/http-server.ts +84 -2
  35. package/src/security/oauth-callback-registry.ts +56 -0
  36. package/src/security/oauth2.ts +174 -58
  37. package/src/swarm/backend-claude-code.ts +1 -1
  38. package/src/tools/assets/search.ts +1 -36
  39. package/src/tools/claude-code/claude-code.ts +3 -3
  40. package/src/tools/tasks/work-item-list.ts +16 -2
  41. package/src/workspace/provider-commit-message-generator.ts +39 -23
  42. package/src/workspace/turn-commit.ts +6 -2
@@ -10,9 +10,10 @@ export type CommitMessageSource = 'llm' | 'deterministic';
10
10
  export type LLMFallbackReason =
11
11
  | 'disabled'
12
12
  | 'missing_provider_api_key'
13
- | 'provider_not_initialized'
14
13
  | 'breaker_open'
15
14
  | 'insufficient_budget'
15
+ | 'missing_fast_model'
16
+ | 'provider_not_initialized'
16
17
  | 'timeout'
17
18
  | 'provider_error'
18
19
  | 'invalid_output';
@@ -103,17 +104,25 @@ export class ProviderCommitMessageGenerator {
103
104
  const config = getConfig();
104
105
  const llmConfig = config.workspaceGit.commitMessageLLM;
105
106
 
107
+ // ── Fallback check order (canonical) ──────────────────────────────
108
+ // 1. disabled
109
+ // 2. missing_provider_api_key (except keyless providers like ollama)
110
+ // 3. breaker_open
111
+ // 4. insufficient_budget
112
+ // 5. missing_fast_model
113
+ // 6. provider_not_initialized
114
+ // 7. call provider → timeout / provider_error / invalid_output
115
+ // ──────────────────────────────────────────────────────────────────
116
+
106
117
  // Step 1: Feature gate
107
118
  if (!llmConfig.enabled) {
108
119
  return buildDeterministicResult(context, 'disabled');
109
120
  }
110
-
111
- // Step 2: Provider gate
112
121
  if (!llmConfig.useConfiguredProvider) {
113
122
  return buildDeterministicResult(context, 'disabled');
114
123
  }
115
124
 
116
- // Step 2.5: API key preflight (skip for providers that run without a key)
125
+ // Step 2: API key preflight (skip for providers that run without a key)
117
126
  if (!KEYLESS_PROVIDERS.has(config.provider)) {
118
127
  const providerApiKey = config.apiKeys[config.provider];
119
128
  if (!providerApiKey || providerApiKey === '') {
@@ -143,7 +152,19 @@ export class ProviderCommitMessageGenerator {
143
152
  }
144
153
  }
145
154
 
146
- // Step 5: Call the provider
155
+ // Step 5: Fast model preflight — resolve before any provider call
156
+ const fastModel = llmConfig.providerFastModelOverrides[config.provider]
157
+ ?? PROVIDER_DEFAULT_FAST_MODELS[config.provider];
158
+
159
+ if (!fastModel) {
160
+ log.debug(
161
+ { provider: config.provider },
162
+ 'No fast model resolvable for provider; falling back to deterministic',
163
+ );
164
+ return buildDeterministicResult(context, 'missing_fast_model');
165
+ }
166
+
167
+ // Step 6 + 7: Call the provider
147
168
  try {
148
169
  const { getProvider } = await import('../providers/registry.js');
149
170
 
@@ -179,14 +200,6 @@ export class ProviderCommitMessageGenerator {
179
200
  },
180
201
  ];
181
202
 
182
- // Resolve fast model
183
- const fastModel = llmConfig.providerFastModelOverrides[config.provider]
184
- ?? PROVIDER_DEFAULT_FAST_MODELS[config.provider];
185
- if (!fastModel) {
186
- log.debug({ provider: config.provider }, 'No default fast model for provider; falling back to deterministic');
187
- return buildDeterministicResult(context, 'provider_error');
188
- }
189
-
190
203
  // AbortController with timeout
191
204
  const ac = new AbortController();
192
205
  const timer = setTimeout(() => ac.abort(), llmConfig.timeoutMs);
@@ -199,7 +212,11 @@ export class ProviderCommitMessageGenerator {
199
212
  SYSTEM_PROMPT,
200
213
  {
201
214
  signal: ac.signal,
202
- config: { model: fastModel, max_tokens: llmConfig.maxTokens, temperature: llmConfig.temperature },
215
+ config: {
216
+ model: fastModel,
217
+ max_tokens: llmConfig.maxTokens,
218
+ temperature: llmConfig.temperature,
219
+ },
203
220
  },
204
221
  );
205
222
  } catch (err: unknown) {
@@ -230,21 +247,20 @@ export class ProviderCommitMessageGenerator {
230
247
  return buildDeterministicResult(context, 'invalid_output');
231
248
  }
232
249
 
233
- // Validate single-line subject: first line must be <= 72 chars
234
- const firstLine = text.split('\n')[0];
235
- if (firstLine.length > 72) {
250
+ // Cap subject line to 72 chars deterministically (no fallback, no breaker failure)
251
+ const lines = text.split('\n');
252
+ if (lines[0].length > 72) {
236
253
  log.debug(
237
- { subjectLength: firstLine.length },
238
- 'LLM subject line too long; falling back to deterministic',
254
+ { originalLength: lines[0].length },
255
+ 'Capping LLM subject line to 72 chars',
239
256
  );
240
- this.recordFailure();
241
- return buildDeterministicResult(context, 'invalid_output');
257
+ lines[0] = lines[0].slice(0, 72);
242
258
  }
259
+ const finalMessage = lines.join('\n');
243
260
 
244
261
  this.recordSuccess();
245
- return { message: text, source: 'llm' };
262
+ return { message: finalMessage, source: 'llm' };
246
263
  } catch (err: unknown) {
247
- // Step 6: Any error -> deterministic fallback
248
264
  log.warn(
249
265
  { err: err instanceof Error ? err.message : String(err) },
250
266
  'Commit message LLM provider error; falling back to deterministic',
@@ -72,10 +72,14 @@ export async function commitTurnChanges(
72
72
  if (!provider) {
73
73
  // Guard: skip pre-check if deadline already elapsed to avoid unnecessary mutex contention
74
74
  let preClean = false;
75
+ let candidateChangedFiles: string[] = [];
75
76
  if (!deadlineMs || Date.now() < deadlineMs) {
76
77
  try {
77
78
  const preStatus = await gitService.getStatus();
78
79
  preClean = preStatus.clean;
80
+ if (!preClean) {
81
+ candidateChangedFiles = [...new Set([...preStatus.staged, ...preStatus.modified, ...preStatus.untracked])];
82
+ }
79
83
  } catch {
80
84
  // If we can't determine status, assume dirty so we don't skip the commit
81
85
  }
@@ -90,10 +94,10 @@ export async function commitTurnChanges(
90
94
  trigger: 'turn',
91
95
  sessionId,
92
96
  turnNumber,
93
- changedFiles: [], // File list unavailable outside the git mutex; generator handles empty arrays
97
+ changedFiles: candidateChangedFiles,
94
98
  timestampMs: Date.now(),
95
99
  },
96
- { deadlineMs, changedFiles: [] },
100
+ { deadlineMs, changedFiles: candidateChangedFiles },
97
101
  );
98
102
  commitMessageSource = result.source;
99
103
  llmFallbackReason = result.reason;