@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.4

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 (209) hide show
  1. package/AGENTS.md +8 -2
  2. package/ARCHITECTURE.md +2 -0
  3. package/docs/architecture/memory.md +15 -0
  4. package/docs/browser-use-architecture-phase2.md +128 -56
  5. package/docs/flux-turn-detection-spike.md +11 -6
  6. package/docs/guardian-request-flow.md +35 -0
  7. package/knip.json +3 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  10. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  11. package/openapi.yaml +136 -76
  12. package/package.json +1 -1
  13. package/scripts/write-plugin-api-shim.ts +10 -0
  14. package/src/__tests__/app-control-flow.test.ts +1 -0
  15. package/src/__tests__/approval-routes-http.test.ts +2 -2
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  17. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  18. package/src/__tests__/compaction-events.test.ts +8 -10
  19. package/src/__tests__/conversation-agent-loop.test.ts +4 -1
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  21. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  22. package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
  23. package/src/__tests__/conversation-queue.test.ts +39 -62
  24. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  27. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  28. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
  30. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  31. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  36. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  37. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  38. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  39. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  40. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  41. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  42. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  43. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  44. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  45. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  46. package/src/__tests__/document-sync-tags.test.ts +0 -75
  47. package/src/__tests__/file-ops-service.test.ts +163 -30
  48. package/src/__tests__/filesystem-tools.test.ts +23 -24
  49. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  50. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  51. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  52. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  53. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  54. package/src/__tests__/media-generate-image.test.ts +131 -21
  55. package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
  56. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  57. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
  58. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  59. package/src/__tests__/qdrant-manager.test.ts +14 -1
  60. package/src/__tests__/run-due-schedules.test.ts +21 -0
  61. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  62. package/src/__tests__/schedule-routes.test.ts +23 -0
  63. package/src/__tests__/schedule-store.test.ts +17 -0
  64. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  65. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  66. package/src/__tests__/starter-task-flow.test.ts +5 -4
  67. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  68. package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
  69. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  70. package/src/__tests__/subagent-tools.test.ts +81 -101
  71. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  72. package/src/__tests__/tool-executor.test.ts +5 -1
  73. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  74. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  75. package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
  76. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  77. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  78. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  79. package/src/acp/session-manager.ts +8 -1
  80. package/src/api/events/host-file.ts +2 -2
  81. package/src/api/surfaces.ts +5 -0
  82. package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
  83. package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
  84. package/src/calls/voice-session-bridge.ts +44 -25
  85. package/src/calls/voice-triage-escalate.ts +1 -0
  86. package/src/cli/bundled-modules.ts +29 -0
  87. package/src/cli/commands/db/repair.ts +4 -8
  88. package/src/cli/commands/domain.ts +6 -3
  89. package/src/cli/commands/email.ts +6 -3
  90. package/src/cli/commands/keys.ts +8 -3
  91. package/src/cli/commands/plugins.ts +85 -36
  92. package/src/cli/commands/schedules.ts +35 -1
  93. package/src/cli/lib/bundled-marketplace.json +1 -1
  94. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  95. package/src/config/assistant-feature-flags.ts +36 -15
  96. package/src/config/balanced-model-experiment.ts +35 -0
  97. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  98. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  99. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  100. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  101. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  102. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  103. package/src/config/call-site-defaults.ts +7 -0
  104. package/src/config/default-profile-catalog.ts +96 -4
  105. package/src/config/feature-flag-registry.json +11 -11
  106. package/src/config/skills.ts +9 -2
  107. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  108. package/src/daemon/conversation-agent-loop.ts +14 -13
  109. package/src/daemon/conversation-notifiers.ts +11 -9
  110. package/src/daemon/conversation-process.ts +0 -27
  111. package/src/daemon/conversation-runtime-assembly.ts +9 -2
  112. package/src/daemon/conversation-store.ts +4 -4
  113. package/src/daemon/conversation-surfaces.ts +27 -13
  114. package/src/daemon/conversation-tool-setup.ts +2 -4
  115. package/src/daemon/conversation.ts +104 -51
  116. package/src/daemon/doordash-steps.ts +2 -2
  117. package/src/daemon/lifecycle.ts +14 -1
  118. package/src/daemon/process-message.ts +0 -13
  119. package/src/daemon/windows-compiled-entry.ts +4 -0
  120. package/src/documents/document-store.ts +5 -235
  121. package/src/hooks/types.ts +5 -0
  122. package/src/ipc/gateway-flag-listener.ts +17 -3
  123. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
  124. package/src/live-voice/live-voice-manager.ts +16 -3
  125. package/src/live-voice/live-voice-session.ts +63 -9
  126. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  127. package/src/monitoring/control.ts +1 -0
  128. package/src/monitoring/db-integrity-sample.ts +4 -5
  129. package/src/notifications/AGENTS.md +2 -0
  130. package/src/notifications/approval-card-data.ts +33 -0
  131. package/src/permissions/prompter.ts +1 -5
  132. package/src/persistence/conversation-queries.ts +66 -16
  133. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  134. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  135. package/src/persistence/schema/documents.ts +4 -4
  136. package/src/plugin-api/constants.ts +8 -0
  137. package/src/plugin-api/index.ts +5 -1
  138. package/src/plugin-api/webhook-url.ts +13 -11
  139. package/src/plugins/defaults/main.ts +6 -7
  140. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  141. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  142. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  143. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  144. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  145. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  146. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  147. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  148. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  149. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  150. package/src/plugins/mtime-cache.ts +17 -0
  151. package/src/prompts/templates/system-sections.ts +0 -7
  152. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  153. package/src/providers/__tests__/retry-callsite.test.ts +20 -0
  154. package/src/providers/openai/chat-completions-provider.ts +11 -1
  155. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
  156. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
  157. package/src/routes/control.ts +1 -0
  158. package/src/routes/route-host-client.ts +1 -0
  159. package/src/runtime/AGENTS.md +16 -17
  160. package/src/runtime/agent-wake.ts +15 -12
  161. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
  162. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  163. package/src/runtime/routes/conversation-list-routes.ts +54 -22
  164. package/src/runtime/routes/conversation-management-routes.ts +2 -3
  165. package/src/runtime/routes/conversation-routes.ts +11 -13
  166. package/src/runtime/routes/documents-routes.ts +3 -222
  167. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  168. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  169. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  170. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  171. package/src/runtime/routes/schedule-routes.ts +94 -7
  172. package/src/runtime/routes/workspace-routes.ts +0 -9
  173. package/src/runtime/routes/workspace-utils.ts +3 -13
  174. package/src/runtime/services/conversation-serializer.ts +7 -2
  175. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  176. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  177. package/src/schedule/plugin-schedule-availability.ts +58 -0
  178. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  179. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  180. package/src/schedule/schedule-store.ts +5 -1
  181. package/src/schedule/scheduler.ts +9 -4
  182. package/src/schedule/worker-control.ts +1 -0
  183. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  184. package/src/subagent/consult-context.ts +11 -11
  185. package/src/subagent/consult-prompt.ts +26 -35
  186. package/src/subagent/manager.ts +20 -37
  187. package/src/subagent/notify.ts +7 -1
  188. package/src/subagent/types.ts +15 -13
  189. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  190. package/src/tools/acp/spawn.ts +6 -4
  191. package/src/tools/filesystem/read.ts +27 -10
  192. package/src/tools/host-filesystem/read.ts +27 -15
  193. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  194. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  195. package/src/tools/shared/filesystem/types.ts +5 -5
  196. package/src/tools/skills/scaffold-managed.ts +25 -7
  197. package/src/tools/subagent/spawn.ts +28 -88
  198. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  199. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  200. package/src/util/logger.ts +16 -0
  201. package/src/util/worker-process.ts +37 -4
  202. package/src/windows-compiled-cli.ts +32 -0
  203. package/src/windows-compiled-entry.ts +4 -0
  204. package/src/windows-compiled-logger.ts +6 -0
  205. package/src/windows-compiled-worker-entry.ts +29 -0
  206. package/src/__tests__/document-workspace-file.test.ts +0 -467
  207. package/src/daemon/interactive-turn-sender.ts +0 -59
  208. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  209. package/src/subagent/consult-transcript.ts +0 -90
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
3
3
  import type { Command } from "commander";
4
4
 
5
5
  import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
6
+ import { resolveBundledCliModule } from "../bundled-modules.js";
6
7
  import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
7
8
  import { registerCommand } from "../lib/register-command.js";
8
9
  import { getCliLogger } from "../logger.js";
@@ -19,9 +20,11 @@ const log = getCliLogger("domain");
19
20
  * synchronously, so there is no place to await a module load.
20
21
  */
21
22
  function envDomains(): { apexDomain: string; baseDomain: string } {
22
- const { getApexDomain, getAssistantDomain } = loadModule(
23
- "../../config/env.js",
24
- ) as typeof import("../../config/env.js");
23
+ const { getApexDomain, getAssistantDomain } = resolveBundledCliModule(
24
+ "configEnv",
25
+ () =>
26
+ loadModule("../../config/env.js") as typeof import("../../config/env.js"),
27
+ );
25
28
  return { apexDomain: getApexDomain(), baseDomain: getAssistantDomain() };
26
29
  }
27
30
 
@@ -16,6 +16,7 @@ import {
16
16
  } from "../../ipc/cli-client.js";
17
17
  import { guessMimeType } from "../../util/mime-type.js";
18
18
  import { readStdinSync } from "../../util/read-stdin.js";
19
+ import { resolveBundledCliModule } from "../bundled-modules.js";
19
20
  import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
20
21
  import { registerCommand } from "../lib/register-command.js";
21
22
  import { getCliLogger } from "../logger.js";
@@ -60,9 +61,11 @@ function handleEmailIpcError(
60
61
  * synchronously, so there is no place to await a module load.
61
62
  */
62
63
  function assistantDomain(): string {
63
- const { getAssistantDomain } = loadModule(
64
- "../../config/env.js",
65
- ) as typeof import("../../config/env.js");
64
+ const { getAssistantDomain } = resolveBundledCliModule(
65
+ "configEnv",
66
+ () =>
67
+ loadModule("../../config/env.js") as typeof import("../../config/env.js"),
68
+ );
66
69
  return getAssistantDomain();
67
70
  }
68
71
 
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
2
2
 
3
3
  import type { Command } from "commander";
4
4
 
5
+ import { resolveBundledCliModule } from "../bundled-modules.js";
5
6
  import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
6
7
  import { registerCommand } from "../lib/register-command.js";
7
8
  import { log } from "../logger.js";
@@ -16,9 +17,13 @@ const loadModule = createRequire(import.meta.url);
16
17
  * callbacks synchronously, so there is no place to await a module load.
17
18
  */
18
19
  function apiKeyProviders(): readonly string[] {
19
- const { API_KEY_PROVIDERS } = loadModule(
20
- "../../providers/provider-secret-catalog.js",
21
- ) as typeof import("../../providers/provider-secret-catalog.js");
20
+ const { API_KEY_PROVIDERS } = resolveBundledCliModule(
21
+ "providerSecretCatalog",
22
+ () =>
23
+ loadModule(
24
+ "../../providers/provider-secret-catalog.js",
25
+ ) as typeof import("../../providers/provider-secret-catalog.js"),
26
+ );
22
27
  return API_KEY_PROVIDERS;
23
28
  }
24
29
 
@@ -15,6 +15,7 @@ import { cliIpcCall } from "../../ipc/cli-client.js";
15
15
  import { stripAnsiAndControlChars } from "../../util/ansi.js";
16
16
  import { getWorkspacePluginsDir } from "../../util/platform.js";
17
17
  import { truncate } from "../../util/truncate.js";
18
+ import { resolveBundledCliModule } from "../bundled-modules.js";
18
19
  import { yellow } from "../lib/cli-colors.js";
19
20
  import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
20
21
  import { confirmPrompt } from "../lib/confirm-prompt.js";
@@ -61,64 +62,112 @@ const loadModule = createRequire(import.meta.url);
61
62
  /** Lazy accessors: each getter loads its implementation module on first use. */
62
63
  const libs = {
63
64
  get catalogCache() {
64
- return loadModule(
65
- "../lib/plugin-catalog-cache.js",
66
- ) as typeof import("../lib/plugin-catalog-cache.js");
65
+ return resolveBundledCliModule(
66
+ "pluginCatalogCache",
67
+ () =>
68
+ loadModule(
69
+ "../lib/plugin-catalog-cache.js",
70
+ ) as typeof import("../lib/plugin-catalog-cache.js"),
71
+ );
67
72
  },
68
73
  get catalogLocal() {
69
- return loadModule(
70
- "../lib/plugin-catalog-local.js",
71
- ) as typeof import("../lib/plugin-catalog-local.js");
74
+ return resolveBundledCliModule(
75
+ "pluginCatalogLocal",
76
+ () =>
77
+ loadModule(
78
+ "../lib/plugin-catalog-local.js",
79
+ ) as typeof import("../lib/plugin-catalog-local.js"),
80
+ );
72
81
  },
73
82
  get diff() {
74
- return loadModule(
75
- "../lib/diff-plugin.js",
76
- ) as typeof import("../lib/diff-plugin.js");
83
+ return resolveBundledCliModule(
84
+ "pluginDiff",
85
+ () =>
86
+ loadModule(
87
+ "../lib/diff-plugin.js",
88
+ ) as typeof import("../lib/diff-plugin.js"),
89
+ );
77
90
  },
78
91
  get inspect() {
79
- return loadModule(
80
- "../lib/inspect-plugin.js",
81
- ) as typeof import("../lib/inspect-plugin.js");
92
+ return resolveBundledCliModule(
93
+ "pluginInspect",
94
+ () =>
95
+ loadModule(
96
+ "../lib/inspect-plugin.js",
97
+ ) as typeof import("../lib/inspect-plugin.js"),
98
+ );
82
99
  },
83
100
  get installGitHub() {
84
- return loadModule(
85
- "../lib/install-from-github.js",
86
- ) as typeof import("../lib/install-from-github.js");
101
+ return resolveBundledCliModule(
102
+ "pluginInstallGitHub",
103
+ () =>
104
+ loadModule(
105
+ "../lib/install-from-github.js",
106
+ ) as typeof import("../lib/install-from-github.js"),
107
+ );
87
108
  },
88
109
  get installPlatform() {
89
- return loadModule(
90
- "../lib/install-from-platform.js",
91
- ) as typeof import("../lib/install-from-platform.js");
110
+ return resolveBundledCliModule(
111
+ "pluginInstallPlatform",
112
+ () =>
113
+ loadModule(
114
+ "../lib/install-from-platform.js",
115
+ ) as typeof import("../lib/install-from-platform.js"),
116
+ );
92
117
  },
93
118
  get installed() {
94
- return loadModule(
95
- "../lib/list-installed-plugins.js",
96
- ) as typeof import("../lib/list-installed-plugins.js");
119
+ return resolveBundledCliModule(
120
+ "pluginInstalled",
121
+ () =>
122
+ loadModule(
123
+ "../lib/list-installed-plugins.js",
124
+ ) as typeof import("../lib/list-installed-plugins.js"),
125
+ );
97
126
  },
98
127
  get pinHistory() {
99
- return loadModule(
100
- "../lib/plugin-pin-history.js",
101
- ) as typeof import("../lib/plugin-pin-history.js");
128
+ return resolveBundledCliModule(
129
+ "pluginPinHistory",
130
+ () =>
131
+ loadModule(
132
+ "../lib/plugin-pin-history.js",
133
+ ) as typeof import("../lib/plugin-pin-history.js"),
134
+ );
102
135
  },
103
136
  get search() {
104
- return loadModule(
105
- "../lib/search-plugins.js",
106
- ) as typeof import("../lib/search-plugins.js");
137
+ return resolveBundledCliModule(
138
+ "pluginSearch",
139
+ () =>
140
+ loadModule(
141
+ "../lib/search-plugins.js",
142
+ ) as typeof import("../lib/search-plugins.js"),
143
+ );
107
144
  },
108
145
  get surfaces() {
109
- return loadModule(
110
- "../lib/plugin-surfaces.js",
111
- ) as typeof import("../lib/plugin-surfaces.js");
146
+ return resolveBundledCliModule(
147
+ "pluginSurfaces",
148
+ () =>
149
+ loadModule(
150
+ "../lib/plugin-surfaces.js",
151
+ ) as typeof import("../lib/plugin-surfaces.js"),
152
+ );
112
153
  },
113
154
  get uninstall() {
114
- return loadModule(
115
- "../lib/uninstall-plugin.js",
116
- ) as typeof import("../lib/uninstall-plugin.js");
155
+ return resolveBundledCliModule(
156
+ "pluginUninstall",
157
+ () =>
158
+ loadModule(
159
+ "../lib/uninstall-plugin.js",
160
+ ) as typeof import("../lib/uninstall-plugin.js"),
161
+ );
117
162
  },
118
163
  get upgrade() {
119
- return loadModule(
120
- "../lib/upgrade-plugin.js",
121
- ) as typeof import("../lib/upgrade-plugin.js");
164
+ return resolveBundledCliModule(
165
+ "pluginUpgrade",
166
+ () =>
167
+ loadModule(
168
+ "../lib/upgrade-plugin.js",
169
+ ) as typeof import("../lib/upgrade-plugin.js"),
170
+ );
122
171
  },
123
172
  };
124
173
 
@@ -39,9 +39,39 @@ interface ScheduleRecord {
39
39
  wakeConversationId: string | null;
40
40
  sourceKey: string | null;
41
41
  userEnabled: boolean | null;
42
+ disarmReason: string | null;
42
43
  isOneShot: boolean;
43
44
  }
44
45
 
46
+ /** Plain-language copy for each `disarmReason` the daemon sends. */
47
+ const DISARM_REASON_TEXT: Record<string, string> = {
48
+ user_disabled: "turned off by you",
49
+ plugin_removed: "plugin removed",
50
+ plugin_disabled: "plugin disabled",
51
+ declaration_removed: "removed from plugin",
52
+ declaration_disabled: "paused by plugin",
53
+ };
54
+
55
+ /**
56
+ * Why a schedule is off, when the daemon knows. An off row with no reason (an
57
+ * imperative schedule, or a plugin row from a daemon that predates the field)
58
+ * reads as plain "disabled".
59
+ */
60
+ function describeDisarmReason(schedule: ScheduleRecord): string | null {
61
+ if (schedule.enabled || !schedule.disarmReason) {
62
+ return null;
63
+ }
64
+ return DISARM_REASON_TEXT[schedule.disarmReason] ?? null;
65
+ }
66
+
67
+ function formatEnabledCell(schedule: ScheduleRecord): string {
68
+ if (schedule.enabled) {
69
+ return "enabled";
70
+ }
71
+ const reason = describeDisarmReason(schedule);
72
+ return reason ? `off (${reason})` : "disabled";
73
+ }
74
+
45
75
  interface ListSchedulesResponse {
46
76
  schedules: ScheduleRecord[];
47
77
  }
@@ -116,7 +146,7 @@ export function registerSchedulesCommand(program: Command): void {
116
146
  const rows = entries.map((schedule) => ({
117
147
  id: schedule.id,
118
148
  name: schedule.name,
119
- enabled: schedule.enabled ? "enabled" : "disabled",
149
+ enabled: formatEnabledCell(schedule),
120
150
  mode: schedule.mode,
121
151
  source: describeScheduleSource(schedule.sourceKey) ?? "",
122
152
  schedule: describeSchedule(schedule),
@@ -247,6 +277,10 @@ export function registerSchedulesCommand(program: Command): void {
247
277
  ["Source conversation", schedule.createdFromConversationId ?? "—"],
248
278
  ["Plugin", describeScheduleSource(schedule.sourceKey) ?? "-"],
249
279
  ];
280
+ const disarmReason = describeDisarmReason(schedule);
281
+ if (disarmReason) {
282
+ fields.push(["Paused", disarmReason]);
283
+ }
250
284
  const labelWidth = Math.max(...fields.map(([label]) => label.length));
251
285
  for (const [label, value] of fields) {
252
286
  log.info(`${`${label}:`.padEnd(labelWidth + 2)}${value}`);
@@ -232,7 +232,7 @@
232
232
  "source": {
233
233
  "source": "github",
234
234
  "repo": "vellum-ai/level-up",
235
- "ref": "d3cbb50f8aba1f8320cd2d59ea3a85d0a4e9210b"
235
+ "ref": "4357028acc339c6d6f718d5d6ab279ebf198673b"
236
236
  },
237
237
  "description": "Surfaces a first-class \"Level Up\" diff card whenever the assistant edits its own skills or plugins, so you can see how it improved itself after the fact.",
238
238
  "category": "developer",
@@ -0,0 +1,278 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { afterEach, describe, expect, test } from "bun:test";
4
+
5
+ import type { z } from "zod";
6
+
7
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
8
+ import {
9
+ catalogMaxOutputTokens,
10
+ isModelInCatalog,
11
+ } from "../../providers/model-catalog.js";
12
+ import { getManagedUpstream } from "../../providers/vellum-model-routing.js";
13
+ import { BALANCED_MODEL_EXPERIMENT_FLAG_KEY } from "../balanced-model-experiment.js";
14
+ import {
15
+ CODE_DEFAULT_PROFILE_ENTRIES,
16
+ getEffectiveProfilesForProvider,
17
+ resolveDefaultProfileForProvider,
18
+ } from "../default-profile-catalog.js";
19
+ import { clearCachedOverrides } from "../feature-flag-cache.js";
20
+ import { resolveCallSiteConfig } from "../llm-resolver.js";
21
+ import {
22
+ type DefaultProviderConfig,
23
+ LLMSchema,
24
+ type ProfileEntry,
25
+ } from "../schemas/llm.js";
26
+
27
+ // The `experiment-balanced-model-2026-08-06` arm repoints the model the managed
28
+ // Balanced profile resolves to. Both the runtime resolver and the client-facing
29
+ // profile listing go through `defaultProfileBodyForProvider`, so both are
30
+ // exercised here: a disagreement between them shows the user one model beside a
31
+ // profile that runs another.
32
+
33
+ const FLAG = BALANCED_MODEL_EXPERIMENT_FLAG_KEY;
34
+ const SHIPPED_MODEL = "gpt-5.6-luna";
35
+ const GLM_MODEL = "accounts/fireworks/models/glm-5p2";
36
+
37
+ function setArm(value: boolean | string): void {
38
+ setOverridesForTesting({ [FLAG]: value });
39
+ }
40
+
41
+ const managed: DefaultProviderConfig = { provider: "vellum" };
42
+
43
+ function llmWithActiveBalanced(
44
+ overrides: Record<string, unknown> = {},
45
+ ): z.infer<typeof LLMSchema> {
46
+ return LLMSchema.parse({
47
+ profiles: {},
48
+ profileOrder: [],
49
+ activeProfile: "balanced",
50
+ defaultProvider: managed,
51
+ ...overrides,
52
+ });
53
+ }
54
+
55
+ afterEach(() => {
56
+ clearCachedOverrides();
57
+ });
58
+
59
+ describe("balanced-model experiment arms", () => {
60
+ test("terra repoints mainAgent at gpt-5.6-terra on the managed connection", () => {
61
+ setArm("terra");
62
+ const resolved = resolveCallSiteConfig(
63
+ "mainAgent",
64
+ llmWithActiveBalanced(),
65
+ );
66
+ expect(resolved.model).toBe("gpt-5.6-terra");
67
+ expect(resolved.provider).toBe("vellum");
68
+ expect(getManagedUpstream("gpt-5.6-terra")).toBe("openai");
69
+
70
+ const entry = resolveDefaultProfileForProvider(
71
+ undefined,
72
+ "balanced",
73
+ managed,
74
+ );
75
+ expect(entry?.model).toBe("gpt-5.6-terra");
76
+ expect(entry?.provider).toBe("vellum");
77
+ // Routing-identity providers derive their connection per request.
78
+ expect(entry?.provider_connection).toBeUndefined();
79
+ expect(entry?.source).toBe("managed");
80
+ });
81
+
82
+ test("glm-5p2 repoints mainAgent at GLM 5.2 within the model's output cap", () => {
83
+ setArm("glm-5p2");
84
+ const resolved = resolveCallSiteConfig(
85
+ "mainAgent",
86
+ llmWithActiveBalanced(),
87
+ );
88
+ expect(resolved.model).toBe(GLM_MODEL);
89
+ expect(resolved.provider).toBe("vellum");
90
+
91
+ const upstream = getManagedUpstream(GLM_MODEL);
92
+ expect(upstream).toBe("fireworks");
93
+ const cap = catalogMaxOutputTokens(upstream as string, GLM_MODEL);
94
+ expect(cap).toBeDefined();
95
+ expect(resolved.maxTokens).toBeLessThanOrEqual(cap as number);
96
+ // Only the model moves: the shipped token budget stands.
97
+ expect(resolved.maxTokens).toBe(
98
+ CODE_DEFAULT_PROFILE_ENTRIES.balanced.maxTokens as number,
99
+ );
100
+ });
101
+
102
+ test("an arm changes only the model of the managed balanced body", () => {
103
+ setArm("terra");
104
+ const entry = resolveDefaultProfileForProvider(
105
+ undefined,
106
+ "balanced",
107
+ managed,
108
+ );
109
+ const shipped = CODE_DEFAULT_PROFILE_ENTRIES.balanced;
110
+ expect(entry).toEqual({ ...shipped, model: "gpt-5.6-terra" });
111
+ });
112
+
113
+ test("the other default profiles are untouched by an arm", () => {
114
+ setArm("glm-5p2");
115
+ for (const key of [
116
+ "quality-optimized",
117
+ "cost-optimized",
118
+ "latency-optimized",
119
+ ]) {
120
+ expect(
121
+ resolveDefaultProfileForProvider(undefined, key, managed)?.model,
122
+ ).toBe(CODE_DEFAULT_PROFILE_ENTRIES[key].model);
123
+ }
124
+ });
125
+
126
+ test("every pinned arm model is managed-routable and in its upstream catalog", () => {
127
+ for (const model of ["gpt-5.6-terra", GLM_MODEL]) {
128
+ const upstream = getManagedUpstream(model);
129
+ expect(upstream).not.toBeNull();
130
+ expect(isModelInCatalog(upstream as string, model)).toBe(true);
131
+ }
132
+ });
133
+ });
134
+
135
+ describe("balanced-model experiment fallbacks", () => {
136
+ const shippedCases: [string, () => void][] = [
137
+ ["the flag is unset", () => clearCachedOverrides()],
138
+ ["no override is present", () => setOverridesForTesting({})],
139
+ ["the arm is control", () => setArm("control")],
140
+ ["the arm is an unknown string", () => setArm("gpt-9-does-not-exist")],
141
+ ["the arm is the empty string", () => setArm("")],
142
+ ["the value is boolean true", () => setArm(true)],
143
+ ["the value is boolean false", () => setArm(false)],
144
+ // The arm is remote input, so it can name an Object.prototype member. The
145
+ // pin table is a Map, which has no such members to inherit.
146
+ ["the arm is constructor", () => setArm("constructor")],
147
+ ["the arm is toString", () => setArm("toString")],
148
+ ["the arm is valueOf", () => setArm("valueOf")],
149
+ ["the arm is __proto__", () => setArm("__proto__")],
150
+ ["the arm is hasOwnProperty", () => setArm("hasOwnProperty")],
151
+ ];
152
+
153
+ for (const [label, seed] of shippedCases) {
154
+ test(`balanced stays on the shipped model when ${label}`, () => {
155
+ seed();
156
+ expect(
157
+ resolveDefaultProfileForProvider(undefined, "balanced", managed)?.model,
158
+ ).toBe(SHIPPED_MODEL);
159
+ expect(
160
+ resolveCallSiteConfig("mainAgent", llmWithActiveBalanced()).model,
161
+ ).toBe(SHIPPED_MODEL);
162
+ });
163
+ }
164
+
165
+ test("the shipped catalog body is the control arm", () => {
166
+ expect(CODE_DEFAULT_PROFILE_ENTRIES.balanced.model).toBe(SHIPPED_MODEL);
167
+ });
168
+
169
+ test("the registry default is control, which the override-only flag read assumes", () => {
170
+ const registryPath = join(
171
+ import.meta.dirname,
172
+ "..",
173
+ "..",
174
+ "..",
175
+ "..",
176
+ "meta",
177
+ "feature-flags",
178
+ "feature-flag-registry.json",
179
+ );
180
+ const registry = JSON.parse(readFileSync(registryPath, "utf-8")) as {
181
+ flags: {
182
+ key: string;
183
+ scope: string;
184
+ defaultEnabled: boolean | string;
185
+ values?: string[];
186
+ }[];
187
+ };
188
+ const flag = registry.flags.find((entry) => entry.key === FLAG);
189
+ expect(flag).toBeDefined();
190
+ expect(flag?.scope).toBe("assistant");
191
+ expect(flag?.defaultEnabled).toBe("control");
192
+ expect(flag?.values).toEqual(["control", "terra", "glm-5p2"]);
193
+ });
194
+ });
195
+
196
+ describe("balanced-model experiment boundaries", () => {
197
+ test("a user-owned balanced shadow wins over the arm", () => {
198
+ setArm("terra");
199
+ const profiles: Record<string, ProfileEntry> = {
200
+ balanced: { source: "user", provider: "openai", model: "gpt-5.5" },
201
+ };
202
+ expect(
203
+ resolveDefaultProfileForProvider(profiles, "balanced", managed),
204
+ ).toEqual(profiles.balanced);
205
+ expect(
206
+ resolveCallSiteConfig("mainAgent", llmWithActiveBalanced({ profiles }))
207
+ .model,
208
+ ).toBe("gpt-5.5");
209
+ });
210
+
211
+ test("a managed-source stub still resolves to the arm's body", () => {
212
+ setArm("terra");
213
+ const profiles: Record<string, ProfileEntry> = {
214
+ balanced: { source: "managed", label: "My Balanced" },
215
+ };
216
+ const entry = resolveDefaultProfileForProvider(
217
+ profiles,
218
+ "balanced",
219
+ managed,
220
+ );
221
+ expect(entry?.model).toBe("gpt-5.6-terra");
222
+ expect(entry?.label).toBe("My Balanced");
223
+ });
224
+
225
+ test("the chatgpt and BYOK columns stay out of the experiment", () => {
226
+ setArm("terra");
227
+ for (const provider of ["anthropic", "openai", "chatgpt"] as const) {
228
+ const armed = resolveDefaultProfileForProvider(undefined, "balanced", {
229
+ provider,
230
+ });
231
+ clearCachedOverrides();
232
+ const shipped = resolveDefaultProfileForProvider(undefined, "balanced", {
233
+ provider,
234
+ });
235
+ expect(armed).toEqual(shipped as ProfileEntry);
236
+ expect(armed?.provider).toBe(provider);
237
+ setArm("terra");
238
+ }
239
+ });
240
+
241
+ test("an install with no defaultProvider still picks up the arm", () => {
242
+ setArm("glm-5p2");
243
+ expect(
244
+ resolveDefaultProfileForProvider(undefined, "balanced", null)?.model,
245
+ ).toBe(GLM_MODEL);
246
+ });
247
+ });
248
+
249
+ describe("client-facing profile listing", () => {
250
+ test("reports the arm's model so the UI matches what runs", () => {
251
+ setArm("terra");
252
+ const profiles = getEffectiveProfilesForProvider(undefined, managed);
253
+ expect(profiles.balanced?.model).toBe("gpt-5.6-terra");
254
+ expect(profiles["quality-optimized"]?.model).toBe(
255
+ CODE_DEFAULT_PROFILE_ENTRIES["quality-optimized"].model,
256
+ );
257
+ });
258
+
259
+ test("reports the shipped model on the control arm", () => {
260
+ setArm("control");
261
+ expect(
262
+ getEffectiveProfilesForProvider(undefined, managed).balanced?.model,
263
+ ).toBe(SHIPPED_MODEL);
264
+ });
265
+
266
+ test("agrees with the runtime resolver on every arm", () => {
267
+ for (const arm of ["control", "terra", "glm-5p2", "nonsense"]) {
268
+ setArm(arm);
269
+ const listed = getEffectiveProfilesForProvider(undefined, managed)
270
+ .balanced?.model;
271
+ const resolved = resolveCallSiteConfig(
272
+ "mainAgent",
273
+ llmWithActiveBalanced(),
274
+ ).model;
275
+ expect(listed).toBe(resolved as string);
276
+ }
277
+ });
278
+ });
@@ -59,6 +59,12 @@ function loadDefaultsRegistry(): FeatureFlagDefaultsRegistry {
59
59
  // Works in Docker / packaged builds where the repo-root `meta/` dir
60
60
  // is not available.
61
61
  join(thisDir, REGISTRY_FILENAME),
62
+ ...(process.platform === "win32"
63
+ ? [
64
+ // Packaged Windows CLI runtime: assets live beside the executable.
65
+ join(dirname(process.execPath), REGISTRY_FILENAME),
66
+ ]
67
+ : []),
62
68
  // Packaged macOS app layout: the daemon binary lives at
63
69
  // <App>.app/Contents/MacOS/vellum-daemon and the registry is copied
64
70
  // to <App>.app/Contents/Resources/ by build.sh. In bun --compile
@@ -190,11 +196,11 @@ const DEFAULT_INIT_RETRY_BACKOFFS_MS: readonly number[] = [
190
196
  * intentionally simulate an unreachable gateway and want immediate
191
197
  * fallback without waiting through the production schedule).
192
198
  *
193
- * No-ops when the cache is already populated — callers that want to
194
- * refresh must call `clearFeatureFlagOverridesCache()` first. This lets
195
- * tests preseed flag state via `setOverridesForTesting()` (in
196
- * `__tests__/feature-flag-test-helpers.ts`) without the gateway IPC call
197
- * clobbering their setup.
199
+ * No-ops when the cache is already populated — this is the load-time
200
+ * populate, and `refreshOverridesFromGateway()` is what re-reads a cache that
201
+ * already holds values. The no-op also lets tests preseed flag state via
202
+ * `setOverridesForTesting()` (in `__tests__/feature-flag-test-helpers.ts`)
203
+ * without the gateway IPC call clobbering their setup.
198
204
  *
199
205
  * Resolves `true` when the override cache is populated from the gateway and
200
206
  * `false` when the cache is left unset (exhausted retries / unreachable
@@ -274,10 +280,13 @@ function loadOverrides(): Record<string, boolean | string> {
274
280
  * Invalidate the cached overrides so the next call to
275
281
  * `isAssistantFeatureFlagEnabled` re-reads from the gateway.
276
282
  *
277
- * Called by `refreshOverridesFromGateway()` when the gateway pushes a
278
- * `feature_flags_changed` event, and by tests between cases to reset
279
- * module state. (Tests typically call `setOverridesForTesting()` from
280
- * `__tests__/feature-flag-test-helpers.ts`, which combines clear + seed.)
283
+ * Used by tests between cases to reset module state. (Tests typically call
284
+ * `setOverridesForTesting()` from `__tests__/feature-flag-test-helpers.ts`,
285
+ * which combines clear + seed.)
286
+ *
287
+ * Not a step in refreshing from the gateway: `refreshOverridesFromGateway()`
288
+ * swaps the cache atomically so flag values never read as unset while a
289
+ * refresh is in flight.
281
290
  */
282
291
  export function clearFeatureFlagOverridesCache(): void {
283
292
  clearCachedOverrides();
@@ -286,18 +295,30 @@ export function clearFeatureFlagOverridesCache(): void {
286
295
  /**
287
296
  * Re-fetch feature flag overrides from the gateway.
288
297
  *
289
- * Clears the cached overrides and re-runs the gateway IPC fetch without
290
- * retries (the gateway is known to be up because it just pushed an event).
291
- * Called by the gateway flag listener when a `feature_flags_changed` event
292
- * arrives.
298
+ * Runs the gateway IPC fetch without retries (the gateway is known to be up
299
+ * because it just pushed an event) and replaces the cache only once that fetch
300
+ * succeeds. Called by the gateway flag listener when a `feature_flags_changed`
301
+ * event arrives and on reconnect.
302
+ *
303
+ * The swap is atomic on purpose. The previously fetched values stay readable
304
+ * for the whole call, so a sync flag read that lands mid-refresh sees the last
305
+ * known values rather than an empty cache, and a failed fetch leaves them in
306
+ * place rather than dropping every flag to its registry default until the next
307
+ * event. A flag that selects behavior rather than gating it (an experiment
308
+ * arm, a kill switch) therefore cannot flip to its default because one IPC
309
+ * call timed out.
293
310
  *
294
311
  * Resolves `true` when the cache was repopulated from the gateway, `false`
295
312
  * when the fetch came back empty/failed — callers gate persisted-state
296
313
  * reconciliation on a `true` result.
297
314
  */
298
315
  export async function refreshOverridesFromGateway(): Promise<boolean> {
299
- clearFeatureFlagOverridesCache();
300
- return initFeatureFlagOverrides({ retryBackoffsMs: [] });
316
+ const gatewayOverrides = await fetchOverridesFromGateway();
317
+ if (Object.keys(gatewayOverrides).length === 0) {
318
+ return false;
319
+ }
320
+ setCachedOverrides(gatewayOverrides, { fromGateway: true });
321
+ return true;
301
322
  }
302
323
 
303
324
  // ---------------------------------------------------------------------------