* feat(kap-server): enable multi-server shared home by default - always register kap-server instances under server/instances and drop the legacy single-instance lock (acquireLock/getLiveLock/ServerLockedError) - remove the multi_server experimental flag and its KIMI_CODE_EXPERIMENTAL_MULTI_SERVER env var from agent-core-v2 - discover running servers via the instance registry in server ps/kill/rotate-token, kimi web daemon reuse, and the desktop app - remove the pending minidb changesets * feat(cli): add per-instance targeting to server kill and ps - `kimi server kill [serverId]` stops only the matching instance; without an id it still stops the longest-running one, and an unknown id errors with the live server ids listed - `kimi server ps` lists connections grouped per server id (`--json` nests them under a per-server object); an unreachable instance degrades to a per-server note instead of failing the whole listing - update the zh/en command reference and the multi-server changeset * feat(cli): replace kimi server with the kimi web command tree - `kimi web` now runs the local server in the foreground and opens the browser; the background daemon (ensureDaemon / spawn / idle-exit) is removed, so repeated runs simply start another instance on the next free port - drop the OS-service lifecycle (install/uninstall/start/stop/restart/ status) together with kap-server's svc layer - `kimi web kill [serverId]`, `kimi web ps`, and `kimi web rotate-token` manage instances from the registry - the TUI /web command now connects to an already-running instance instead of spawning a background daemon - update the zh/en command reference, dev scripts, and tests * feat(cli): route kimi server invocations to a deprecation notice Any `kimi server …` call — bare or with any legacy subcommand/flags — now prints a deprecation notice pointing at `kimi web` and exits 1, instead of failing with an opaque "unknown command". The shim is scheduled for removal in the next major version. * feat(cli): add the `all` keyword to kimi web kill `kimi web kill all` stops every live instance in the registry (ULIDs can never collide with the keyword). Each instance still gets the API shutdown + SIGTERM/SIGKILL treatment; a failure on one instance does not stop the sweep and is reported at the end. * docs(changeset): drop the web-foreground-default changeset The kimi web command tree replaces the foreground-default behavior this entry describes: --background, daemon reuse, and the version-mismatch hint no longer exist, so the pending entry would contradict the actual release notes. * docs(changeset): tighten the multi-server entry wording * feat(cli): let /web pick a running server or start a new one The /web picker now lists the live instances from the registry with their versions (flagging a CLI mismatch) instead of only connecting to the longest-running one, and offers starting a new server: that one runs in the foreground attached to the terminal after the TUI exits, via the restored exit-takeover wiring. formatReadyBanner is exported and adapts its Stop hint to Ctrl+C for the attached case. * feat(cli): skip the /web picker and start a new server when none is running
6.9 KiB
flag
Experimental feature-flag gating for agent-core-v2 — a App-scope
IFlagServiceresolver plus a writableIFlagRegistrycatalog that domains contribute their flags to, backed by the[experimental]config section.
Gates not-yet-public features behind IFlagService.enabled(id), per the repository hard rule that unreleased behavior must be flag-gated. Ported from packages/agent-core/src/flags/**; v1 was a process-global FlagResolver singleton over a central FLAG_DEFINITIONS array, v2 is a scoped DI service whose flag definitions are registered decentrally by each owning domain — there is no central catalog to edit.
Layout
src/flag/flagRegistry.ts—IFlagRegistrytoken +FlagDefinitionInput/FlagId/FlagSurfacetypes +registerFlagDefinition/getContributedFlags(import-time contribution queue).src/flag/flagRegistryService.ts—FlagRegistryServiceimpl; in-memory catalog seeded from import-time contributions; App scope.src/flag/flag.ts—IFlagServicetoken + resolver types (ExperimentalFlagMap,ExperimentalFlagConfig,ExperimentalFlagSource,ExperimentalFeatureState) +ExperimentalConfigSchema/ExperimentalConfig(zod).src/flag/flagService.ts—FlagServiceimpl +MASTER_ENV(KIMI_CODE_EXPERIMENTAL_FLAG) +EXPERIMENTAL_SECTION(experimental); reads definitions fromIFlagRegistry; self-registers at App scope.src/flag/index.ts— barrel; re-exported bysrc/index.tsat the L3 block.src/<domain>/flag.ts— each domain that owns a flag declares it here and callsregisterFlagDefinitionat the module top level (e.g.src/agent/toolSelect/flag.ts). The directory already names the domain, so the file is justflag.ts.
Public surface
IFlagService(DI token, App scope):enabled(id),explain(id),snapshot(),enabledIds(),explainAll(),setConfigOverrides(overrides),registry.IFlagRegistry(DI token, App scope):register(definition),get(id),list()— writable catalog.registeris the runtime path (tests, dynamic registration);IFlagService.registryexposes the same instance for hosts/UI to enumerate flags without resolving them.registerFlagDefinition(definition)— the import-time path. Domains call this from theirflag.tstop level; contributions are queued and drained byFlagRegistryServicewhen it is instantiated.FlagService/FlagRegistryService: exported for tests and hosts that construct them directly.
Resolution precedence
Highest wins; env is read live on every call (nothing cached):
- L1 master env
KIMI_CODE_EXPERIMENTAL_FLAGtruthy → every flag on. - L2 per-feature
def.env(e.g.KIMI_CODE_EXPERIMENTAL_MY_FEATURE) → forces on/off. - L3
[experimental]config section per-flag override. - L4 registry
default.
explain(id) returns the winning source (master-env | env | config | default) plus the effective configValue. explain(id) returns undefined (and enabled(id) returns false) for an id that no domain has registered.
Config integration
FlagServiceregisters the[experimental]section intoIConfigRegistryat construction (registerSection('experimental', ExperimentalConfigSchema)) and reads overrides fromIConfigService.- It subscribes
IConfigService.onDidChangeConfigurationand refreshes overrides whenever theexperimentaldomain changes, so config edits apply live. IConfigRegistry.registerSectionthrows if a domain is registered twice —experimentalis owned exclusively byFlagService.setConfigOverrides(overrides)is an imperative escape hatch for tests and hosts without anIConfigService; hosts onIConfigServiceshould set the[experimental]section instead.
Config shape mirrors v1:
[experimental]
my_feature = false
Keys are intentionally loose (z.record(z.string(), z.boolean())), so obsolete flags stay inert config.
Add a flag
Declare the definition in the owning domain's flag.ts and call registerFlagDefinition at the module top level. There is no central catalog to edit.
src/<domain>/flag.ts:
import { type FlagDefinitionInput, registerFlagDefinition } from '#/flag';
export const myFeatureFlag: FlagDefinitionInput = {
id: 'my_feature',
title: 'My feature',
description: '...',
env: 'KIMI_CODE_EXPERIMENTAL_MY_FEATURE',
default: false,
surface: 'both',
};
registerFlagDefinition(myFeatureFlag);
Then load it from the domain barrel so the top-level call runs at import time:
// src/<domain>/index.ts
import './flag';
export * from './flag';
src/index.ts already re-exports every domain barrel, so the contribution runs during bootstrap, before any scope is created — and therefore before any consumer resolves IFlagService.
envmust start withKIMI_CODE_EXPERIMENTAL_, be unique, and not equalKIMI_CODE_EXPERIMENTAL_FLAG.idmust not beflag. A duplicateidthrows whenFlagRegistryServicedrains the contributions.FlagIdisstring, not a literal union: with no central catalog there is nothing to derive it from, soenabled()has no compile-time typo-checking. Cover gated behavior with tests instead.surface:core|tui|both(documentation/grouping only; not used in resolution).
Consume a flag
Inject IFlagService and gate on it. It is resolvable from any scope (App ancestor):
constructor(@IFlagService private readonly flags: IFlagService) {}
// ...
if (!this.flags.enabled('my_feature')) return;
Layering & scope
- Domain
flagis registered at L3 (scripts/check-domain-layers.mjs→['flag', 3]). It imports onlyconfig(L2) downward. - It cannot live in
_base(L0): registering/reading the config section requires importingconfig, and L0 must not import L2. - Scope:
IFlagRegistryandIFlagServiceare bothApp. Env + config are process-global inputs, so there is no per-session/agent state. Flag definitions are contributed at import time (top-levelregisterFlagDefinitioncalls), so they are queued before any scope is created and drained whenFlagRegistryServiceis first instantiated — beforeIFlagServiceis first resolved. - Tests build
FlagService+FlagRegistryServicedirectly with a realConfigRegistry/ConfigServiceand an injected env map, thenregisterthe flags they exercise (test/flag/flag.test.ts).
References
packages/agent-core-v2/src/flag/— implementation (IFlagRegistry+IFlagService).packages/agent-core-v2/src/agent/toolSelect/flag.ts— example per-domain flag contribution.packages/agent-core-v2/test/flag/flag.test.ts— precedence + config subscription tests.packages/agent-core/src/flags/— v1 source this was ported from.plan/PLAN.md§2/§3 — domain placement (flagat L3, not_base/flags).packages/agent-core-v2/GAP_ANALYSIS.md§2.1 — gap closure note.- Root
AGENTS.md— experimental-feature gating rule.