Skip to content

Commit c1a8916

Browse files
localai-botmudler
andauthored
refactor(settings): single declarative registry for runtime settings (fixes the #10845 bug class) (#10864)
* feat(settings): add declarative runtime-settings field registry One fieldSpec row per RuntimeSettings field, with a reflection completeness spec so a field added without a registry row is a red test instead of a silently-dropped setting (the #10845 bug class). Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * refactor(settings): drive ToRuntimeSettings/ApplyRuntimeSettings from the field registry Behavior-preserving: ~350 hand-written per-field lines become two loops over runtimeSettingsFields, gated by a To->Apply->To round-trip spec. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * feat(settings): baseline-driven startup merge for persisted runtime settings ApplyRuntimeSettingsAtStartup compares the live config against DefaultRuntimeBaseline (option-less-run defaults incl. kong-injected flag defaults) instead of per-field == 0 guards. Fixes persisted lru_eviction_max_retries, tracing_max_items, agent_job_retention_days, memory_reclaimer_threshold, galleries and autoload flags being silently ignored at boot. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * fix(settings): registry-driven startup merge, applied before consumers loadRuntimeSettingsFromFile becomes a thin wrapper over ApplyRuntimeSettingsAtStartup and runs at the top of New(), before model configs capture app-level defaults. WithThreads stops eagerly resolving 0 so a persisted thread count survives restart while LOCALAI_THREADS still wins (#10845); the physical-core fallback moves after the merge. Also: run.go now injects the memory-reclaimer threshold unconditionally so the option-less boot matches DefaultRuntimeBaseline and a UI-saved threshold survives restart. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * refactor(settings): file watcher delegates to the registry merge; shared API-key merge Manual edits to runtime_settings.json now behave like a boot-time load (env still wins) instead of the inverted diverged-from-startup guard that ignored most manual edits. MergeAPIKeys dedups env keys in one place for the endpoint and the watcher. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * docs(settings): document unified runtime-settings precedence Document the single env/CLI > runtime_settings.json > defaults rule, applied identically at boot, on POST /api/settings, and on manual file edits, plus the two known limitations (default-valued env vars are indistinguishable from unset; API-changed fields hot-apply on the next restart only). Also add a completion debug log when the watcher applies runtime_settings.json. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 * test(settings): reset the global VRAM cap leaked by the round-trip spec The round-trip spec applies vram_budget=12GiB, whose post-loop hook installs a process-global default cap; without a reset every spec ordered after it runs under that phantom budget. Also drop a stale enumeration in the ApplyRuntimeSettings doc comment. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-fable-5 --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 06b4a29 commit c1a8916

13 files changed

Lines changed: 1033 additions & 979 deletions

cmd/local-ai/main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/alecthomas/kong"
88
"github.com/joho/godotenv"
99
"github.com/mudler/LocalAI/core/cli"
10+
"github.com/mudler/LocalAI/core/config"
1011
"github.com/mudler/LocalAI/internal"
1112
"github.com/mudler/xlog"
1213

@@ -65,8 +66,8 @@ For documentation and support:
6566
// denied". See cli.DefaultGeneratedContentPath.
6667
"generatedcontentpath": cli.DefaultGeneratedContentPath(),
6768
"uploadpath": cli.DefaultUploadPath(),
68-
"galleries": `[{"name":"localai", "url":"github:mudler/LocalAI/gallery/index.yaml@master"}]`,
69-
"backends": `[{"name":"localai", "url":"github:mudler/LocalAI/backend/index.yaml@master"}]`,
69+
"galleries": config.DefaultGalleriesJSON,
70+
"backends": config.DefaultBackendGalleriesJSON,
7071
"version": internal.PrintableVersion(),
7172
},
7273
)

core/application/config_file_watcher.go

Lines changed: 19 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"os"
77
"path"
88
"path/filepath"
9-
"slices"
109
"time"
1110

1211
"dario.cat/mergo"
@@ -186,202 +185,27 @@ func readExternalBackendsJson(startupAppConfig config.ApplicationConfig) fileHan
186185
}
187186

188187
func readRuntimeSettingsJson(startupAppConfig config.ApplicationConfig) fileHandler {
189-
handler := func(fileContent []byte, appConfig *config.ApplicationConfig) error {
188+
return func(fileContent []byte, appConfig *config.ApplicationConfig) error {
190189
xlog.Debug("processing runtime_settings.json")
191-
192-
// Determine if settings came from env vars by comparing with startup config
193-
// startupAppConfig contains the original values set from env vars at startup.
194-
// If current values match startup values, they came from env vars (or defaults).
195-
// We apply file settings only if current values match startup values (meaning not from env vars).
196-
envWatchdogIdle := appConfig.WatchDogIdle == startupAppConfig.WatchDogIdle
197-
envWatchdogBusy := appConfig.WatchDogBusy == startupAppConfig.WatchDogBusy
198-
envWatchdogIdleTimeout := appConfig.WatchDogIdleTimeout == startupAppConfig.WatchDogIdleTimeout
199-
envWatchdogBusyTimeout := appConfig.WatchDogBusyTimeout == startupAppConfig.WatchDogBusyTimeout
200-
envWatchdogInterval := appConfig.WatchDogInterval == startupAppConfig.WatchDogInterval
201-
envSingleBackend := appConfig.SingleBackend == startupAppConfig.SingleBackend
202-
envMaxActiveBackends := appConfig.MaxActiveBackends == startupAppConfig.MaxActiveBackends
203-
envMemoryReclaimerEnabled := appConfig.MemoryReclaimerEnabled == startupAppConfig.MemoryReclaimerEnabled
204-
envMemoryReclaimerThreshold := appConfig.MemoryReclaimerThreshold == startupAppConfig.MemoryReclaimerThreshold
205-
envThreads := appConfig.Threads == startupAppConfig.Threads
206-
envContextSize := appConfig.ContextSize == startupAppConfig.ContextSize
207-
envF16 := appConfig.F16 == startupAppConfig.F16
208-
envDebug := appConfig.Debug == startupAppConfig.Debug
209-
envCORS := appConfig.CORS == startupAppConfig.CORS
210-
envCSRF := appConfig.DisableCSRF == startupAppConfig.DisableCSRF
211-
envCORSAllowOrigins := appConfig.CORSAllowOrigins == startupAppConfig.CORSAllowOrigins
212-
envP2PToken := appConfig.P2PToken == startupAppConfig.P2PToken
213-
envP2PNetworkID := appConfig.P2PNetworkID == startupAppConfig.P2PNetworkID
214-
envFederated := appConfig.Federated == startupAppConfig.Federated
215-
envGalleries := slices.Equal(appConfig.Galleries, startupAppConfig.Galleries)
216-
envBackendGalleries := slices.Equal(appConfig.BackendGalleries, startupAppConfig.BackendGalleries)
217-
envAutoloadGalleries := appConfig.AutoloadGalleries == startupAppConfig.AutoloadGalleries
218-
envAutoloadBackendGalleries := appConfig.AutoloadBackendGalleries == startupAppConfig.AutoloadBackendGalleries
219-
envPIIDefaultDetectors := slices.Equal(appConfig.PIIDefaultDetectors, startupAppConfig.PIIDefaultDetectors)
220-
envAgentJobRetentionDays := appConfig.AgentJobRetentionDays == startupAppConfig.AgentJobRetentionDays
221-
envForceEvictionWhenBusy := appConfig.ForceEvictionWhenBusy == startupAppConfig.ForceEvictionWhenBusy
222-
envLRUEvictionMaxRetries := appConfig.LRUEvictionMaxRetries == startupAppConfig.LRUEvictionMaxRetries
223-
envLRUEvictionRetryInterval := appConfig.LRUEvictionRetryInterval == startupAppConfig.LRUEvictionRetryInterval
224-
225-
if len(fileContent) > 0 {
226-
var settings config.RuntimeSettings
227-
err := json.Unmarshal(fileContent, &settings)
228-
if err != nil {
229-
return err
230-
}
231-
232-
// Apply file settings only if they don't match startup values (i.e., not from env vars)
233-
if settings.WatchdogIdleEnabled != nil && !envWatchdogIdle {
234-
appConfig.WatchDogIdle = *settings.WatchdogIdleEnabled
235-
if appConfig.WatchDogIdle {
236-
appConfig.WatchDog = true
237-
}
238-
}
239-
if settings.WatchdogBusyEnabled != nil && !envWatchdogBusy {
240-
appConfig.WatchDogBusy = *settings.WatchdogBusyEnabled
241-
if appConfig.WatchDogBusy {
242-
appConfig.WatchDog = true
243-
}
244-
}
245-
if settings.WatchdogIdleTimeout != nil && !envWatchdogIdleTimeout {
246-
dur, err := time.ParseDuration(*settings.WatchdogIdleTimeout)
247-
if err == nil {
248-
appConfig.WatchDogIdleTimeout = dur
249-
} else {
250-
xlog.Warn("invalid watchdog idle timeout in runtime_settings.json", "error", err, "timeout", *settings.WatchdogIdleTimeout)
251-
}
252-
}
253-
if settings.WatchdogBusyTimeout != nil && !envWatchdogBusyTimeout {
254-
dur, err := time.ParseDuration(*settings.WatchdogBusyTimeout)
255-
if err == nil {
256-
appConfig.WatchDogBusyTimeout = dur
257-
} else {
258-
xlog.Warn("invalid watchdog busy timeout in runtime_settings.json", "error", err, "timeout", *settings.WatchdogBusyTimeout)
259-
}
260-
}
261-
if settings.WatchdogInterval != nil && !envWatchdogInterval {
262-
dur, err := time.ParseDuration(*settings.WatchdogInterval)
263-
if err == nil {
264-
appConfig.WatchDogInterval = dur
265-
} else {
266-
xlog.Warn("invalid watchdog interval in runtime_settings.json", "error", err, "interval", *settings.WatchdogInterval)
267-
}
268-
}
269-
// Handle MaxActiveBackends (new) and SingleBackend (deprecated)
270-
if settings.MaxActiveBackends != nil && !envMaxActiveBackends {
271-
appConfig.MaxActiveBackends = *settings.MaxActiveBackends
272-
// For backward compatibility, also set SingleBackend if MaxActiveBackends == 1
273-
appConfig.SingleBackend = (*settings.MaxActiveBackends == 1)
274-
} else if settings.SingleBackend != nil && !envSingleBackend {
275-
// Legacy: SingleBackend maps to MaxActiveBackends = 1
276-
appConfig.SingleBackend = *settings.SingleBackend
277-
if *settings.SingleBackend {
278-
appConfig.MaxActiveBackends = 1
279-
} else {
280-
appConfig.MaxActiveBackends = 0
281-
}
282-
}
283-
if settings.MemoryReclaimerEnabled != nil && !envMemoryReclaimerEnabled {
284-
appConfig.MemoryReclaimerEnabled = *settings.MemoryReclaimerEnabled
285-
if appConfig.MemoryReclaimerEnabled {
286-
appConfig.WatchDog = true // Memory reclaimer requires watchdog
287-
}
288-
}
289-
if settings.MemoryReclaimerThreshold != nil && !envMemoryReclaimerThreshold {
290-
appConfig.MemoryReclaimerThreshold = *settings.MemoryReclaimerThreshold
291-
}
292-
if settings.ForceEvictionWhenBusy != nil && !envForceEvictionWhenBusy {
293-
appConfig.ForceEvictionWhenBusy = *settings.ForceEvictionWhenBusy
294-
}
295-
if settings.LRUEvictionMaxRetries != nil && !envLRUEvictionMaxRetries {
296-
appConfig.LRUEvictionMaxRetries = *settings.LRUEvictionMaxRetries
297-
}
298-
if settings.LRUEvictionRetryInterval != nil && !envLRUEvictionRetryInterval {
299-
dur, err := time.ParseDuration(*settings.LRUEvictionRetryInterval)
300-
if err == nil {
301-
appConfig.LRUEvictionRetryInterval = dur
302-
} else {
303-
xlog.Warn("invalid LRU eviction retry interval in runtime_settings.json", "error", err, "interval", *settings.LRUEvictionRetryInterval)
304-
}
305-
}
306-
if settings.Threads != nil && !envThreads {
307-
appConfig.Threads = *settings.Threads
308-
}
309-
if settings.ContextSize != nil && !envContextSize {
310-
appConfig.ContextSize = *settings.ContextSize
311-
}
312-
if settings.F16 != nil && !envF16 {
313-
appConfig.F16 = *settings.F16
314-
}
315-
if settings.Debug != nil && !envDebug {
316-
appConfig.Debug = *settings.Debug
317-
}
318-
if settings.CORS != nil && !envCORS {
319-
appConfig.CORS = *settings.CORS
320-
}
321-
if settings.CSRF != nil && !envCSRF {
322-
appConfig.DisableCSRF = *settings.CSRF
323-
}
324-
if settings.CORSAllowOrigins != nil && !envCORSAllowOrigins {
325-
appConfig.CORSAllowOrigins = *settings.CORSAllowOrigins
326-
}
327-
if settings.P2PToken != nil && !envP2PToken {
328-
appConfig.P2PToken = *settings.P2PToken
329-
}
330-
if settings.P2PNetworkID != nil && !envP2PNetworkID {
331-
appConfig.P2PNetworkID = *settings.P2PNetworkID
332-
}
333-
if settings.Federated != nil && !envFederated {
334-
appConfig.Federated = *settings.Federated
335-
}
336-
if settings.Galleries != nil && !envGalleries {
337-
appConfig.Galleries = *settings.Galleries
338-
}
339-
if settings.BackendGalleries != nil && !envBackendGalleries {
340-
appConfig.BackendGalleries = *settings.BackendGalleries
341-
}
342-
if settings.AutoloadGalleries != nil && !envAutoloadGalleries {
343-
appConfig.AutoloadGalleries = *settings.AutoloadGalleries
344-
}
345-
if settings.AutoloadBackendGalleries != nil && !envAutoloadBackendGalleries {
346-
appConfig.AutoloadBackendGalleries = *settings.AutoloadBackendGalleries
347-
}
348-
if settings.PIIDefaultDetectors != nil && !envPIIDefaultDetectors {
349-
// Request-side default redaction reads this live via
350-
// ResolvePIIPolicy, so a file edit takes effect on the next chat
351-
// request. The MITM listener resolves its per-host detector map
352-
// once at start, so a raw file edit reaches cloud-proxy traffic
353-
// only after a restart or a POST /api/settings (which rebuilds
354-
// the listener) — the admin UI uses the latter.
355-
appConfig.PIIDefaultDetectors = append([]string(nil), (*settings.PIIDefaultDetectors)...)
356-
}
357-
if settings.AutoUpgradeBackends != nil {
358-
appConfig.AutoUpgradeBackends = *settings.AutoUpgradeBackends
359-
}
360-
if settings.PreferDevelopmentBackends != nil {
361-
appConfig.PreferDevelopmentBackends = *settings.PreferDevelopmentBackends
362-
}
363-
if settings.ApiKeys != nil {
364-
// API keys from env vars (startup) should be kept, runtime settings keys replace all runtime keys
365-
// If runtime_settings.json specifies ApiKeys (even if empty), it replaces all runtime keys
366-
// Start with env keys, then add runtime_settings.json keys (which may be empty to clear them)
367-
envKeys := startupAppConfig.ApiKeys
368-
runtimeKeys := *settings.ApiKeys
369-
// Replace all runtime keys with what's in runtime_settings.json
370-
appConfig.ApiKeys = append(envKeys, runtimeKeys...)
371-
}
372-
if settings.AgentJobRetentionDays != nil && !envAgentJobRetentionDays {
373-
appConfig.AgentJobRetentionDays = *settings.AgentJobRetentionDays
374-
}
375-
376-
// If watchdog is enabled via file but not via env, ensure WatchDog flag is set
377-
if !envWatchdogIdle && !envWatchdogBusy {
378-
if settings.WatchdogEnabled != nil && *settings.WatchdogEnabled {
379-
appConfig.WatchDog = true
380-
}
381-
}
190+
if len(fileContent) == 0 {
191+
return nil
382192
}
383-
xlog.Debug("runtime settings loaded from runtime_settings.json")
193+
var settings config.RuntimeSettings
194+
if err := json.Unmarshal(fileContent, &settings); err != nil {
195+
return err
196+
}
197+
// Same merge semantics as boot: env/CLI-claimed fields win, the
198+
// file supplies the rest. This replaces the old inverted guard
199+
// (apply only when live != startup snapshot) which skipped genuine
200+
// manual edits of any field still at its boot value. Trade-off: a
201+
// field previously changed via the API looks env-set to the
202+
// baseline comparison, so a manual file edit of that field lands
203+
// on the next restart instead of hot-applying.
204+
appConfig.ApplyRuntimeSettingsAtStartup(&settings)
205+
if settings.ApiKeys != nil {
206+
appConfig.ApiKeys = config.MergeAPIKeys(startupAppConfig.ApiKeys, *settings.ApiKeys)
207+
}
208+
xlog.Debug("runtime settings applied from runtime_settings.json")
384209
return nil
385210
}
386-
return handler
387211
}

core/application/runtime_settings_branding_test.go

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,20 +157,32 @@ var _ = Describe("loadRuntimeSettingsFromFile", func() {
157157
})
158158
})
159159

160-
// The live file watcher applies pii_default_detectors on a runtime change
161-
// the same way it handles galleries/threads/etc.: env-set values (current
162-
// == startup snapshot) are left alone, otherwise the file value is applied
163-
// to the live config so request-side default redaction picks it up without
164-
// a restart.
160+
// The live file watcher delegates to ApplyRuntimeSettingsAtStartup, so a
161+
// manual edit of runtime_settings.json behaves exactly like a boot-time
162+
// load: env/CLI-claimed values win, the file supplies the rest, and the
163+
// applied value reaches request-side default redaction without a restart.
165164
Describe("file watcher: pii_default_detectors", func() {
166-
It("applies a changed file value to the live config", func() {
165+
It("applies a file value when the live config is still at its default", func() {
167166
startup := config.ApplicationConfig{} // no env baseline
168-
live := &config.ApplicationConfig{PIIDefaultDetectors: []string{"old"}}
167+
live := config.NewApplicationConfig() // detectors at default (empty)
169168
handler := readRuntimeSettingsJson(startup)
170169
Expect(handler([]byte(`{"pii_default_detectors":["new-a","new-b"]}`), live)).To(Succeed())
171170
Expect(live.PIIDefaultDetectors).To(Equal([]string{"new-a", "new-b"}))
172171
})
173172

173+
It("defers a file edit of an already-customized value to the next restart", func() {
174+
// The baseline comparison cannot tell an API-set value from an
175+
// env-set one, so the watcher leaves it alone; the boot loader
176+
// applies it on restart. Documented trade-off of the unified
177+
// merge semantics (design doc 2026-07-16).
178+
startup := config.ApplicationConfig{}
179+
live := config.NewApplicationConfig()
180+
live.PIIDefaultDetectors = []string{"old"}
181+
handler := readRuntimeSettingsJson(startup)
182+
Expect(handler([]byte(`{"pii_default_detectors":["from-file"]}`), live)).To(Succeed())
183+
Expect(live.PIIDefaultDetectors).To(Equal([]string{"old"}))
184+
})
185+
174186
It("leaves an env-controlled value untouched", func() {
175187
startup := config.ApplicationConfig{PIIDefaultDetectors: []string{"from-env"}}
176188
live := &config.ApplicationConfig{PIIDefaultDetectors: []string{"from-env"}}
@@ -262,6 +274,79 @@ var _ = Describe("loadRuntimeSettingsFromFile", func() {
262274
})
263275
})
264276

277+
// Issue #10845 root cause class: fields with non-zero boot defaults
278+
// whose == 0 guards never fired, so the persisted value was ignored on
279+
// every restart. The registry's baseline comparison fixes them; these
280+
// specs pin that.
281+
Describe("fields with non-zero defaults (silent restart loss)", func() {
282+
It("loads a persisted lru_eviction_max_retries over the default 30", func() {
283+
cfg := config.NewApplicationConfig()
284+
cfg.DynamicConfigsDir = seedSettings(`{"lru_eviction_max_retries": 99}`)
285+
loadRuntimeSettingsFromFile(cfg)
286+
Expect(cfg.LRUEvictionMaxRetries).To(Equal(99))
287+
})
288+
289+
It("loads a persisted tracing_max_items over the default 1024", func() {
290+
cfg := config.NewApplicationConfig()
291+
cfg.DynamicConfigsDir = seedSettings(`{"tracing_max_items": 4096}`)
292+
loadRuntimeSettingsFromFile(cfg)
293+
Expect(cfg.TracingMaxItems).To(Equal(4096))
294+
})
295+
296+
It("loads a persisted agent_job_retention_days over the default 30", func() {
297+
cfg := config.NewApplicationConfig()
298+
cfg.DynamicConfigsDir = seedSettings(`{"agent_job_retention_days": 7}`)
299+
loadRuntimeSettingsFromFile(cfg)
300+
Expect(cfg.AgentJobRetentionDays).To(Equal(7))
301+
})
302+
})
303+
304+
// #10845: threads saved via the UI must survive a restart, while
305+
// LOCALAI_THREADS/CLI still wins. WithThreads no longer eagerly
306+
// resolves 0 to the physical-core count; application.New() does that
307+
// after this loader has run.
308+
Describe("performance fields", func() {
309+
It("loads persisted threads when env/CLI left them unset", func() {
310+
cfg := config.NewApplicationConfig(config.WithThreads(0))
311+
cfg.DynamicConfigsDir = seedSettings(`{"threads": 4}`)
312+
loadRuntimeSettingsFromFile(cfg)
313+
Expect(cfg.Threads).To(Equal(4))
314+
})
315+
316+
It("keeps an env/CLI thread count over the file", func() {
317+
cfg := config.NewApplicationConfig(config.WithThreads(8))
318+
cfg.DynamicConfigsDir = seedSettings(`{"threads": 4}`)
319+
loadRuntimeSettingsFromFile(cfg)
320+
Expect(cfg.Threads).To(Equal(8))
321+
})
322+
323+
It("loads persisted context_size and f16", func() {
324+
cfg := config.NewApplicationConfig()
325+
cfg.DynamicConfigsDir = seedSettings(`{"context_size": 4096, "f16": true}`)
326+
loadRuntimeSettingsFromFile(cfg)
327+
Expect(cfg.ContextSize).To(Equal(4096))
328+
Expect(cfg.F16).To(BeTrue())
329+
})
330+
})
331+
332+
// Option-less boot contract with core/cli/run.go: the kong threshold
333+
// default (0.95) is injected unconditionally even when the reclaimer is
334+
// disabled, so DefaultRuntimeBaseline's 0.95 overlay matches reality and
335+
// a UI-persisted threshold is not mistaken for an env-set one. Both
336+
// persisted fields must apply, and enabling the reclaimer must force the
337+
// watchdog master flag (startup invariant).
338+
Describe("memory reclaimer", func() {
339+
It("applies a persisted enable + threshold on an option-less boot", func() {
340+
cfg := config.NewApplicationConfig()
341+
cfg.MemoryReclaimerThreshold = 0.95 // what run.go injects when no flag is passed
342+
cfg.DynamicConfigsDir = seedSettings(`{"memory_reclaimer_enabled": true, "memory_reclaimer_threshold": 0.5}`)
343+
loadRuntimeSettingsFromFile(cfg)
344+
Expect(cfg.MemoryReclaimerEnabled).To(BeTrue())
345+
Expect(cfg.MemoryReclaimerThreshold).To(Equal(0.5))
346+
Expect(cfg.WatchDog).To(BeTrue(), "an enabled reclaimer must force the watchdog on")
347+
})
348+
})
349+
265350
// Backend logging capture. Worker/distributed mode force-enables it
266351
// (core/services/worker.SetBackendLoggingEnabled(true)); single mode used
267352
// to leave it off by default with no CLI flag, so the UI "Backend Logs"

0 commit comments

Comments
 (0)