@vellumai/assistant 0.5.4 → 0.5.6

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 (151) hide show
  1. package/Dockerfile +17 -27
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
  3. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-service.test.ts +113 -0
  6. package/src/__tests__/config-schema.test.ts +2 -2
  7. package/src/__tests__/context-window-manager.test.ts +78 -0
  8. package/src/__tests__/conversation-title-service.test.ts +30 -1
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  10. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  11. package/src/__tests__/memory-regressions.test.ts +8 -30
  12. package/src/__tests__/openai-whisper.test.ts +93 -0
  13. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  14. package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  16. package/src/__tests__/tool-executor.test.ts +4 -0
  17. package/src/__tests__/volume-security-guard.test.ts +155 -0
  18. package/src/cli/commands/conversations.ts +0 -18
  19. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
  20. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
  21. package/src/config/env-registry.ts +9 -0
  22. package/src/config/env.ts +8 -2
  23. package/src/config/feature-flag-registry.json +8 -8
  24. package/src/config/schema.ts +0 -12
  25. package/src/config/schemas/memory.ts +0 -4
  26. package/src/config/schemas/platform.ts +1 -1
  27. package/src/config/schemas/security.ts +4 -0
  28. package/src/context/window-manager.ts +53 -2
  29. package/src/credential-execution/managed-catalog.ts +5 -15
  30. package/src/daemon/conversation-agent-loop.ts +0 -60
  31. package/src/daemon/conversation-memory.ts +0 -117
  32. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  33. package/src/daemon/daemon-control.ts +7 -0
  34. package/src/daemon/handlers/conversations.ts +0 -11
  35. package/src/daemon/lifecycle.ts +10 -47
  36. package/src/daemon/providers-setup.ts +2 -1
  37. package/src/followups/followup-store.ts +5 -2
  38. package/src/hooks/manager.ts +7 -0
  39. package/src/instrument.ts +33 -1
  40. package/src/memory/conversation-crud.ts +0 -236
  41. package/src/memory/conversation-title-service.ts +26 -10
  42. package/src/memory/db-init.ts +5 -13
  43. package/src/memory/embedding-local.ts +11 -5
  44. package/src/memory/indexer.ts +15 -106
  45. package/src/memory/job-handlers/conversation-starters.ts +24 -36
  46. package/src/memory/job-handlers/embedding.ts +0 -79
  47. package/src/memory/job-utils.ts +1 -1
  48. package/src/memory/jobs-store.ts +0 -8
  49. package/src/memory/jobs-worker.ts +0 -20
  50. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  51. package/src/memory/migrations/index.ts +1 -3
  52. package/src/memory/qdrant-client.ts +4 -6
  53. package/src/memory/schema/conversations.ts +0 -3
  54. package/src/memory/schema/index.ts +0 -2
  55. package/src/messaging/draft-store.ts +2 -2
  56. package/src/messaging/provider.ts +9 -0
  57. package/src/messaging/providers/slack/adapter.ts +29 -2
  58. package/src/oauth/connection-resolver.test.ts +22 -18
  59. package/src/oauth/connection-resolver.ts +92 -7
  60. package/src/oauth/platform-connection.test.ts +78 -69
  61. package/src/oauth/platform-connection.ts +12 -19
  62. package/src/permissions/defaults.ts +3 -3
  63. package/src/permissions/trust-client.ts +332 -0
  64. package/src/permissions/trust-store-interface.ts +105 -0
  65. package/src/permissions/trust-store.ts +531 -39
  66. package/src/platform/client.test.ts +148 -0
  67. package/src/platform/client.ts +71 -0
  68. package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
  69. package/src/providers/speech-to-text/openai-whisper.ts +68 -0
  70. package/src/providers/speech-to-text/resolve.ts +9 -0
  71. package/src/providers/speech-to-text/types.ts +17 -0
  72. package/src/runtime/auth/route-policy.ts +14 -0
  73. package/src/runtime/auth/token-service.ts +133 -0
  74. package/src/runtime/http-server.ts +4 -2
  75. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  76. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  77. package/src/runtime/routes/conversation-routes.ts +2 -1
  78. package/src/runtime/routes/inbound-message-handler.ts +27 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
  80. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
  81. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
  82. package/src/runtime/routes/log-export-routes.ts +1 -0
  83. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  84. package/src/runtime/routes/memory-item-routes.ts +124 -2
  85. package/src/runtime/routes/secret-routes.ts +4 -1
  86. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  87. package/src/schedule/schedule-store.ts +0 -21
  88. package/src/security/ces-credential-client.ts +173 -0
  89. package/src/security/secure-keys.ts +65 -22
  90. package/src/signals/bash.ts +3 -0
  91. package/src/signals/cancel.ts +3 -0
  92. package/src/signals/confirm.ts +3 -0
  93. package/src/signals/conversation-undo.ts +3 -0
  94. package/src/signals/event-stream.ts +7 -0
  95. package/src/signals/shotgun.ts +3 -0
  96. package/src/signals/trust-rule.ts +3 -0
  97. package/src/skills/inline-command-render.ts +5 -1
  98. package/src/skills/inline-command-runner.ts +30 -2
  99. package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
  100. package/src/telemetry/usage-telemetry-reporter.ts +21 -19
  101. package/src/tools/memory/handlers.ts +1 -129
  102. package/src/tools/permission-checker.ts +18 -0
  103. package/src/tools/skills/load.ts +9 -2
  104. package/src/util/device-id.ts +70 -7
  105. package/src/util/logger.ts +35 -9
  106. package/src/util/platform.ts +29 -5
  107. package/src/util/xml.ts +8 -0
  108. package/src/workspace/heartbeat-service.ts +5 -24
  109. package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
  110. package/src/workspace/migrations/registry.ts +2 -0
  111. package/src/__tests__/archive-recall.test.ts +0 -560
  112. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  113. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  114. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  115. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  116. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  117. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  118. package/src/__tests__/memory-brief-time.test.ts +0 -285
  119. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  120. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  121. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  122. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  123. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  124. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  125. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  126. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  127. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  128. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  129. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  130. package/src/__tests__/memory-reducer.test.ts +0 -704
  131. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  132. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  133. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  134. package/src/config/schemas/memory-simplified.ts +0 -101
  135. package/src/memory/archive-recall.ts +0 -516
  136. package/src/memory/archive-store.ts +0 -400
  137. package/src/memory/brief-formatting.ts +0 -33
  138. package/src/memory/brief-open-loops.ts +0 -266
  139. package/src/memory/brief-time.ts +0 -162
  140. package/src/memory/brief.ts +0 -75
  141. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  142. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  143. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  144. package/src/memory/migrations/186-memory-archive.ts +0 -109
  145. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  146. package/src/memory/reducer-scheduler.ts +0 -242
  147. package/src/memory/reducer-store.ts +0 -271
  148. package/src/memory/reducer-types.ts +0 -106
  149. package/src/memory/reducer.ts +0 -467
  150. package/src/memory/schema/memory-archive.ts +0 -121
  151. package/src/memory/schema/memory-brief.ts +0 -55
@@ -0,0 +1,155 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ /**
6
+ * Guard test: assistant source code must not directly access files in the
7
+ * `protected/` directory (`trust.json`, `keys.enc`, `store.key`,
8
+ * `actor-token-signing-key`). In containerized (Docker) mode these files
9
+ * live outside the assistant's data volume and are managed by the gateway.
10
+ *
11
+ * All access must go through the appropriate abstraction layer:
12
+ * - Trust rules: trust-store.ts / trust-client.ts (file vs gateway backend)
13
+ * - Credentials: encrypted-store.ts / ces-credential-client.ts
14
+ * - Signing keys: secure-keys.ts / credential-backend.ts
15
+ *
16
+ * Only the abstraction-layer files themselves (and tests) are allowed to
17
+ * reference the raw file paths / helper functions.
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Allowed files — abstraction layers that legitimately access protected/ files
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const ALLOWED_FILES = new Set([
25
+ // Trust store backends
26
+ "assistant/src/permissions/trust-store.ts",
27
+ "assistant/src/permissions/trust-client.ts",
28
+ "assistant/src/permissions/trust-store-interface.ts",
29
+ // Credential / encrypted store backends
30
+ "assistant/src/security/encrypted-store.ts",
31
+ "assistant/src/security/secure-keys.ts",
32
+ "assistant/src/security/credential-backend.ts",
33
+ "assistant/src/security/ces-credential-client.ts",
34
+ // Token service owns the signing key lifecycle
35
+ "assistant/src/runtime/auth/token-service.ts",
36
+ // CLI commands that run outside Docker (doctor diagnostics, trust management)
37
+ "assistant/src/cli/commands/doctor.ts",
38
+ "assistant/src/cli/commands/trust.ts",
39
+ // Auth middleware documentation comment (not a file access)
40
+ "assistant/src/runtime/auth/middleware.ts",
41
+ ]);
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Patterns that indicate direct access to protected directory files
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Each entry is a `git grep -E` pattern and a human-readable description
49
+ * for the error message.
50
+ */
51
+ const GUARDED_PATTERNS: Array<{ pattern: string; description: string }> = [
52
+ {
53
+ pattern: "protected/trust\\.json",
54
+ description: "direct reference to protected/trust.json",
55
+ },
56
+ {
57
+ pattern: "protected/keys\\.enc",
58
+ description: "direct reference to protected/keys.enc",
59
+ },
60
+ {
61
+ pattern: "protected/store\\.key",
62
+ description: "direct reference to protected/store.key",
63
+ },
64
+ {
65
+ pattern: "actor-token-signing-key",
66
+ description: "direct reference to actor-token-signing-key file",
67
+ },
68
+ {
69
+ pattern: "\\bgetTrustPath\\b",
70
+ description: "use of getTrustPath() (trust-store internal)",
71
+ },
72
+ {
73
+ pattern: "\\bgetStoreKeyPath\\b",
74
+ description: "use of getStoreKeyPath() (encrypted-store internal)",
75
+ },
76
+ ];
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Helpers
80
+ // ---------------------------------------------------------------------------
81
+
82
+ function getRepoRoot(): string {
83
+ return join(process.cwd(), "..");
84
+ }
85
+
86
+ function isTestFile(filePath: string): boolean {
87
+ return (
88
+ filePath.includes("/__tests__/") ||
89
+ filePath.endsWith(".test.ts") ||
90
+ filePath.endsWith(".test.js") ||
91
+ filePath.endsWith(".spec.ts") ||
92
+ filePath.endsWith(".spec.js")
93
+ );
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Tests
98
+ // ---------------------------------------------------------------------------
99
+
100
+ describe("volume security: protected directory access guard", () => {
101
+ for (const { pattern, description } of GUARDED_PATTERNS) {
102
+ test(`no ${description} outside allowed files`, () => {
103
+ const repoRoot = getRepoRoot();
104
+
105
+ let grepOutput = "";
106
+ try {
107
+ grepOutput = execFileSync(
108
+ "git",
109
+ [
110
+ "grep",
111
+ "-lE",
112
+ pattern,
113
+ "--",
114
+ "assistant/src/**/*.ts",
115
+ "assistant/src/*.ts",
116
+ ],
117
+ { encoding: "utf-8", cwd: repoRoot },
118
+ ).trim();
119
+ } catch (err) {
120
+ // Exit code 1 means no matches — happy path
121
+ if ((err as { status?: number }).status === 1) {
122
+ return;
123
+ }
124
+ throw err;
125
+ }
126
+
127
+ const files = grepOutput.split("\n").filter((f) => f.length > 0);
128
+ const violations = files.filter(
129
+ (f) => !isTestFile(f) && !ALLOWED_FILES.has(f),
130
+ );
131
+
132
+ if (violations.length > 0) {
133
+ const message = [
134
+ `Found assistant source files with ${description}.`,
135
+ "",
136
+ "In containerized (Docker) mode, the protected/ directory is not",
137
+ "accessible to the assistant. All access to protected files must go",
138
+ "through the abstraction layers:",
139
+ " - Trust rules: trust-store.ts / trust-client.ts",
140
+ " - Credentials: encrypted-store.ts / ces-credential-client.ts",
141
+ " - Signing keys: secure-keys.ts / credential-backend.ts",
142
+ "",
143
+ "If this file is a new abstraction backend, add it to ALLOWED_FILES",
144
+ "in this guard test. Otherwise, use the appropriate abstraction layer",
145
+ "or gate the access behind !getIsContainerized().",
146
+ "",
147
+ "Violations:",
148
+ ...violations.map((f) => ` - ${f}`),
149
+ ].join("\n");
150
+
151
+ expect(violations, message).toEqual([]);
152
+ }
153
+ });
154
+ }
155
+ });
@@ -375,24 +375,6 @@ Examples:
375
375
  targetId: summaryId,
376
376
  });
377
377
  }
378
- for (const obsId of result.deletedObservationIds) {
379
- enqueueMemoryJob("delete_qdrant_vectors", {
380
- targetType: "observation",
381
- targetId: obsId,
382
- });
383
- }
384
- for (const chunkId of result.deletedChunkIds) {
385
- enqueueMemoryJob("delete_qdrant_vectors", {
386
- targetType: "chunk",
387
- targetId: chunkId,
388
- });
389
- }
390
- for (const episodeId of result.deletedEpisodeIds) {
391
- enqueueMemoryJob("delete_qdrant_vectors", {
392
- targetType: "episode",
393
- targetId: episodeId,
394
- });
395
- }
396
378
 
397
379
  log.info(
398
380
  `Wiped conversation "${conversation.title ?? "Untitled"}". ` +
@@ -136,6 +136,7 @@ export async function getProviderConnection(
136
136
  provider: MessagingProvider,
137
137
  account?: string,
138
138
  ): Promise<OAuthConnection | string> {
139
+ if (provider.resolveConnection) return provider.resolveConnection(account);
139
140
  if (await provider.isConnected?.()) return "";
140
141
  return resolveOAuthConnection(provider.credentialService, { account });
141
142
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { tmpdir } from "node:os";
11
11
  import { extname, join } from "node:path";
12
12
 
13
+ import { OpenAIWhisperProvider } from "../../../../providers/speech-to-text/openai-whisper.js";
13
14
  import { getProviderKeyAsync } from "../../../../security/secure-keys.js";
14
15
  import type {
15
16
  ToolContext,
@@ -168,12 +169,19 @@ async function transcribeViaApi(
168
169
  apiKey: string,
169
170
  context: ToolContext,
170
171
  ): Promise<string> {
172
+ const provider = new OpenAIWhisperProvider(apiKey);
171
173
  const duration = await getAudioDuration(audioPath);
172
174
  const fileSize = Bun.file(audioPath).size;
173
175
 
174
176
  // If small enough, send directly
175
177
  if (fileSize <= WHISPER_API_MAX_BYTES) {
176
- return await whisperApiRequest(audioPath, apiKey);
178
+ const audioBuffer = await readFile(audioPath);
179
+ const result = await provider.transcribe(
180
+ audioBuffer,
181
+ "audio/wav",
182
+ AbortSignal.timeout(API_REQUEST_TIMEOUT_MS),
183
+ );
184
+ return result.text;
177
185
  }
178
186
 
179
187
  // Split into chunks for large files
@@ -199,8 +207,13 @@ async function transcribeViaApi(
199
207
  for (let i = 0; i < chunks.length; i++) {
200
208
  if (context.signal?.aborted) throw new Error("Cancelled");
201
209
  context.onOutput?.(` Transcribing chunk ${i + 1}/${chunks.length}...\n`);
202
- const text = await whisperApiRequest(chunks[i], apiKey);
203
- if (text) parts.push(text);
210
+ const audioBuffer = await readFile(chunks[i]);
211
+ const result = await provider.transcribe(
212
+ audioBuffer,
213
+ "audio/wav",
214
+ AbortSignal.timeout(API_REQUEST_TIMEOUT_MS),
215
+ );
216
+ if (result.text) parts.push(result.text);
204
217
  }
205
218
 
206
219
  return parts.join(" ");
@@ -213,40 +226,6 @@ async function transcribeViaApi(
213
226
  }
214
227
  }
215
228
 
216
- async function whisperApiRequest(
217
- audioPath: string,
218
- apiKey: string,
219
- ): Promise<string> {
220
- const audioData = await readFile(audioPath);
221
- const formData = new FormData();
222
- formData.append(
223
- "file",
224
- new Blob([audioData], { type: "audio/wav" }),
225
- "audio.wav",
226
- );
227
- formData.append("model", "whisper-1");
228
-
229
- const response = await fetch(
230
- "https://api.openai.com/v1/audio/transcriptions",
231
- {
232
- method: "POST",
233
- headers: { Authorization: `Bearer ${apiKey}` },
234
- body: formData,
235
- signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS),
236
- },
237
- );
238
-
239
- if (!response.ok) {
240
- const body = await response.text().catch(() => "");
241
- throw new Error(
242
- `Whisper API error (${response.status}): ${body.slice(0, 300)}`,
243
- );
244
- }
245
-
246
- const result = (await response.json()) as { text?: string };
247
- return result.text?.trim() ?? "";
248
- }
249
-
250
229
  // ---------------------------------------------------------------------------
251
230
  // Local mode - whisper.cpp
252
231
  // ---------------------------------------------------------------------------
@@ -54,6 +54,15 @@ export function getIsContainerized(): boolean {
54
54
  return flag("IS_CONTAINERIZED");
55
55
  }
56
56
 
57
+ /**
58
+ * WORKSPACE_DIR — string, default: undefined
59
+ * When set, overrides the default workspace directory. Used in containerized
60
+ * deployments where the workspace is a separate volume.
61
+ */
62
+ export function getWorkspaceDirOverride(): string | undefined {
63
+ return str("WORKSPACE_DIR");
64
+ }
65
+
57
66
  // ── Known env var names ──────────────────────────────────────────────────────
58
67
 
59
68
  /**
package/src/config/env.ts CHANGED
@@ -51,9 +51,15 @@ export function getGatewayPort(): number {
51
51
  return int("GATEWAY_PORT", DEFAULT_GATEWAY_PORT);
52
52
  }
53
53
 
54
- /** Resolve the gateway base URL for internal service-to-service calls. */
54
+ /**
55
+ * Resolve the gateway base URL for internal service-to-service calls.
56
+ *
57
+ * In containerized deployments the gateway runs in a separate container,
58
+ * reachable via `GATEWAY_INTERNAL_URL` (e.g. `http://gateway:7822`).
59
+ * Falls back to `http://127.0.0.1:<GATEWAY_PORT>` for local deployments.
60
+ */
55
61
  export function getGatewayInternalBaseUrl(): string {
56
- return `http://127.0.0.1:${getGatewayPort()}`;
62
+ return str("GATEWAY_INTERNAL_URL") ?? `http://127.0.0.1:${getGatewayPort()}`;
57
63
  }
58
64
 
59
65
  // ── Ingress ──────────────────────────────────────────────────────────────────
@@ -25,14 +25,6 @@
25
25
  "description": "Show the Contacts tab in Settings for viewing and managing contacts",
26
26
  "defaultEnabled": true
27
27
  },
28
- {
29
- "id": "custom-inference-provider",
30
- "scope": "macos",
31
- "key": "custom_inference_provider_enabled",
32
- "label": "Custom Inference Provider",
33
- "description": "Allow selecting a specific LLM provider and model for inference in Your Own mode",
34
- "defaultEnabled": false
35
- },
36
28
  {
37
29
  "id": "email-channel",
38
30
  "scope": "assistant",
@@ -288,6 +280,14 @@
288
280
  "label": "Inline Skill Command Expansion",
289
281
  "description": "Enable secure inline skill command expansion via !`command` syntax, with version-pinned approval and sandboxed execution at skill load time",
290
282
  "defaultEnabled": true
283
+ },
284
+ {
285
+ "id": "channel-voice-transcription",
286
+ "scope": "assistant",
287
+ "key": "feature_flags.channel-voice-transcription.enabled",
288
+ "label": "Channel Voice Transcription",
289
+ "description": "Auto-transcribe voice/audio messages received from channels (Telegram, WhatsApp) before processing",
290
+ "defaultEnabled": true
291
291
  }
292
292
  ]
293
293
  }
@@ -106,18 +106,6 @@ export {
106
106
  MemoryDynamicBudgetConfigSchema,
107
107
  MemoryRetrievalConfigSchema,
108
108
  } from "./schemas/memory-retrieval.js";
109
- export type {
110
- MemorySimplifiedArchiveRecallConfig,
111
- MemorySimplifiedBriefConfig,
112
- MemorySimplifiedConfig,
113
- MemorySimplifiedReducerConfig,
114
- } from "./schemas/memory-simplified.js";
115
- export {
116
- MemorySimplifiedArchiveRecallConfigSchema,
117
- MemorySimplifiedBriefConfigSchema,
118
- MemorySimplifiedConfigSchema,
119
- MemorySimplifiedReducerConfigSchema,
120
- } from "./schemas/memory-simplified.js";
121
109
  export type {
122
110
  MemoryEmbeddingsConfig,
123
111
  MemorySegmentationConfig,
@@ -10,7 +10,6 @@ import {
10
10
  MemorySummarizationConfigSchema,
11
11
  } from "./memory-processing.js";
12
12
  import { MemoryRetrievalConfigSchema } from "./memory-retrieval.js";
13
- import { MemorySimplifiedConfigSchema } from "./memory-simplified.js";
14
13
  import {
15
14
  MemoryEmbeddingsConfigSchema,
16
15
  MemorySegmentationConfigSchema,
@@ -46,9 +45,6 @@ export const MemoryConfigSchema = z
46
45
  summarization: MemorySummarizationConfigSchema.default(
47
46
  MemorySummarizationConfigSchema.parse({}),
48
47
  ),
49
- simplified: MemorySimplifiedConfigSchema.default(
50
- MemorySimplifiedConfigSchema.parse({}),
51
- ),
52
48
  })
53
49
  .describe(
54
50
  "Long-term memory system — stores, retrieves, and manages persistent knowledge across conversations",
@@ -43,7 +43,7 @@ export const DaemonConfigSchema = z
43
43
  .number({ error: "daemon.titleGenerationMaxTokens must be a number" })
44
44
  .int("daemon.titleGenerationMaxTokens must be an integer")
45
45
  .positive("daemon.titleGenerationMaxTokens must be a positive integer")
46
- .default(30)
46
+ .default(50)
47
47
  .describe(
48
48
  "Maximum number of tokens for auto-generated conversation titles",
49
49
  ),
@@ -77,6 +77,10 @@ export const PermissionsConfigSchema = z
77
77
  .describe(
78
78
  "Permission mode — 'strict' requires explicit approval for all operations, 'workspace' allows operations within the workspace",
79
79
  ),
80
+ dangerouslySkipPermissions: z
81
+ .boolean({ error: "permissions.dangerouslySkipPermissions must be a boolean" })
82
+ .default(false)
83
+ .describe("Auto-accept all permission prompts without asking"),
80
84
  })
81
85
  .describe("Permission enforcement mode for tool operations");
82
86
 
@@ -538,12 +538,12 @@ export class ContextWindowManager {
538
538
  }
539
539
 
540
540
  const keepTurns = lo;
541
- const keepFromIndex =
541
+ const rawKeepFromIndex =
542
542
  keepTurns === 0
543
543
  ? messages.length
544
544
  : (userTurnStarts[userTurnStarts.length - keepTurns] ??
545
545
  messages.length);
546
-
546
+ const keepFromIndex = adjustForToolPairs(messages, rawKeepFromIndex);
547
547
  return { keepFromIndex, keepTurns };
548
548
  }
549
549
 
@@ -703,6 +703,57 @@ function isToolResultOnly(message: Message): boolean {
703
703
  );
704
704
  }
705
705
 
706
+ /**
707
+ * Walk the keep boundary backward to ensure tool_use/tool_result pairs are
708
+ * never split across the compaction boundary. If the first kept message is
709
+ * a user message containing tool_result blocks whose matching tool_use blocks
710
+ * live in the preceding (compacted-away) assistant message, include that
711
+ * assistant message in the kept set.
712
+ */
713
+ function adjustForToolPairs(
714
+ messages: Message[],
715
+ keepFromIndex: number,
716
+ ): number {
717
+ let idx = keepFromIndex;
718
+ while (idx > 0) {
719
+ const msg = messages[idx];
720
+ if (!msg || msg.role !== "user") break;
721
+
722
+ // Collect tool_use_ids referenced by tool_results in this user message
723
+ const referencedIds = new Set<string>();
724
+ for (const block of msg.content) {
725
+ if ((block.type === "tool_result" || block.type === "web_search_tool_result") && "tool_use_id" in block) {
726
+ referencedIds.add((block as { tool_use_id: string }).tool_use_id);
727
+ }
728
+ }
729
+ if (referencedIds.size === 0) break;
730
+
731
+ // Check if the preceding assistant message contains matching tool_uses
732
+ const prev = messages[idx - 1];
733
+ if (!prev || prev.role !== "assistant") break;
734
+
735
+ const hasOrphanedPair = prev.content.some(
736
+ (block) =>
737
+ (block.type === "tool_use" || block.type === "server_tool_use") &&
738
+ "id" in block &&
739
+ referencedIds.has((block as { id: string }).id),
740
+ );
741
+ if (!hasOrphanedPair) break;
742
+
743
+ // Include the assistant message
744
+ idx--;
745
+
746
+ // The assistant message may itself be preceded by a tool_result user
747
+ // message that pairs with an even earlier assistant — continue the check
748
+ if (idx > 0 && messages[idx - 1]?.role === "user") {
749
+ idx--;
750
+ } else {
751
+ break;
752
+ }
753
+ }
754
+ return idx;
755
+ }
756
+
706
757
  export function getSummaryFromContextMessage(
707
758
  message: Message | undefined,
708
759
  ): string | null {
@@ -11,8 +11,7 @@
11
11
 
12
12
  import { platformOAuthHandle } from "@vellumai/ces-contracts";
13
13
 
14
- import { getPlatformAssistantId } from "../config/env.js";
15
- import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
14
+ import { VellumPlatformClient } from "../platform/client.js";
16
15
  import { getLogger } from "../util/logger.js";
17
16
 
18
17
  const log = getLogger("managed-catalog");
@@ -79,25 +78,18 @@ export interface FetchManagedCatalogResult {
79
78
  * error message that never contains secret material.
80
79
  */
81
80
  export async function fetchManagedCatalog(): Promise<FetchManagedCatalogResult> {
82
- const ctx = await resolveManagedProxyContext();
81
+ const client = await VellumPlatformClient.create();
83
82
 
84
- if (!ctx.enabled) {
83
+ if (!client || !client.platformAssistantId) {
85
84
  return { ok: true, descriptors: [] };
86
85
  }
87
86
 
88
- const assistantId = getPlatformAssistantId();
89
- if (!assistantId) {
90
- log.warn("PLATFORM_ASSISTANT_ID not set; cannot fetch managed catalog");
91
- return { ok: true, descriptors: [] };
92
- }
93
-
94
- const url = `${ctx.platformBaseUrl}/v1/assistants/${encodeURIComponent(assistantId)}/oauth/managed/catalog/`;
87
+ const path = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/managed/catalog/`;
95
88
 
96
89
  try {
97
- const response = await fetch(url, {
90
+ const response = await client.fetch(path, {
98
91
  method: "GET",
99
92
  headers: {
100
- Authorization: `Api-Key ${ctx.assistantApiKey}`,
101
93
  Accept: "application/json",
102
94
  },
103
95
  });
@@ -139,8 +131,6 @@ export async function fetchManagedCatalog(): Promise<FetchManagedCatalogResult>
139
131
  return { ok: true, descriptors };
140
132
  } catch (err) {
141
133
  const message = err instanceof Error ? err.message : String(err);
142
- // Ensure the error message does not leak secrets — strip any URL params
143
- // that might contain tokens (defensive, since we use Api-Key header).
144
134
  const safeMessage = message.replace(
145
135
  /Api-Key\s+\S+/gi,
146
136
  "Api-Key [REDACTED]",
@@ -33,7 +33,6 @@ import {
33
33
  } from "../instrument.js";
34
34
  import { commitAppTurnChanges } from "../memory/app-git-service.js";
35
35
  import { getApp, listAppFiles, resolveAppDir } from "../memory/app-store.js";
36
- import { insertCompactionEpisode } from "../memory/archive-store.js";
37
36
  import {
38
37
  addMessage,
39
38
  deleteMessageById,
@@ -514,12 +513,6 @@ export async function runAgentLoopImpl(
514
513
  compacted.summaryText,
515
514
  ctx.contextCompactedMessageCount,
516
515
  );
517
- dualWriteCompactionEpisode(
518
- ctx.conversationId,
519
- ctx.memoryPolicy.scopeId,
520
- compacted.summaryText,
521
- compacted.summaryOutputTokens,
522
- );
523
516
  onEvent({
524
517
  type: "context_compacted",
525
518
  previousEstimatedInputTokens: compacted.previousEstimatedInputTokens,
@@ -787,12 +780,6 @@ export async function runAgentLoopImpl(
787
780
  step.compactionResult.summaryText,
788
781
  ctx.contextCompactedMessageCount,
789
782
  );
790
- dualWriteCompactionEpisode(
791
- ctx.conversationId,
792
- ctx.memoryPolicy.scopeId,
793
- step.compactionResult.summaryText,
794
- step.compactionResult.summaryOutputTokens,
795
- );
796
783
  onEvent({
797
784
  type: "context_compacted",
798
785
  previousEstimatedInputTokens:
@@ -977,12 +964,6 @@ export async function runAgentLoopImpl(
977
964
  midLoopCompact.summaryText,
978
965
  ctx.contextCompactedMessageCount,
979
966
  );
980
- dualWriteCompactionEpisode(
981
- ctx.conversationId,
982
- ctx.memoryPolicy.scopeId,
983
- midLoopCompact.summaryText,
984
- midLoopCompact.summaryOutputTokens,
985
- );
986
967
  onEvent({
987
968
  type: "context_compacted",
988
969
  previousEstimatedInputTokens:
@@ -1179,12 +1160,6 @@ export async function runAgentLoopImpl(
1179
1160
  step.compactionResult.summaryText,
1180
1161
  ctx.contextCompactedMessageCount,
1181
1162
  );
1182
- dualWriteCompactionEpisode(
1183
- ctx.conversationId,
1184
- ctx.memoryPolicy.scopeId,
1185
- step.compactionResult.summaryText,
1186
- step.compactionResult.summaryOutputTokens,
1187
- );
1188
1163
  onEvent({
1189
1164
  type: "context_compacted",
1190
1165
  previousEstimatedInputTokens:
@@ -1292,12 +1267,6 @@ export async function runAgentLoopImpl(
1292
1267
  emergencyCompact.summaryText,
1293
1268
  ctx.contextCompactedMessageCount,
1294
1269
  );
1295
- dualWriteCompactionEpisode(
1296
- ctx.conversationId,
1297
- ctx.memoryPolicy.scopeId,
1298
- emergencyCompact.summaryText,
1299
- emergencyCompact.summaryOutputTokens,
1300
- );
1301
1270
  onEvent({
1302
1271
  type: "context_compacted",
1303
1272
  previousEstimatedInputTokens:
@@ -1402,12 +1371,6 @@ export async function runAgentLoopImpl(
1402
1371
  emergencyCompact.summaryText,
1403
1372
  ctx.contextCompactedMessageCount,
1404
1373
  );
1405
- dualWriteCompactionEpisode(
1406
- ctx.conversationId,
1407
- ctx.memoryPolicy.scopeId,
1408
- emergencyCompact.summaryText,
1409
- emergencyCompact.summaryOutputTokens,
1410
- );
1411
1374
  onEvent({
1412
1375
  type: "context_compacted",
1413
1376
  previousEstimatedInputTokens:
@@ -1873,26 +1836,3 @@ function collapseRawResponses(rawResponses?: unknown[]): unknown | undefined {
1873
1836
  if (!rawResponses || rawResponses.length === 0) return undefined;
1874
1837
  return rawResponses.length === 1 ? rawResponses[0] : rawResponses;
1875
1838
  }
1876
-
1877
- /**
1878
- * Dual-write a compaction summary as an archive episode so it becomes
1879
- * searchable via vector recall. Called after each successful compaction
1880
- * that produces a new summary.
1881
- */
1882
- function dualWriteCompactionEpisode(
1883
- conversationId: string,
1884
- scopeId: string,
1885
- summaryText: string,
1886
- summaryOutputTokens: number,
1887
- ): void {
1888
- const now = Date.now();
1889
- insertCompactionEpisode({
1890
- conversationId,
1891
- scopeId,
1892
- title: truncate(summaryText, 120, ""),
1893
- summary: summaryText,
1894
- tokenEstimate: summaryOutputTokens,
1895
- startAt: now,
1896
- endAt: now,
1897
- });
1898
- }