@vellumai/assistant 0.10.3 → 0.10.4-staging.1

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 (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -46,6 +46,8 @@ export interface GitContext {
46
46
  repo: string;
47
47
  dirty: boolean;
48
48
  pushed: boolean;
49
+ /** Repo-relative path to the plugin directory, or "" if at repo root. */
50
+ pluginPath: string;
49
51
  }
50
52
 
51
53
  export interface PublishPayload {
@@ -225,6 +227,12 @@ export async function resolveGitContext(dir: string): Promise<GitContext> {
225
227
  }
226
228
  const repo = remoteMatch[1];
227
229
 
230
+ // Compute the repo-relative path to the plugin directory.
231
+ const repoRoot = await runGit(dir, ["rev-parse", "--show-toplevel"]);
232
+ const pluginPath = resolve(dir)
233
+ .slice(resolve(repoRoot).length + 1)
234
+ .replace(/\\/g, "/");
235
+
228
236
  // Check if the commit is pushed
229
237
  let pushed = true;
230
238
  try {
@@ -235,7 +243,7 @@ export async function resolveGitContext(dir: string): Promise<GitContext> {
235
243
  pushed = true;
236
244
  }
237
245
 
238
- return { sha, repo, dirty, pushed };
246
+ return { sha, repo, dirty, pushed, pluginPath };
239
247
  }
240
248
 
241
249
  // ---------------------------------------------------------------------------
@@ -261,6 +269,7 @@ export function buildPublishPayload(
261
269
  source: "github",
262
270
  repo: git.repo,
263
271
  ref: git.sha,
272
+ ...(git.pluginPath ? { path: git.pluginPath } : {}),
264
273
  },
265
274
  category,
266
275
  };
@@ -379,7 +388,228 @@ export function formatValidationResult(validation: PublishValidation): string {
379
388
  return lines.join("\n");
380
389
  }
381
390
 
391
+ // ---------------------------------------------------------------------------
392
+ // CLI entrypoint
393
+ // ---------------------------------------------------------------------------
394
+
395
+ export interface PublishCliOptions {
396
+ print?: boolean;
397
+ path?: string;
398
+ force?: boolean;
399
+ json?: boolean;
400
+ category?: string;
401
+ }
402
+
403
+ export interface PublishCliDeps {
404
+ confirmPrompt: (opts: {
405
+ question: string;
406
+ isTTY: boolean;
407
+ refuseNonInteractiveMessage: string;
408
+ }) => Promise<string>;
409
+ }
410
+
382
411
  /**
412
+ * Run the full `plugins publish` flow. This is the single entrypoint that
413
+ * the CLI command calls. All validation, git resolution, confirmation,
414
+ * and submission logic lives here rather than in the command file.
415
+ *
416
+ * Returns `true` on success, `false` on failure (with an appropriate
417
+ * message already printed).
418
+ */
419
+ export async function runPublish(
420
+ opts: PublishCliOptions,
421
+ deps: PublishCliDeps,
422
+ ): Promise<boolean> {
423
+ // 1. Find the plugin root
424
+ const searchDir = opts.path ?? process.cwd();
425
+ const pluginDir = findPluginRoot(searchDir);
426
+ if (!pluginDir) {
427
+ if (opts.json) {
428
+ process.stdout.write(
429
+ JSON.stringify({
430
+ ok: false,
431
+ error: "no_package_json",
432
+ message: `No package.json found in ${searchDir} or any parent directory.`,
433
+ }) + "\n",
434
+ );
435
+ } else {
436
+ console.error(
437
+ `No package.json found in ${searchDir} or any parent directory.`,
438
+ );
439
+ }
440
+ return false;
441
+ }
442
+
443
+ // 2. Validate the plugin
444
+ const validation = validatePluginForPublish(pluginDir);
445
+ if (!validation.valid) {
446
+ if (opts.json) {
447
+ process.stdout.write(
448
+ JSON.stringify({ ok: false, errors: validation.issues }) + "\n",
449
+ );
450
+ } else {
451
+ console.error(formatValidationResult(validation));
452
+ }
453
+ return false;
454
+ }
455
+
456
+ if (!opts.json && validation.warnings.length > 0) {
457
+ console.warn(formatValidationResult(validation));
458
+ }
459
+
460
+ // 3. Handle --print mode early (no git context needed)
461
+ if (opts.print) {
462
+ // For --print, resolve git context best-effort to fill source fields
463
+ let git: GitContext | null = null;
464
+ try {
465
+ git = await resolveGitContext(pluginDir);
466
+ } catch {
467
+ // Git not available — use placeholder values
468
+ }
469
+
470
+ const category = opts.category ?? "other";
471
+ const payload = git
472
+ ? buildPublishPayload(validation, git, category)
473
+ : {
474
+ name: validation.packageJson.name!,
475
+ source: {
476
+ source: "github" as const,
477
+ repo: "<unknown>",
478
+ ref: "<unknown>",
479
+ },
480
+ category,
481
+ ...(validation.packageJson.description
482
+ ? { description: validation.packageJson.description }
483
+ : {}),
484
+ ...(validation.packageJson.license
485
+ ? { license: validation.packageJson.license }
486
+ : {}),
487
+ ...(validation.packageJson.homepage
488
+ ? { homepage: validation.packageJson.homepage }
489
+ : {}),
490
+ };
491
+
492
+ if (opts.json) {
493
+ process.stdout.write(JSON.stringify({ ok: true, payload }) + "\n");
494
+ } else {
495
+ console.log(formatPayloadForPrint(payload));
496
+ }
497
+ return true;
498
+ }
499
+
500
+ // 4. Resolve git context (required for submission)
501
+ let git: GitContext;
502
+ try {
503
+ git = await resolveGitContext(pluginDir);
504
+ } catch (err) {
505
+ const message = err instanceof Error ? err.message : String(err);
506
+ if (opts.json) {
507
+ process.stdout.write(
508
+ JSON.stringify({
509
+ ok: false,
510
+ error: "git_resolution_failed",
511
+ message: `Git context resolution failed: ${message}`,
512
+ }) + "\n",
513
+ );
514
+ } else {
515
+ console.error(`Git context resolution failed: ${message}`);
516
+ }
517
+ return false;
518
+ }
519
+
520
+ if (git.dirty) {
521
+ console.warn(
522
+ "Warning: working tree is dirty. The pinned commit SHA should match a clean, pushed commit.",
523
+ );
524
+ }
525
+
526
+ if (!git.pushed) {
527
+ if (opts.json) {
528
+ process.stdout.write(
529
+ JSON.stringify({
530
+ ok: false,
531
+ error: "not_pushed",
532
+ message: `Commit ${git.sha.slice(0, 7)} has not been pushed to the remote. Push your changes first.`,
533
+ }) + "\n",
534
+ );
535
+ } else {
536
+ console.error(
537
+ `Commit ${git.sha.slice(0, 7)} has not been pushed to the remote. Push your changes first.`,
538
+ );
539
+ }
540
+ return false;
541
+ }
542
+
543
+ // 5. Determine category and build payload
544
+ const category = opts.category ?? "other";
545
+ const payload = buildPublishPayload(validation, git, category);
546
+
547
+ // 6. Confirm before submitting (unless --force or --json)
548
+ if (!opts.force && !opts.json) {
549
+ console.log("\nPlugin entry to submit:");
550
+ console.log(formatPayloadForPrint(payload));
551
+ console.log("");
552
+ const result = await deps.confirmPrompt({
553
+ question: "Submit this entry to the Vellum marketplace? [y/N] ",
554
+ isTTY: Boolean(process.stdin.isTTY),
555
+ refuseNonInteractiveMessage:
556
+ "Refusing to publish non-interactively. Pass --force to confirm.",
557
+ });
558
+ if (result === "non-interactive" || result === "denied") {
559
+ if (result === "non-interactive") {
560
+ console.error(
561
+ "Refusing to publish non-interactively. Pass --force to confirm.",
562
+ );
563
+ }
564
+ return false;
565
+ }
566
+ }
567
+
568
+ // 7. Submit to the platform API
569
+ const platformDeps = await resolvePlatformDeps();
570
+ if (!platformDeps) {
571
+ const msg =
572
+ "Not connected to Vellum platform. Run `assistant platform connect` to connect, or use --print to generate the entry without submitting.";
573
+ if (opts.json) {
574
+ process.stdout.write(
575
+ JSON.stringify({
576
+ ok: false,
577
+ error: "not_connected",
578
+ message: msg,
579
+ }) + "\n",
580
+ );
581
+ } else {
582
+ console.error(msg);
583
+ }
584
+ return false;
585
+ }
586
+
587
+ try {
588
+ const result = await postPublishRequest(payload, platformDeps);
589
+
590
+ if (opts.json) {
591
+ process.stdout.write(JSON.stringify(result) + "\n");
592
+ } else {
593
+ console.log(formatPublishResult(result));
594
+ }
595
+
596
+ return result.ok;
597
+ } catch (err) {
598
+ const message = err instanceof Error ? err.message : String(err);
599
+ if (opts.json) {
600
+ process.stdout.write(
601
+ JSON.stringify({
602
+ ok: false,
603
+ error: "request_failed",
604
+ message,
605
+ }) + "\n",
606
+ );
607
+ } else {
608
+ console.error(`Publish request failed: ${message}`);
609
+ }
610
+ return false;
611
+ }
612
+ }/**
383
613
  * Format a publish result for terminal output.
384
614
  */
385
615
  export function formatPublishResult(result: PublishResult): string {
@@ -153,7 +153,6 @@ describe("reconcileFlagGatedProfiles", () => {
153
153
  const raw = readConfig();
154
154
  raw.llm.profiles["os-beta"]!.label = "My OS Beta";
155
155
  raw.llm.profiles["os-beta"]!.status = "disabled";
156
- raw.llm.profiles["os-beta"]!.advisorEnabled = true;
157
156
  raw.llm.profiles["os-beta"]!.topP = 0.8;
158
157
  writeConfig(raw);
159
158
  invalidateConfigCache();
@@ -163,7 +162,6 @@ describe("reconcileFlagGatedProfiles", () => {
163
162
  const after = readConfig().llm.profiles["os-beta"]!;
164
163
  expect(after.label).toBe("My OS Beta");
165
164
  expect(after.status).toBe("disabled");
166
- expect(after.advisorEnabled).toBe(true);
167
165
  expect(after.topP).toBe(0.8);
168
166
  expect(after.model).toBe("MiniMaxAI/MiniMax-M3");
169
167
  expect(after.provider_connection).toBe("together-managed");
@@ -324,7 +322,7 @@ describe("reconcileFlagGatedProfiles", () => {
324
322
 
325
323
  const raw = readConfig();
326
324
  (raw.llm as Record<string, unknown>).callSites = {
327
- advisor: { profile: "os-beta", temperature: 0.3 },
325
+ inference: { profile: "os-beta", temperature: 0.3 },
328
326
  };
329
327
  writeConfig(raw);
330
328
  invalidateConfigCache();
@@ -333,14 +331,14 @@ describe("reconcileFlagGatedProfiles", () => {
333
331
  expect(reconcileFlagGatedProfiles()).toBe(true);
334
332
 
335
333
  const after = readConfig();
336
- const advisor = (
334
+ const inference = (
337
335
  after.llm as unknown as Record<
338
336
  string,
339
337
  Record<string, Record<string, unknown>>
340
338
  >
341
- ).callSites.advisor;
342
- expect(advisor.profile).toBeUndefined();
343
- expect(advisor.temperature).toBe(0.3);
339
+ ).callSites.inference;
340
+ expect(inference.profile).toBeUndefined();
341
+ expect(inference.temperature).toBe(0.3);
344
342
 
345
343
  expect(LLMSchema.safeParse(after.llm).success).toBe(true);
346
344
  });
@@ -40,9 +40,24 @@ Each subagent is spawned with a role that determines its tool access. Choose the
40
40
  | `coder` | `bash`, `file_read`, `file_write`, `file_edit`, `web_search`, `recall`, `notify_parent` | Code changes, file editing, running commands, build/test tasks |
41
41
  | `planner` | `file_read`, `file_list`, `web_search`, `web_fetch`, `recall`, `notify_parent` | Analysis, planning, synthesizing information, reviewing approaches |
42
42
  | `investigator` | `code_search`, `file_read`, `file_list`, `web_search`, `web_fetch`, `recall`, `notify_parent` | Root-cause analysis: debugging, log forensics, tracing behavior across many files. Read-only search/read tools only (no shell): use `code_search` to grep file contents across directories, `file_list` to enumerate paths, `file_read` to read whole files and logs; returns a compact root-cause report |
43
+ | `advisor` | None (tool-less) | Read-only senior-advisor consult. Runs on a stronger model, inherits full parent context, and BLOCKS until it returns guidance |
43
44
 
44
45
  All specialized roles (`researcher`, `coder`, `planner`) include `notify_parent` for mid-run communication with the parent.
45
46
 
47
+ ## Consulting the Advisor
48
+
49
+ The `advisor` role is the one role you spawn on your own judgment, unprompted — you do NOT wait for the user to ask for a subagent. The other roles (`researcher`, `coder`, `planner`, `investigator`, `general`) stay delegation-driven: reach for them to offload work, typically when the user's request calls for it. The advisor is different: proactively consult it whenever the conditions below are met.
50
+
51
+ Orient yourself first (read the relevant files, understand the task), then consult the advisor:
52
+
53
+ - **Before you commit to an approach and start building** — to shape a plan when you don't have one, or to pressure-test and sharpen a plan you've already drafted.
54
+ - **When you get stuck or are weighing a change in direction.**
55
+ - **Once before you declare the task done.**
56
+
57
+ The consult is synchronous and read-only: spawning an `advisor` subagent BLOCKS until it returns guidance. It runs on a stronger model and inherits your full context, so it sees the task, your tool calls, and their results without you re-explaining. Give its guidance serious weight; only override it when primary-source evidence contradicts a specific claim — and say so when you do.
58
+
59
+ Spawn the advisor **alone** — do NOT batch the consult in the same turn as other tool calls (especially file edits, shell commands, or anything destructive or expensive). Tool calls you issue in the same turn run concurrently with the consult, so they would execute before you see its guidance. Consult the advisor by itself, read its guidance, then act.
60
+
46
61
  ## Parent Communication
47
62
 
48
63
  Subagents use `notify_parent` to send messages to the parent conversation while still running. Each notification has an urgency level:
@@ -79,7 +94,7 @@ Set `inference_profile` to an `llm.profiles` key when a subagent should run unde
79
94
 
80
95
  Forks are sub-agents that inherit the parent's full context -- messages, system prompt, and memory -- sharing the KV cache for near-free context inheritance. Use forks when the task benefits from knowing what you've been discussing; use a regular sub-agent when the task is self-contained.
81
96
 
82
- **Key behaviors:** Forks always run as `general` role (the `role` parameter is ignored). `send_result_to_user` defaults to `false`. Read fork output with `last_n: 1` to get only the final synthesis.
97
+ **Key behaviors:** Forks default to `general` role (the `role` parameter is ignored for forks), except the special `advisor` role, which is honored even as a fork. `send_result_to_user` defaults to `false`. Read fork output with `last_n: 1` to get only the final synthesis.
83
98
 
84
99
  **When to fork vs regular sub-agent:**
85
100
 
@@ -3,7 +3,7 @@
3
3
  "tools": [
4
4
  {
5
5
  "name": "subagent_spawn",
6
- "description": "Spawn an independent subagent to work on a task in parallel. The subagent runs autonomously and its results are reported back when complete.\n\nTwo modes:\n- **Regular sub-agent** (fork: false or omitted): Gets only the objective + context fields. Use for self-contained tasks with clear objectives. Can use scoped roles (researcher, coder, planner).\n- **Fork** (fork: true): Inherits full parent context (messages, system prompt, memory). Shares KV cache for near-free context inheritance. Use when the task benefits from knowing what you've been discussing. Always runs as general role. Results are internal by default (send_result_to_user: false). Read with last_n: 1 to get only the final synthesis.\n\nDecision heuristic: Does the task need to know what we've been talking about? Fork. Is it self-contained? Regular sub-agent.",
6
+ "description": "Spawn an independent subagent to work on a task in parallel. The subagent runs autonomously and its results are reported back when complete.\n\nTwo modes:\n- **Regular sub-agent** (fork: false or omitted): Gets only the objective + context fields. Use for self-contained tasks with clear objectives. Can use scoped roles (researcher, coder, planner).\n- **Fork** (fork: true): Inherits full parent context (messages, system prompt, memory). Shares KV cache for near-free context inheritance. Use when the task benefits from knowing what you've been discussing. Defaults to general role (the 'advisor' role is the exception — it is honored as a fork). Results are internal by default (send_result_to_user: false). Read with last_n: 1 to get only the final synthesis.\n\nDecision heuristic: Does the task need to know what we've been talking about? Fork. Is it self-contained? Regular sub-agent.\n\nThe 'advisor' role is a synchronous, read-only \"consult a stronger advisor\" mode: it inherits your full context, has no tools, and BLOCKS until it returns focused strategic guidance in a single response.",
7
7
  "category": "orchestration",
8
8
  "risk": "low",
9
9
  "input_schema": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "fork": {
25
25
  "type": "boolean",
26
- "description": "When true, the subagent inherits the parent's full context (messages, system prompt, memory) instead of receiving only the objective + context fields. Fork mode always runs as 'general' role (the role parameter is ignored) and defaults send_result_to_user to false. Use for tasks that benefit from the full conversational context."
26
+ "description": "When true, the subagent inherits the parent's full context (messages, system prompt, memory) instead of receiving only the objective + context fields. Fork mode defaults to 'general' role (the role parameter is ignored for forks), except the special 'advisor' role which is honored even as a fork, and defaults send_result_to_user to false. Use for tasks that benefit from the full conversational context."
27
27
  },
28
28
  "send_result_to_user": {
29
29
  "type": "boolean",
@@ -36,9 +36,10 @@
36
36
  "researcher",
37
37
  "coder",
38
38
  "planner",
39
- "investigator"
39
+ "investigator",
40
+ "advisor"
40
41
  ],
41
- "description": "Agent specialization that controls tool access. 'researcher': read-only (web, files, memory). 'coder': file and bash access. 'planner': read-only analysis. 'investigator': root-cause analysis — debugging, log forensics, tracing behavior across many files; uses read-only search/read tools (code_search, file_read, file_list) with no shell access and returns a compact root-cause report. 'general': full access (default). Ignored when fork: true (forks always use general)."
42
+ "description": "Agent specialization that controls tool access. 'researcher': read-only (web, files, memory). 'coder': file and bash access. 'planner': read-only analysis. 'investigator': root-cause analysis — debugging, log forensics, tracing behavior across many files; uses read-only search/read tools (code_search, file_read, file_list) with no shell access and returns a compact root-cause report. 'advisor': synchronous, read-only \"consult a stronger advisor\" — inherits full context, has no tools, and blocks until it returns focused strategic guidance. 'general': full access (default). Non-advisor roles are ignored when fork: true (forks default to general); the advisor role is honored even as a fork."
42
43
  },
43
44
  "inference_profile": {
44
45
  "type": "string",
@@ -69,12 +69,6 @@ export const CALL_SITE_DEFAULTS: Record<LLMCallSite, CallSiteDefaultConfig> = {
69
69
  meetConsentMonitor: { profile: "cost-optimized" },
70
70
  meetChatOpportunity: { profile: "cost-optimized" },
71
71
  inference: { profile: "cost-optimized" },
72
- // The advisor consults the strongest managed profile (`frontier`, Opus),
73
- // which seeding writes into `llm.advisorProfile` on boot and floats above this
74
- // layer. This static fallback — used only when no `advisorProfile` resolves —
75
- // stays on the always-reserved `quality-optimized` so it can never resolve to
76
- // a user-owned profile that happens to be named `frontier`.
77
- advisor: { profile: "quality-optimized" },
78
72
  // Vision captioning for the image-fallback plugin. No pinned profile — the
79
73
  // plugin resolves a vision-capable profile itself via `doesSupportVision` and
80
74
  // passes it as an `overrideProfile`, so the call-site default is a fallback
@@ -497,9 +497,6 @@ function profileConfigFragment(profile: ProfileEntry): Mergeable {
497
497
  // lower-precedence (e.g. active) profile into one that merely inherited it.
498
498
  // `RetryProvider` resolves it from the applied profile, not the merge.
499
499
  logitBias: _logitBias,
500
- // Per-profile advisor toggle is profile identity, not inheritable model
501
- // config — strip it so it can't leak into the merged `LLMConfigBase`.
502
- advisorEnabled: _advisorEnabled,
503
500
  // `temperature`/`top_p` are provider-coupled: only the winning profile
504
501
  // contributes them (tracked via `samplingRef`, applied post-merge), so a
505
502
  // shadowed profile's sampling can never reach a different provider through
@@ -300,13 +300,6 @@ const CATALOG_RECORD: CatalogRecord = {
300
300
  description: "General-purpose LLM inference call site for skill use.",
301
301
  domain: "skills",
302
302
  },
303
- advisor: {
304
- id: "advisor",
305
- displayName: "Advisor",
306
- description:
307
- "Stronger reviewer model consulted mid-task to shape or pressure-test the plan.",
308
- domain: "skills",
309
- },
310
303
  vision: {
311
304
  id: "vision",
312
305
  displayName: "Vision",
@@ -5,10 +5,7 @@ export const HeartbeatConfigSchema = z
5
5
  .object({
6
6
  enabled: z
7
7
  .boolean({ error: "heartbeat.enabled must be a boolean" })
8
- // Heartbeats are opt-in for new workspaces. Workspaces created while
9
- // the default was true keep their behavior via workspace migration
10
- // 102-preserve-heartbeat-enabled-for-existing-workspaces.
11
- .default(false)
8
+ .default(true)
12
9
  .describe("Whether periodic heartbeat checks are enabled"),
13
10
  intervalMs: z
14
11
  .number({ error: "heartbeat.intervalMs must be a number" })
@@ -64,7 +61,7 @@ export const HeartbeatConfigSchema = z
64
61
  .int("heartbeat.maxDailyRuns must be an integer")
65
62
  .positive("heartbeat.maxDailyRuns must be a positive integer")
66
63
  .nullable()
67
- .default(2)
64
+ .default(10)
68
65
  .describe(
69
66
  "Maximum heartbeats that can run per calendar day. Resets at midnight local time. Set to null for unlimited.",
70
67
  ),
@@ -79,7 +79,6 @@ export const LLMCallSiteEnum = z.enum([
79
79
  "meetConsentMonitor",
80
80
  "meetChatOpportunity",
81
81
  "inference",
82
- "advisor",
83
82
  "vision",
84
83
  "trustRuleSuggestion",
85
84
  "homeGreeting",
@@ -442,13 +441,6 @@ export const ProfileEntry = LLMConfigFragment.extend({
442
441
  * #30362 even though the schema didn't accept it until now.
443
442
  */
444
443
  status: ProfileStatusSchema.nullable().optional(),
445
- /**
446
- * Whether the advisor is active while this profile is the chat profile.
447
- * Absent/null means enabled (default on); only an explicit `false` disables
448
- * it. `.nullable()` matches `status`/`label` so the PUT route's "send null
449
- * to clear" sentinel resets it back to the default-on state.
450
- */
451
- advisorEnabled: z.boolean().nullable().optional(),
452
444
  /**
453
445
  * When present, this profile is a "mix": it carries no model config and
454
446
  * instead references a weighted list of standard profiles. The resolver
@@ -492,10 +484,9 @@ export const LLMSchema = z
492
484
  // schema level, so `LLMSchema.parse({})` yields an empty map.
493
485
  callSites: z.partialRecord(LLMCallSiteEnum, LLMCallSiteConfig).default({}),
494
486
  activeProfile: z.string().min(1).optional(),
495
- // The profile the advisor consults (chosen under Models & Services). It is
496
- // excluded from the chat-profile pickers so it can't be selected as the
497
- // assistant's chat model. Absent falls back to the `advisor` call-site
498
- // default (`quality-optimized`).
487
+ // The profile the advisor role consults when spawned as a subagent (chosen
488
+ // under Models & Services). It is excluded from the chat-profile pickers so
489
+ // it can't be selected as the assistant's chat model.
499
490
  advisorProfile: z.string().min(1).optional(),
500
491
  // TTL bounds for inference profile sessions. `defaultTtlSeconds` is read by
501
492
  // the CLI to apply when `--ttl` is omitted; the daemon handler itself only
@@ -84,7 +84,7 @@ export const MemoryWorkerConfigSchema = z
84
84
  .boolean({ error: "memory.worker.enabled must be a boolean" })
85
85
  .default(false)
86
86
  .describe(
87
- "Whether the memory jobs worker runs as a separate OS process spawned at assistant startup (the `assistant memory worker` implementation) instead of on the assistant's main event loop. Only affects startup; shutdown stops whichever worker is actually running.",
87
+ "Whether the memory jobs worker runs as a separate OS process instead of the assistant's synchronous in-process runner. The assistant's worker supervisor re-reads this flag on every poll: while it is set, the in-process runner stands down (the out-of-process worker, spawned at startup when set, owns the queue); while it is unset, the in-process runner drains the queue. `assistant memory worker start`/`stop` flip the flag (and spawn/stop the worker process) to switch modes at runtime without a restart.",
88
88
  ),
89
89
  })
90
90
  .describe("Memory jobs worker process configuration");
@@ -84,9 +84,6 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
84
84
  effort: "high",
85
85
  thinking: { enabled: true, streamThinking: true },
86
86
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
87
- // This is the advisor's own (strongest) profile: when it's also the chat
88
- // profile there's nothing stronger to consult, so the advisor defaults off.
89
- advisorEnabled: false,
90
87
  },
91
88
  // Served by DeepSeek V4 Flash on Fireworks via managed platform inference: a
92
89
  // fast, low-cost open model. `model` is pinned explicitly rather than
@@ -224,11 +221,9 @@ export type SeedInferenceProfilesOptions = {
224
221
  * `cost-optimized`): reconciled from the code templates on every boot —
225
222
  * on-platform and off-platform alike — so Vellum can push model/config
226
223
  * updates to customers in a release without a workspace migration. The
227
- * templates own all profile content; `label`, `status`, `advisorEnabled`,
228
- * and `topP` are user overrides that survive reseeds. Of those, only
229
- * `label`, `status`, and `topP` are editable through the managed PUT route
230
- * allowlist; `advisorEnabled` is edited via the generic config path but is
231
- * still preserved here so it survives reboots.
224
+ * templates own all profile content; `label`, `status`, and `topP` are user
225
+ * overrides that survive reseeds and are editable through the managed PUT
226
+ * route allowlist.
232
227
  * Platform overlays (`preserveProfileNames`) take precedence for the boot
233
228
  * they are supplied.
234
229
  *
@@ -283,14 +278,11 @@ export function seedInferenceProfiles(
283
278
  // as usual.
284
279
  //
285
280
  // A whitelist of user-editable fields survives the reconcile: `label`
286
- // (display rename), `status` (active/disabled toggle), `advisorEnabled`
287
- // (per-profile advisor toggle), and `topP` (sampling override) — the only
288
- // fields a user may override. The managed PUT route
289
- // `/v1/config/llm/profiles/:name` lets users patch `label`, `status`, and
290
- // `topP` on managed profiles without duplicating (its editable allowlist,
291
- // `MANAGED_PROFILE_EDITABLE_KEYS`, deliberately excludes `advisorEnabled`,
292
- // which is set through the generic config path). We honor every one of
293
- // these overrides across reseeds or they'd silently revert on every boot.
281
+ // (display rename), `status` (active/disabled toggle), and `topP`
282
+ // (sampling override) — the only fields a user may override. The managed
283
+ // PUT route `/v1/config/llm/profiles/:name` lets users patch these on
284
+ // managed profiles without duplicating. We honor every one of these
285
+ // overrides across reseeds or they'd silently revert on every boot.
294
286
  // Carry by key-presence rather than truthiness so an explicit `null` (user
295
287
  // cleared the field) survives too.
296
288
  //
@@ -362,12 +354,6 @@ export function seedInferenceProfiles(
362
354
  : previous.label;
363
355
  }
364
356
  if ("status" in previous) next.status = previous.status;
365
- // The per-profile advisor toggle is a user override — preserve it across
366
- // reseeds so a user's choice survives reboots (the template value only
367
- // seeds the initial default, e.g. off for quality-optimized).
368
- if ("advisorEnabled" in previous) {
369
- next.advisorEnabled = previous.advisorEnabled;
370
- }
371
357
  // `topP` is user-editable on managed profiles (see the managed-profile
372
358
  // editable allowlist on the PUT route) — preserve a user override across
373
359
  // reseeds, including an explicit `null` clear, or it would silently revert
@@ -440,19 +426,32 @@ export function seedInferenceProfiles(
440
426
  }
441
427
  }
442
428
 
443
- // Advisor profile: default to the strongest managed profile when unset, so
444
- // the advisor consults `frontier` (Anthropic Opus) out of the box, falling
445
- // back to `quality-optimized` if `frontier` is unavailable. The `frontier`
446
- // arm requires managed ownership: the seed loop above leaves a user-owned
447
- // profile named `frontier` in place, and pointing the advisor at that would
448
- // consult an arbitrary user model. Guarded on existence so it never names a
449
- // missing profile (superRefine rejects that); off-platform/BYOK installs can
450
- // repoint it at one of their own profiles.
451
- if (readString(llm.advisorProfile) === undefined) {
452
- if (readObject(profiles["frontier"])?.source === "managed") {
453
- llm.advisorProfile = "frontier";
454
- } else if (readObject(profiles["quality-optimized"]) !== null) {
455
- llm.advisorProfile = "quality-optimized";
429
+ // Advisor profile: BYOK hatches default to the strongest personal profile
430
+ // backed by the entered provider key. Managed-profile hatches and registered
431
+ // platform installs default to the strongest active managed profile.
432
+ const requestedAdvisorProfile = readString(llm.advisorProfile);
433
+ const requestedAdvisorEntry =
434
+ requestedAdvisorProfile !== undefined
435
+ ? readObject(profiles[requestedAdvisorProfile])
436
+ : null;
437
+ const requestedAdvisorIsDisabledManaged =
438
+ requestedAdvisorEntry?.source === "managed" &&
439
+ requestedAdvisorEntry.status === "disabled";
440
+ const preferPersonalAdvisor =
441
+ userConnectionName !== undefined &&
442
+ hatchSelectedManagedConnection === undefined;
443
+ if (
444
+ requestedAdvisorProfile === undefined ||
445
+ requestedAdvisorIsDisabledManaged
446
+ ) {
447
+ const defaultAdvisorProfile = selectDefaultAdvisorProfile(
448
+ profiles,
449
+ preferPersonalAdvisor,
450
+ );
451
+ if (defaultAdvisorProfile) {
452
+ llm.advisorProfile = defaultAdvisorProfile;
453
+ } else if (requestedAdvisorIsDisabledManaged) {
454
+ delete llm.advisorProfile;
456
455
  }
457
456
  }
458
457
 
@@ -592,6 +591,48 @@ function readString(value: unknown): string | undefined {
592
591
  return typeof value === "string" && value.length > 0 ? value : undefined;
593
592
  }
594
593
 
594
+ function selectDefaultAdvisorProfile(
595
+ profiles: Record<string, Record<string, unknown>>,
596
+ preferPersonalProfile: boolean,
597
+ ): string | undefined {
598
+ const personal = firstActiveProfile(profiles, [
599
+ "custom-quality-optimized",
600
+ "custom-balanced",
601
+ "custom-cost-optimized",
602
+ ]);
603
+ const managed = firstActiveManagedProfile(profiles, [
604
+ "frontier",
605
+ "quality-optimized",
606
+ "balanced",
607
+ "cost-optimized",
608
+ ]);
609
+ return preferPersonalProfile ? (personal ?? managed) : (managed ?? personal);
610
+ }
611
+
612
+ function firstActiveProfile(
613
+ profiles: Record<string, Record<string, unknown>>,
614
+ names: string[],
615
+ ): string | undefined {
616
+ for (const name of names) {
617
+ const profile = readObject(profiles[name]);
618
+ if (profile && profile.status !== "disabled") return name;
619
+ }
620
+ return undefined;
621
+ }
622
+
623
+ function firstActiveManagedProfile(
624
+ profiles: Record<string, Record<string, unknown>>,
625
+ names: string[],
626
+ ): string | undefined {
627
+ for (const name of names) {
628
+ const profile = readObject(profiles[name]);
629
+ if (profile?.source === "managed" && profile.status !== "disabled") {
630
+ return name;
631
+ }
632
+ }
633
+ return undefined;
634
+ }
635
+
595
636
  function getHatchSelectedManagedConnection(
596
637
  llm: Record<string, unknown>,
597
638
  profiles: Record<string, Record<string, unknown>>,
@@ -117,9 +117,6 @@ function enableProfile(
117
117
  // Preserve user-owned overrides across reconciles.
118
118
  if ("label" in previous) next.label = previous.label;
119
119
  if ("status" in previous) next.status = previous.status;
120
- if ("advisorEnabled" in previous) {
121
- next.advisorEnabled = previous.advisorEnabled;
122
- }
123
120
  if ("topP" in previous) next.topP = previous.topP;
124
121
  }
125
122