@vellumai/assistant 0.10.6-staging.1 → 0.10.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 (58) hide show
  1. package/openapi.yaml +43 -6
  2. package/package.json +1 -1
  3. package/src/__tests__/attachment-stored-path-annotation.test.ts +151 -0
  4. package/src/__tests__/attachments-store.test.ts +37 -1
  5. package/src/__tests__/attachments.test.ts +53 -0
  6. package/src/__tests__/channel-reply-delivery.test.ts +214 -5
  7. package/src/__tests__/config-loader-backfill.test.ts +16 -36
  8. package/src/__tests__/image-source-path-reinject.test.ts +155 -14
  9. package/src/__tests__/managed-profile-guard.test.ts +2 -0
  10. package/src/__tests__/mtime-cache.test.ts +265 -80
  11. package/src/__tests__/plugin-config-data-migration.test.ts +2 -4
  12. package/src/__tests__/plugin-effective-enabled-set.test.ts +9 -6
  13. package/src/__tests__/workspace-migration-125-repoint-managed-connections-to-vellum.test.ts +140 -0
  14. package/src/agent/attachments.ts +55 -13
  15. package/src/cli/commands/platform/__tests__/credits.test.ts +111 -0
  16. package/src/cli/commands/platform/index.ts +86 -4
  17. package/src/cli/lib/__tests__/confirm-prompt.test.ts +9 -9
  18. package/src/cli/lib/diff-plugin.ts +7 -3
  19. package/src/cli/lib/inspect-plugin.ts +28 -10
  20. package/src/cli/lib/install-from-github.ts +69 -24
  21. package/src/cli/lib/ipc-params.ts +3 -3
  22. package/src/cli/lib/list-installed-plugins.ts +11 -5
  23. package/src/cli/lib/merge-plugin-tree.ts +15 -9
  24. package/src/cli/lib/plugin-fingerprint.ts +0 -6
  25. package/src/cli/lib/publish-plugin.ts +1 -1
  26. package/src/cli/lib/unknown-command.ts +3 -1
  27. package/src/cli/lib/upgrade-plugin.ts +2 -5
  28. package/src/config/__tests__/sync-gated-profiles.test.ts +2 -2
  29. package/src/config/seed-inference-profiles.ts +5 -4
  30. package/src/daemon/conversation-lifecycle.ts +37 -15
  31. package/src/daemon/conversation-messaging.ts +56 -28
  32. package/src/daemon/conversation.ts +2 -2
  33. package/src/heartbeat/__tests__/heartbeat-service.test.ts +6 -0
  34. package/src/hooks/__tests__/hook-live-reload.test.ts +110 -96
  35. package/src/hooks/hook-loader.ts +57 -144
  36. package/src/hooks/registry.ts +13 -9
  37. package/src/persistence/attachments-store.ts +2 -1
  38. package/src/persistence/conversation-crud.ts +24 -0
  39. package/src/plugin-api/types.ts +4 -1
  40. package/src/plugins/mtime-cache.ts +347 -33
  41. package/src/plugins/pipeline.ts +3 -2
  42. package/src/providers/__tests__/inference.test.ts +42 -31
  43. package/src/providers/inference/__tests__/connections-label.test.ts +9 -28
  44. package/src/providers/inference/auth.ts +1 -2
  45. package/src/providers/inference/backfill.ts +7 -5
  46. package/src/providers/inference/connections.ts +54 -30
  47. package/src/runtime/channel-reply-delivery.ts +83 -24
  48. package/src/runtime/channel-retry-sweep.ts +4 -0
  49. package/src/runtime/{slack-no-response.ts → no-response.ts} +26 -6
  50. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +43 -3
  51. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +61 -24
  52. package/src/runtime/routes/conversation-query-routes.ts +18 -1
  53. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -1
  54. package/src/runtime/routes/inference-provider-connection-routes.ts +4 -3
  55. package/src/runtime/routes/platform-routes.ts +82 -1
  56. package/src/runtime/slack-reply-session.ts +1 -1
  57. package/src/workspace/migrations/125-repoint-managed-connections-to-vellum.ts +99 -0
  58. package/src/workspace/migrations/registry.ts +2 -0
@@ -39,6 +39,7 @@ import {
39
39
  import { dirname, join, resolve, sep } from "node:path";
40
40
  import { promisify } from "node:util";
41
41
 
42
+ import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
42
43
  import { ensureBun } from "../../util/bun-runtime.js";
43
44
  import { getWorkspacePluginsDir } from "../../util/platform.js";
44
45
  import {
@@ -46,7 +47,6 @@ import {
46
47
  computeFingerprint,
47
48
  type Fingerprint,
48
49
  parseFingerprint,
49
- PRESERVED_ENTRIES,
50
50
  } from "./plugin-fingerprint.js";
51
51
  import {
52
52
  fetchMarketplaceEntries,
@@ -246,7 +246,9 @@ export class PluginSourceUnavailableError extends Error {
246
246
  * a 403 without it is a genuine authorization failure and stays hard.
247
247
  */
248
248
  function isTransientUpstreamStatus(res: Response): boolean {
249
- if (res.status === 429 || res.status >= 500) return true;
249
+ if (res.status === 429 || res.status >= 500) {
250
+ return true;
251
+ }
250
252
  if (res.status === 403) {
251
253
  return res.headers.get("x-ratelimit-remaining") === "0";
252
254
  }
@@ -302,7 +304,9 @@ async function resolvePluginSource(
302
304
  throw err;
303
305
  }
304
306
 
305
- if (!resolved) return null;
307
+ if (!resolved) {
308
+ return null;
309
+ }
306
310
 
307
311
  return {
308
312
  owner: resolved.owner,
@@ -548,9 +552,13 @@ export function finalizeStagedInstall(
548
552
  // user config and runtime data.
549
553
  if (existsSync(target)) {
550
554
  for (const entry of PRESERVED_ENTRIES) {
551
- if (entry === INSTALL_META_FILENAME) continue; // sidecar is rewritten above
555
+ if (entry === INSTALL_META_FILENAME) {
556
+ continue;
557
+ } // sidecar is rewritten above
552
558
  const src = join(target, entry);
553
- if (!existsSync(src)) continue;
559
+ if (!existsSync(src)) {
560
+ continue;
561
+ }
554
562
  const dest = join(stagingDir, entry);
555
563
  const stat = statSync(src);
556
564
  if (stat.isDirectory()) {
@@ -755,8 +763,9 @@ async function copyExternalViaGit(
755
763
  // A missing repo/ref (or a private one we can't reach) is a hard
756
764
  // not-found, surfaced as zero files. Anything else — network loss, a
757
765
  // transient GitHub outage — is retryable, so map it to a 503.
758
- if (isGitRefNotFound(err))
766
+ if (isGitRefNotFound(err)) {
759
767
  return { fileCount: 0, commit: null, committedAt: null };
768
+ }
760
769
  throw new PluginSourceUnavailableError(
761
770
  `git clone failed for ${sourceLabel(source)} @ ${source.ref}: ${subprocessErrorText(err)}`,
762
771
  503,
@@ -865,10 +874,14 @@ async function applyAdapterStub(
865
874
  stagingDir,
866
875
  deps.fetch,
867
876
  );
868
- if (stubFileCount === 0) return false;
877
+ if (stubFileCount === 0) {
878
+ return false;
879
+ }
869
880
 
870
881
  const script = resolvePostinstallScript(name, stagingDir);
871
- if (script === null) return false;
882
+ if (script === null) {
883
+ return false;
884
+ }
872
885
 
873
886
  const run = deps.runPostinstall ?? defaultPostinstallRunner;
874
887
  try {
@@ -891,7 +904,9 @@ type PackageManifest = Record<string, unknown>;
891
904
 
892
905
  /** Parse the `package.json` at `path`, or null if it's absent or unparseable. */
893
906
  function readPackageJson(path: string): PackageManifest | null {
894
- if (!existsSync(path)) return null;
907
+ if (!existsSync(path)) {
908
+ return null;
909
+ }
895
910
  try {
896
911
  const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
897
912
  return typeof parsed === "object" &&
@@ -976,7 +991,9 @@ function resolvePostinstallScript(
976
991
  stagingDir: string,
977
992
  ): string | null {
978
993
  const pkgPath = join(stagingDir, "package.json");
979
- if (!existsSync(pkgPath)) return null;
994
+ if (!existsSync(pkgPath)) {
995
+ return null;
996
+ }
980
997
 
981
998
  let parsed: unknown;
982
999
  try {
@@ -993,7 +1010,9 @@ function resolvePostinstallScript(
993
1010
  typeof scripts === "object" && scripts !== null && "postinstall" in scripts
994
1011
  ? (scripts as { postinstall?: unknown }).postinstall
995
1012
  : undefined;
996
- if (typeof command !== "string" || command.trim() === "") return null;
1013
+ if (typeof command !== "string" || command.trim() === "") {
1014
+ return null;
1015
+ }
997
1016
 
998
1017
  const match = /^bun\s+(\S+)$/.exec(command.trim());
999
1018
  if (!match) {
@@ -1005,7 +1024,9 @@ function resolvePostinstallScript(
1005
1024
  }
1006
1025
 
1007
1026
  let rel = match[1]!;
1008
- if (rel.startsWith("./")) rel = rel.slice(2);
1027
+ if (rel.startsWith("./")) {
1028
+ rel = rel.slice(2);
1029
+ }
1009
1030
  if (!/\.(?:ts|mts|cts|mjs|cjs|js)$/.test(rel)) {
1010
1031
  throw new PluginPostinstallError(
1011
1032
  name,
@@ -1074,7 +1095,9 @@ function pluginPostinstallEnv(bun: string): NodeJS.ProcessEnv {
1074
1095
  .filter(Boolean)
1075
1096
  .join(":"),
1076
1097
  };
1077
- if (process.env.HOME) env.HOME = process.env.HOME;
1098
+ if (process.env.HOME) {
1099
+ env.HOME = process.env.HOME;
1100
+ }
1078
1101
  return env;
1079
1102
  }
1080
1103
 
@@ -1090,7 +1113,9 @@ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
1090
1113
  for (const entry of readdirSync(absDir, { withFileTypes: true })) {
1091
1114
  // Drop git metadata and symlinks: the loader follows neither, and a
1092
1115
  // symlink could otherwise point outside the staging tree.
1093
- if (relDir === "" && entry.name === ".git") continue;
1116
+ if (relDir === "" && entry.name === ".git") {
1117
+ continue;
1118
+ }
1094
1119
  // Drop a top-level `bunfig.toml`. The adapter postinstall runs `bun` with
1095
1120
  // its cwd at the staged root, and Bun auto-loads `$cwd/bunfig.toml` as
1096
1121
  // project config — including a `preload` list it executes before the
@@ -1101,15 +1126,21 @@ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
1101
1126
  // consumes `bunfig.toml`. Match case-insensitively because the macOS
1102
1127
  // install target's filesystem is case-insensitive, where Bun would still
1103
1128
  // open a clone-supplied `BUNFIG.TOML`.
1104
- if (relDir === "" && entry.name.toLowerCase() === "bunfig.toml") continue;
1105
- if (entry.isSymbolicLink()) continue;
1129
+ if (relDir === "" && entry.name.toLowerCase() === "bunfig.toml") {
1130
+ continue;
1131
+ }
1132
+ if (entry.isSymbolicLink()) {
1133
+ continue;
1134
+ }
1106
1135
 
1107
1136
  const rel = relDir ? join(relDir, entry.name) : entry.name;
1108
1137
  if (entry.isDirectory()) {
1109
1138
  walk(rel);
1110
1139
  continue;
1111
1140
  }
1112
- if (!entry.isFile()) continue;
1141
+ if (!entry.isFile()) {
1142
+ continue;
1143
+ }
1113
1144
 
1114
1145
  const dest = join(destDir, rel);
1115
1146
  mkdirSync(dirname(dest), { recursive: true });
@@ -1168,7 +1199,9 @@ export const defaultGitRunner: GitRunner = async (args, opts) => {
1168
1199
  function pluginGitEnv(): NodeJS.ProcessEnv {
1169
1200
  const env: NodeJS.ProcessEnv = {};
1170
1201
  for (const [key, value] of Object.entries(process.env)) {
1171
- if (value !== undefined && !key.startsWith("GIT_")) env[key] = value;
1202
+ if (value !== undefined && !key.startsWith("GIT_")) {
1203
+ env[key] = value;
1204
+ }
1172
1205
  }
1173
1206
  env.GIT_TERMINAL_PROMPT = "0";
1174
1207
  const extraPaths = ["/opt/homebrew/bin", "/usr/local/bin"];
@@ -1199,12 +1232,16 @@ interface WriteInstallMetaParams {
1199
1232
  */
1200
1233
  function readStagedPackageVersion(stagingDir: string): string | undefined {
1201
1234
  const pkgPath = join(stagingDir, "package.json");
1202
- if (!existsSync(pkgPath)) return undefined;
1235
+ if (!existsSync(pkgPath)) {
1236
+ return undefined;
1237
+ }
1203
1238
  try {
1204
1239
  const parsed: unknown = JSON.parse(readFileSync(pkgPath, "utf8"));
1205
1240
  if (typeof parsed === "object" && parsed !== null) {
1206
1241
  const version = (parsed as Record<string, unknown>).version;
1207
- if (typeof version === "string" && version.length > 0) return version;
1242
+ if (typeof version === "string" && version.length > 0) {
1243
+ return version;
1244
+ }
1208
1245
  }
1209
1246
  } catch {
1210
1247
  // fall through to undefined
@@ -1267,7 +1304,9 @@ function writeInstallMeta(
1267
1304
  */
1268
1305
  export function readInstallMeta(pluginDir: string): InstallMeta | null {
1269
1306
  const metaPath = join(pluginDir, INSTALL_META_FILENAME);
1270
- if (!existsSync(metaPath)) return null;
1307
+ if (!existsSync(metaPath)) {
1308
+ return null;
1309
+ }
1271
1310
 
1272
1311
  let parsed: unknown;
1273
1312
  try {
@@ -1343,12 +1382,16 @@ async function copyDir(
1343
1382
  fetchFn: FetchLike,
1344
1383
  ): Promise<number> {
1345
1384
  const entries = await listDir(owner, repo, apiPath, ref, fetchFn);
1346
- if (entries === null) return 0;
1385
+ if (entries === null) {
1386
+ return 0;
1387
+ }
1347
1388
 
1348
1389
  let count = 0;
1349
1390
  for (const entry of entries) {
1350
1391
  // The daemon loader follows neither symlinks nor submodules; skip them.
1351
- if (entry.type === "symlink" || entry.type === "submodule") continue;
1392
+ if (entry.type === "symlink" || entry.type === "submodule") {
1393
+ continue;
1394
+ }
1352
1395
  assertSafeFilename("entry name", entry.name);
1353
1396
 
1354
1397
  if (entry.type === "dir") {
@@ -1402,7 +1445,9 @@ async function listDir(
1402
1445
  `/contents/${encodeURIComponent(apiPath).replaceAll("%2F", "/")}?ref=${encodeURIComponent(ref)}`;
1403
1446
 
1404
1447
  const res = await githubFetch(url, "application/vnd.github+json", fetchFn);
1405
- if (res.status === 404) return null;
1448
+ if (res.status === 404) {
1449
+ return null;
1450
+ }
1406
1451
  if (!res.ok) {
1407
1452
  const label = `GitHub contents listing failed for ${apiPath} @ ${ref}: HTTP ${res.status}`;
1408
1453
  if (isTransientUpstreamStatus(res)) {
@@ -10,9 +10,9 @@
10
10
  /** Keys that are CLI-presentation concerns, not IPC params. */
11
11
  const CLI_ONLY_KEYS = new Set(["json", "verbose"]);
12
12
 
13
- export function optsToQueryParams(
14
- opts: Record<string, unknown>,
15
- ): { queryParams: Record<string, string> } {
13
+ export function optsToQueryParams(opts: Record<string, unknown>): {
14
+ queryParams: Record<string, string>;
15
+ } {
16
16
  const queryParams: Record<string, string> = {};
17
17
  for (const [k, v] of Object.entries(opts)) {
18
18
  if (CLI_ONLY_KEYS.has(k)) continue;
@@ -206,9 +206,7 @@ export function listAllPlugins(
206
206
  // ── User plugins ───────────────────────────────────────────────────────
207
207
  // Filter out default-plugin stub directories (created by `plugins disable
208
208
  // default-<name>`) so they don't show up as duplicate user entries.
209
- const defaultNames = new Set(
210
- readDefaultPluginManifests().map((m) => m.name),
211
- );
209
+ const defaultNames = new Set(readDefaultPluginManifests().map((m) => m.name));
212
210
  const userPlugins: AllPluginInfo[] = listInstalledPlugins(opts)
213
211
  .filter((entry) => !defaultNames.has(entry.name))
214
212
  .map((entry) => ({
@@ -255,7 +253,12 @@ export function listAllPlugins(
255
253
  );
256
254
  // enabledDefault and disabledDefault keep repo array order (no sort).
257
255
 
258
- return [...enabledUser, ...disabledUser, ...enabledDefault, ...disabledDefault];
256
+ return [
257
+ ...enabledUser,
258
+ ...disabledUser,
259
+ ...enabledDefault,
260
+ ...disabledDefault,
261
+ ];
259
262
  }
260
263
 
261
264
  interface DefaultPluginManifest {
@@ -309,7 +312,10 @@ function getPluginInstallDate(plugin: AllPluginInfo): number {
309
312
  const metaPath = join(plugin.target, "install-meta.json");
310
313
  try {
311
314
  if (existsSync(metaPath)) {
312
- const raw = JSON.parse(readFileSync(metaPath, "utf8")) as Record<string, unknown>;
315
+ const raw = JSON.parse(readFileSync(metaPath, "utf8")) as Record<
316
+ string,
317
+ unknown
318
+ >;
313
319
  if (typeof raw.installedAt === "string") {
314
320
  const ms = Date.parse(raw.installedAt);
315
321
  if (Number.isFinite(ms)) return ms;
@@ -44,10 +44,8 @@ import { tmpdir } from "node:os";
44
44
  import { dirname, join } from "node:path";
45
45
  import { promisify } from "node:util";
46
46
 
47
- import {
48
- computeFingerprint,
49
- PRESERVED_ENTRIES,
50
- } from "./plugin-fingerprint.js";
47
+ import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
48
+ import { computeFingerprint } from "./plugin-fingerprint.js";
51
49
 
52
50
  const execFileAsync = promisify(execFile);
53
51
 
@@ -122,7 +120,9 @@ interface MergedFile {
122
120
  function isBinary(buf: Buffer): boolean {
123
121
  const len = Math.min(buf.length, 8000);
124
122
  for (let i = 0; i < len; i++) {
125
- if (buf[i] === 0) return true;
123
+ if (buf[i] === 0) {
124
+ return true;
125
+ }
126
126
  }
127
127
  return false;
128
128
  }
@@ -153,10 +153,12 @@ async function threeWayMergeFile(
153
153
  labels: ConflictLabels,
154
154
  ): Promise<MergedFile> {
155
155
  if (isBinary(ours) || isBinary(base) || isBinary(theirs)) {
156
- if (ours.equals(base))
156
+ if (ours.equals(base)) {
157
157
  return { content: theirs, conflicted: false, binaryConflicted: false };
158
- if (theirs.equals(base))
158
+ }
159
+ if (theirs.equals(base)) {
159
160
  return { content: ours, conflicted: false, binaryConflicted: false };
161
+ }
160
162
  if (strategy === "assistant") {
161
163
  return { content: ours, conflicted: false, binaryConflicted: true };
162
164
  }
@@ -287,8 +289,12 @@ export async function mergePluginTree({
287
289
  labels,
288
290
  );
289
291
  keep(rel, merged.content);
290
- if (merged.conflicted) conflicts.push(rel);
291
- if (merged.binaryConflicted) binaryConflicts.push(rel);
292
+ if (merged.conflicted) {
293
+ conflicts.push(rel);
294
+ }
295
+ if (merged.binaryConflicted) {
296
+ binaryConflicts.push(rel);
297
+ }
292
298
  }
293
299
  continue;
294
300
  }
@@ -159,12 +159,6 @@ export function parseFingerprint(value: unknown): Fingerprint | null {
159
159
  return { algorithm: "sha256", files };
160
160
  }
161
161
 
162
- // The preserved-entries constant lives with the shared walk so install
163
- // fingerprinting and the live-reload source fingerprint can't disagree on
164
- // what counts as the plugin's source tree; re-exported here for the
165
- // install/upgrade/diff callers that treat this module as the fingerprint API.
166
- export { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
167
-
168
162
  /**
169
163
  * Aggregate SHA-256 digest over a tree's contents, returned as `v2:<hex>`.
170
164
  *
@@ -609,7 +609,7 @@ export async function runPublish(
609
609
  }
610
610
  return false;
611
611
  }
612
- }/**
612
+ } /**
613
613
  * Format a publish result for terminal output.
614
614
  */
615
615
  export function formatPublishResult(result: PublishResult): string {
@@ -46,7 +46,9 @@ export function detectUnknownCommand(
46
46
  if (known.has(firstPositional)) return null;
47
47
 
48
48
  const suggestion = findClosestCommand(firstPositional, [...known]);
49
- return suggestion ? { token: firstPositional, suggestion } : { token: firstPositional };
49
+ return suggestion
50
+ ? { token: firstPositional, suggestion }
51
+ : { token: firstPositional };
50
52
  }
51
53
 
52
54
  /**
@@ -32,6 +32,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
32
32
  import { tmpdir } from "node:os";
33
33
  import { dirname, join } from "node:path";
34
34
 
35
+ import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
35
36
  import { getWorkspacePluginsDir } from "../../util/platform.js";
36
37
  import {
37
38
  inspectPlugin,
@@ -55,11 +56,7 @@ import {
55
56
  sanitizePluginName,
56
57
  } from "./install-from-github.js";
57
58
  import { type ConflictLabels, mergePluginTree } from "./merge-plugin-tree.js";
58
- import {
59
- computeFingerprint,
60
- fingerprintsEqual,
61
- PRESERVED_ENTRIES,
62
- } from "./plugin-fingerprint.js";
59
+ import { computeFingerprint, fingerprintsEqual } from "./plugin-fingerprint.js";
63
60
  import { PluginNotInstalledError } from "./uninstall-plugin.js";
64
61
 
65
62
  /**
@@ -110,7 +110,7 @@ describe("reconcileFlagGatedProfiles", () => {
110
110
  const raw = readConfig();
111
111
  const osBeta = raw.llm.profiles["os-beta"]!;
112
112
  expect(osBeta.model).toBe("MiniMaxAI/MiniMax-M3");
113
- expect(osBeta.provider_connection).toBe("together-managed");
113
+ expect(osBeta.provider_connection).toBe("vellum");
114
114
  expect(osBeta.provider).toBe("together");
115
115
  expect(osBeta.source).toBe("managed");
116
116
  expect(osBeta.label).toBe("OS Beta");
@@ -164,7 +164,7 @@ describe("reconcileFlagGatedProfiles", () => {
164
164
  expect(after.status).toBe("disabled");
165
165
  expect(after.topP).toBe(0.8);
166
166
  expect(after.model).toBe("MiniMaxAI/MiniMax-M3");
167
- expect(after.provider_connection).toBe("together-managed");
167
+ expect(after.provider_connection).toBe("vellum");
168
168
  expect(after.effort).toBe("low");
169
169
  });
170
170
 
@@ -4,6 +4,7 @@ import {
4
4
  getConnection,
5
5
  MANAGED_CONNECTION_NAMES,
6
6
  PROVIDERS_REQUIRING_BASE_URL_AND_MODELS,
7
+ VELLUM_MANAGED_CONNECTION_NAME,
7
8
  } from "../providers/inference/connections.js";
8
9
  import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
9
10
  import { resolveModelIntent } from "../providers/model-intents.js";
@@ -49,7 +50,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
49
50
  balanced: {
50
51
  model: "accounts/fireworks/models/glm-5p2",
51
52
  provider: "fireworks",
52
- connectionName: "fireworks-managed",
53
+ connectionName: VELLUM_MANAGED_CONNECTION_NAME,
53
54
  source: "managed",
54
55
  label: "Balanced",
55
56
  description: "Good balance of quality, cost, and speed",
@@ -64,7 +65,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
64
65
  "quality-optimized": {
65
66
  intent: "quality-optimized",
66
67
  provider: "anthropic",
67
- connectionName: "anthropic-managed",
68
+ connectionName: VELLUM_MANAGED_CONNECTION_NAME,
68
69
  source: "managed",
69
70
  label: "Quality",
70
71
  description: "High-quality results with the most capable model",
@@ -85,7 +86,7 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
85
86
  "cost-optimized": {
86
87
  model: "accounts/fireworks/models/deepseek-v4-flash",
87
88
  provider: "fireworks",
88
- connectionName: "fireworks-managed",
89
+ connectionName: VELLUM_MANAGED_CONNECTION_NAME,
89
90
  source: "managed",
90
91
  label: "Speed",
91
92
  description: "Fastest responses at lower cost (DeepSeek V4 Flash)",
@@ -154,7 +155,7 @@ export const OS_BETA_FEATURE_FLAG_KEY = "os-beta";
154
155
  export const OS_BETA_PROFILE_TEMPLATE: ManagedProfileTemplate = {
155
156
  intent: "balanced",
156
157
  provider: "together",
157
- connectionName: "together-managed",
158
+ connectionName: VELLUM_MANAGED_CONNECTION_NAME,
158
159
  source: "managed",
159
160
  label: "OS Beta",
160
161
  description: "Good balance of quality, cost, and speed, in beta",
@@ -4,6 +4,10 @@
4
4
  * can delegate without exposing its full surface.
5
5
  */
6
6
 
7
+ import {
8
+ formatImageSourceAnnotation,
9
+ formatStoredPathAnnotation,
10
+ } from "../agent/attachments.js";
7
11
  import { getConfig } from "../config/loader.js";
8
12
  import type { EventBus } from "../events/bus.js";
9
13
  import type { AssistantDomainEvents } from "../events/domain-events.js";
@@ -32,14 +36,19 @@ import type { SurfaceData, SurfaceType } from "./message-protocol.js";
32
36
  const log = getLogger("conversation-lifecycle");
33
37
 
34
38
  /**
35
- * Re-inject image source path annotations into message content blocks.
39
+ * Re-inject attachment path annotations into message content blocks.
36
40
  *
37
- * When the desktop client attaches images from local files, the source paths
38
- * are stored in `metadata.imageSourcePaths` (keyed by filename). The LLM-facing
39
- * content omits these paths at persistence time, so we re-inject them when
40
- * loading history from the DB. Only user messages are annotated.
41
+ * The LLM-facing content omits path annotations at persistence time, so we
42
+ * re-inject them when loading history from the DB, from two metadata keys:
43
+ * `imageSourcePaths` (where desktop-attached images came from) and
44
+ * `attachmentStoredPaths` (the canonical, collision-suffixed copies in the
45
+ * conversation's attachments/ directory), both keyed by
46
+ * `${position}:${filename}`. The rebuilt block must stay byte-identical to
47
+ * the one `enrichMessageWithSourcePaths` appends at persist time so reloads
48
+ * and forks keep provider prefix-cache parity. Only user messages are
49
+ * annotated.
41
50
  */
42
- export function reinjectImageSourcePaths(
51
+ export function reinjectAttachmentPathAnnotations(
43
52
  content: ContentBlock[],
44
53
  role: string,
45
54
  metadataJson: string | null,
@@ -47,17 +56,30 @@ export function reinjectImageSourcePaths(
47
56
  if (role !== "user" || !metadataJson) return content;
48
57
  try {
49
58
  const meta = JSON.parse(metadataJson);
50
- if (!meta.imageSourcePaths || typeof meta.imageSourcePaths !== "object") {
59
+ const lines: string[] = [];
60
+ if (meta.imageSourcePaths && typeof meta.imageSourcePaths === "object") {
61
+ for (const p of Object.values(meta.imageSourcePaths)) {
62
+ if (typeof p === "string") {
63
+ lines.push(formatImageSourceAnnotation(p));
64
+ }
65
+ }
66
+ }
67
+ if (
68
+ meta.attachmentStoredPaths &&
69
+ typeof meta.attachmentStoredPaths === "object"
70
+ ) {
71
+ for (const [key, p] of Object.entries(meta.attachmentStoredPaths)) {
72
+ if (typeof p !== "string") {
73
+ continue;
74
+ }
75
+ const filename = key.slice(key.indexOf(":") + 1);
76
+ lines.push(formatStoredPathAnnotation(filename, p));
77
+ }
78
+ }
79
+ if (lines.length === 0) {
51
80
  return content;
52
81
  }
53
- const paths = Object.values(meta.imageSourcePaths).filter(
54
- (v): v is string => typeof v === "string",
55
- );
56
- if (paths.length === 0) return content;
57
- const annotation = paths
58
- .map((p) => `[Attached image source: ${p}]`)
59
- .join("\n");
60
- return [...content, { type: "text" as const, text: annotation }];
82
+ return [...content, { type: "text" as const, text: lines.join("\n") }];
61
83
  } catch {
62
84
  // metadata parse failure — skip annotation, not critical
63
85
  return content;