@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.
- package/AGENTS.md +8 -2
- package/ARCHITECTURE.md +2 -0
- package/docs/architecture/memory.md +15 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +11 -6
- package/docs/guardian-request-flow.md +35 -0
- package/knip.json +3 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/openapi.yaml +136 -76
- package/package.json +1 -1
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/conversation-agent-loop.test.ts +4 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +39 -62
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +53 -0
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
- package/src/__tests__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/tool-executor.test.ts +5 -1
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- package/src/acp/session-manager.ts +8 -1
- package/src/api/events/host-file.ts +2 -2
- package/src/api/surfaces.ts +5 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
- package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
- package/src/calls/voice-session-bridge.ts +44 -25
- package/src/calls/voice-triage-escalate.ts +1 -0
- package/src/cli/bundled-modules.ts +29 -0
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
- package/src/config/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- package/src/config/call-site-defaults.ts +7 -0
- package/src/config/default-profile-catalog.ts +96 -4
- package/src/config/feature-flag-registry.json +11 -11
- package/src/config/skills.ts +9 -2
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
- package/src/daemon/conversation-agent-loop.ts +14 -13
- package/src/daemon/conversation-notifiers.ts +11 -9
- package/src/daemon/conversation-process.ts +0 -27
- package/src/daemon/conversation-runtime-assembly.ts +9 -2
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surfaces.ts +27 -13
- package/src/daemon/conversation-tool-setup.ts +2 -4
- package/src/daemon/conversation.ts +104 -51
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/process-message.ts +0 -13
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +5 -235
- package/src/hooks/types.ts +5 -0
- package/src/ipc/gateway-flag-listener.ts +17 -3
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
- package/src/live-voice/live-voice-manager.ts +16 -3
- package/src/live-voice/live-voice-session.ts +63 -9
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- package/src/monitoring/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/approval-card-data.ts +33 -0
- package/src/permissions/prompter.ts +1 -5
- package/src/persistence/conversation-queries.ts +66 -16
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
- package/src/persistence/schema/documents.ts +4 -4
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/index.ts +5 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/defaults/main.ts +6 -7
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/mtime-cache.ts +17 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- package/src/providers/__tests__/retry-callsite.test.ts +20 -0
- package/src/providers/openai/chat-completions-provider.ts +11 -1
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
- package/src/routes/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/agent-wake.ts +15 -12
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/conversation-list-routes.ts +54 -22
- package/src/runtime/routes/conversation-management-routes.ts +2 -3
- package/src/runtime/routes/conversation-routes.ts +11 -13
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +20 -37
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +15 -13
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/acp/spawn.ts +6 -4
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/daemon/interactive-turn-sender.ts +0 -59
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- 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 } =
|
|
23
|
-
"
|
|
24
|
-
|
|
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 } =
|
|
64
|
-
"
|
|
65
|
-
|
|
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
|
|
package/src/cli/commands/keys.ts
CHANGED
|
@@ -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 } =
|
|
20
|
-
"
|
|
21
|
-
|
|
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
|
|
65
|
-
"
|
|
66
|
-
|
|
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
|
|
70
|
-
"
|
|
71
|
-
|
|
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
|
|
75
|
-
"
|
|
76
|
-
|
|
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
|
|
80
|
-
"
|
|
81
|
-
|
|
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
|
|
85
|
-
"
|
|
86
|
-
|
|
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
|
|
90
|
-
"
|
|
91
|
-
|
|
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
|
|
95
|
-
"
|
|
96
|
-
|
|
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
|
|
100
|
-
"
|
|
101
|
-
|
|
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
|
|
105
|
-
"
|
|
106
|
-
|
|
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
|
|
110
|
-
"
|
|
111
|
-
|
|
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
|
|
115
|
-
"
|
|
116
|
-
|
|
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
|
|
120
|
-
"
|
|
121
|
-
|
|
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
|
|
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": "
|
|
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 —
|
|
194
|
-
*
|
|
195
|
-
* tests preseed flag state via
|
|
196
|
-
* `__tests__/feature-flag-test-helpers.ts`)
|
|
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
|
-
*
|
|
278
|
-
* `
|
|
279
|
-
*
|
|
280
|
-
*
|
|
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
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* Called by the gateway flag listener when a `feature_flags_changed`
|
|
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
|
-
|
|
300
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|