* feat(agent-core-v2): add cross-process session occupancy lock
Multiple clients (TUI / kap-server / VS Code / ACP) running core in
separate processes could open the same session and corrupt each
other's wire.jsonl and metadata. Each session now holds a cross-process
occupancy lock ({homeDir}/locks/sessions/{id}.lock) taken only while
writing: runs acquire it on submit / compact / undo / cron / delete /
fork / cold-archive, the append log holds it before flushing, and a
stale check compares the main agent wire's on-disk length with the
length this process knows.
Collisions surface as session.occupied (holder in details); writes by
another client detected after acquire surface as session.stale and the
caller reloads before continuing. kap-server maps them to
SESSION_OCCUPIED (40903) / SESSION_STALE (40930), reloads the session
on stale, and re-attaches WS subscriptions; the TUI shows an occupancy
dialog (retry/cancel) or reloads and replays history keeping the input;
print exits with code 4 on occupied and reloads automatically on stale;
VS Code / ACP get minimal occupancy notices. Locks are released a few
seconds after the session goes idle, on close, and on process exit;
locking I/O failures degrade to unlocked operation with a warning.
* docs: document session occupancy error codes and lock directory
* fix: address codex review on session occupancy lock
- metadata: re-read state.json on the first write of each lock-hold
epoch (holdToken), so pure-metadata changes made by another client
(title, archive) are not clobbered by turn-end lastPrompt writes
- lifecycle: hold an explicit lease across live archive(), matching
delete(), so the idle release timer cannot drop the lock mid-teardown
- tui: route the occupied dialog's Cancel selection through the same
cleanup as Esc (restore input, release recalled media)
- acp: tell stale-session users to close and reopen the session
- append-log: warn when retirement drops records kept in memory because
the session lock was unavailable
* fix: always run the staleness check in beginSessionRun
The prompt channels (AgentPromptChannel.submit/submitSteer, kap
/prompts, promptWithSkills) write prompt metadata before calling
loop.submit, and the metadata write acquires the session lock first.
beginSessionRun then short-circuited on 'already held' and skipped the
wire-length staleness check, so the main prompt path never detected
updates written by another client. Drop the already-held shortcut: the
check is a single stat and always passes while the lock is continuously
held, so every acquisition path now runs it regardless of order.
* fix(print): preserve occupied handling on the stale retry
If another client acquires the session between the reload and the
retry, the retried turn now goes through the same conflict handler:
session.occupied reports to stderr / stream-json and exits with code 4
instead of bubbling out as exit code 1.
* fix: do not mistake own in-flight writes for external modification
storage.append grows the file (writeFile) before fsync/close complete,
and knownLength only catches up afterwards; a beginSessionRun landing
in that window (steer, queued prompts, cron fires during a turn) was
misjudged as session.stale and deleted the lock file mid-turn. Track
in-flight bytes per log and compare disk against known + in-flight.
Cover the analogous rewrite window (post-rename syncDir) with an
expected-final-length marker.
* fix: bound the staleness comparison and check once per lock hold
The equality check disk !== known + inFlight moved the false-positive
window to before the append lands (mkdir/open) and into chunked
writes, so beginSessionRun during a turn (steer, cron, queued prompts)
could misjudge, delete the lock file mid-turn, and later append behind
another client. Compare known <= disk <= known + inFlight instead, and
evaluate the check only on a fresh hold (holdToken epoch): re-entrant
runs pass through without a stat, matching the RFC's 'already held
passes through' while every acquisition path still triggers the check
exactly once per hold.
Also unify the occupancy prompt copy across clients: the engine error
message is now the single user-facing text ('该会话正在被其他 Kimi
Code 进程使用;请停止其他进程中的会话或退出该进程后重试'), and the
TUI dialog, /compact /undo errors, print output, and the VS Code
fallback all display it instead of maintaining their own wordings.
* fix: address third-round codex review
- metadata: hold an explicit lease across each state.json write so the
idle release timer cannot unlink the lock mid-write
- print: preflight the session lock before goal creation; make goal
creation idempotent and emit the goal summary only for attempts that
did not fail with session.stale, so a stale reload + retry neither
duplicates the goal nor the summary
- tui: route every prompt-launch path (plain, goal steer, plugin
command, inline skill, batch steer) through one conflict handler
with the occupied dialog and stale reload + input restore
- tui: snapshot and restore the attachment store across stale reloads
so pasted media placeholders still resolve after history replay
* fix: move session occupancy to a fresh error code, use English copy, release locks on exit
SESSION_OCCUPIED reused 40903, which belonged to PROMPT_ALREADY_COMPLETED until #3747 retired it, and web clients still allowlist 40903 for that old meaning; reusing it conflated the two semantics. Move session occupancy to the never-used 40931 and update the server API reference in both locales.
The occupied error message was hardcoded in Chinese while every other engine error is English, so the TUI, print mode, VS Code, and ACP clients all rendered mixed-language output; replace it with an English equivalent.
The lock release hook ran on 'beforeExit', which never fires when the process leaves through process.exit (SIGHUP in the TUI goes through emergencyTerminalExit), leaving stale lock files that could keep a session looking occupied forever after a hostname change or pid reuse. releaseAll is fully synchronous, so hook 'exit' instead.
* fix(agent-core-v2): decide prompt-derived titles after re-reading session metadata
applyPromptMetadataUpdate decided whether to derive a title from the
prompt from the in-memory metadata cache, which is only refreshed when
the session lock is re-acquired. If another client named the session in
the meantime, the stale cache still looked untitled, so the next prompt
from this client overwrote the other client's title — both on the stale
path, where the prompt is rejected afterwards, and on the fresh path,
where the other client only changed the title on disk.
Move the untitled check into SessionMetadata.applyPromptMetadata, which
runs after ensureWriteOwnership() has re-read state.json, so the
decision is based on the latest persisted metadata. The published
SessionMetaUpdated event now carries the derived title only when it was
actually written.
* fix: address fourth-round codex review
- vscode: classify session.occupied and session.stale as preflight errors so the webview toasts them instead of silently swallowing the send, and give session.occupied the same English copy the engine emits (the toast text comes from getUserMessage in the extension host).
- engine: beginSessionRun now takes the session context and agent lifecycle service, and on the first check of each lock hold it runs externallyModified over every loaded agent's wire (scope agents/<agentId>, key wire.jsonl) instead of only the main agent's, so a fork that only touched a subagent journal is detected as stale. All four call sites (loop submit, manual compaction begin, undo, print preflight) pass the extra dependency.
- tui: keep the request in flight across a stale reload instead of dropping to idle, so input submitted during the reload queues rather than racing the closing session; park the queue around switchToSession (resetSessionRuntime would clear it) and drain it into the reloaded session afterwards; when restoring the rejected text, prepend it before content the user typed during the reload instead of overwriting it.
- kap-server: clear the in-flight turn tracker and subagent roster in detachSession so a reattached session no longer reports stale in-flight turns or running subagents in REST snapshots and WS resyncs.
* fix: harden session conflict recovery
Detect external appends that race journal reads so a later run cannot miss session.stale. Keep deferred wire migrations pending and finish them before appending, while rejecting the append if migration still cannot complete. Restore the complete plugin command after an occupied cancellation, including commands without arguments.
* fix: scope session staleness cache to session context
Replace the process-global session-id map with a WeakMap keyed by ISessionContext so closed and reloaded sessions do not retain cache entries for the lifetime of the process.
* fix: document session conflicts for metadata routes
Declare SESSION_OCCUPIED and SESSION_STALE in the response schemas for profile updates, title generation, and child creation. Cover the OpenAPI declarations and the existing HTTP 200 conflict envelopes with regression tests.
* refactor: centralize session conflict handling
Move session error mapping and stale reloads into one shared route helper, reuse shared conflict response declarations, and cover skill activation conflicts. Remove the duplicate prompt, session, and skill error mappers while preserving their existing envelopes.
* fix(sdk): restate session conflict errors
* fix(tui): keep the reloaded session open and resend after a stale conflict
* fix(agent-core-v2): lazily rebuild registered agents after a session reload
* refactor: simplify session conflict handling
* refactor: hygiene fixes from session-lock review
- kap-server: move SESSION_STALE to 40932 (collided with
DEFAULT_WORKSPACE_UNAVAILABLE at 40930) and pin ErrorCode value
uniqueness with a test
- node-sdk: fold six catch-and-restate copies into a restate() helper
- acp-server: sync mapPromptLaunchError docstring with the
occupied/stale branches
- replace hardcoded session.stale/session.occupied literals with
ErrorCodes constants
- inline the single-use sessionStaleError
- restore the strict timer-leak assertion with a warmup baseline
instead of a blanket +1 allowance
- narrow SessionState.detached to ?: true
* refactor: introduce session-level run gate
- replace the beginSessionRun free function with ISessionRunGate, a
Session-scoped service; loop/compaction/undo now inject one
dependency instead of four, and sessionLock.ts no longer imports
session-layer modules
- tryHold() absorbs the duplicated try/catch hold copies in
wireService and sessionMetadataService
- resolve IAgentLifecycleService lazily inside the gate to avoid a
construction-time cycle with agentLifecycleService
* refactor: tighten session lock and conflict recovery structure
- Narrow ISessionLockService to hold/tryHold/release/lease/held and share
the hold bookkeeping between the memory and node-fs backends; drop
holdToken and the module-level exit hook state.
- Session metadata refreshes on a new hold via onDidHold and skips agent
registration through tryHold instead of catching session.occupied.
- AppendLogStore reads sizes through IFileSystemStorageService.sizeSync and
exposes a generic write guard instead of a session-lock binding.
- kap: reload stale sessions only at the call sites that can raise
session.stale and log reload failures; error mapping stays side-effect free.
- TUI: recover from a stale conflict through the in-place session reload with
history replay, removing the same-session switch special case, the media
store snapshot, and the queue parking.
- print: bound stale reloads with a simple retry loop.
* refactor(agent-core-v2): merge session lock release into run gate
* test(agent-core-v2): reuse append log fixtures
* refactor(agent-core-v2): break run gate and agent lifecycle dependency cycle
* refactor: reload stale sessions in core instead of each host
Stale detection already lived in core, but every host re-implemented the
recovery: TUI, print, and kap each closed and resumed the session, and TUI
and web resent the rejected input themselves.
Core now exposes reloadOnStale: on session.stale it closes and resumes the
session, lets the caller refresh, then retries the call once (undo reloads
and still rejects, since its count depends on what the user saw).
- kap: prompt submit, skill activation, and compact retry in place instead
of returning 40932; undo still returns 40932 after reloading
- print: retries through reloadOnStale on the same session id and keeps the
original permission mode to restore
- SDK: run-starting calls reload and retry internally; Session gains
setStaleReloadHandler so hosts refresh before the retry
- TUI: drops its own reload/resend/queue-merge path; the stale handler
replays history and keeps in-flight messages
* docs(server-api): note that only undo returns 40932 after a stale reload
* refactor(agent-core-v2): ask each agent's wire whether it was modified externally
* refactor(agent-core-v2): clarify agent restoration helper name
* Delete .changeset/ensure-agent-lazy-rebuild.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(tui): wrap session occupied notice
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(agent-core-v2): fold UserPromptSubmit hook output into prompt content parts
UserPromptSubmit hook messages now merge into the user prompt message as
meta-marked text content parts (contentType text/xml, source 'user prompt
submit hook') at content position 0 instead of a standalone hook_result
user message, so one prompt always yields exactly one user message and
turn pairing can no longer shift. Structured hook JSON without a message
field (e.g. {"continue":true}) no longer injects anything; plain-text
stdout still falls through. turn.prompt records, TurnStarted prompts,
fold turn openings, and steer content keys all strip hook parts; existing
wires with standalone hook_result messages keep their current behavior.
* feat(agent-core-v2): keep UserPromptSubmit hook parts in turn records and handle them across client surfaces
* feat(agent-core-v2): meta-mark bundled skill parts and replace positional slicing
Bundled skill blocks are now constructed with meta { source: 'skill
activation' }, matching the hook-part convention. Every former
content.slice(skillActivations.length) site — turn prompt text, steer
merge/echo, session and fork titles, transcript fold, TUI replay — now
annotates then filters by meta, so prompt content no longer depends on
block positions and the loop needs no hook-part knowledge. A shared
annotate step keeps legacy wires working: leading unmarked blocks (one
per bundled activation) are marked on read before filtering.
* feat(agent-core-v2): strip hook parts from the public TurnStarted prompt at construction
* fix(remote-control): pick credential slot and relay from login region
Global login stores tokens in kimi-code-env-*.json and should use
code-rc.kimi.ai; Remote Control always read kimi-code.json and
code-rc.kimi.com. Resolve both from the persisted oauth ref.
* fix(telemetry): skip the dead global collector and keep shutdown alive
The global collector host has no DNS. Drop it from the region profile and
skip the send instead of falling back to the mainland endpoint. Keep the
shutdown timeout timer referenced so a failed flush cannot exit before the
TUI hands the process to the foreground server.
* fix(remote-control): require explicit relay origin and drop hardcoded .com defaults
* feat(agent-core-v2): support api_key_env for provider credentials
Providers can declare api_key_env = "VAR" in config.toml to read their
Bearer token from an environment variable instead of storing it inline.
The value is resolved per request (rotation-friendly); an unset or empty
variable fails with config.invalid naming the provider and variable.
apiKey+apiKeyEnv and apiKeyEnv+oauth are rejected as conflicts.
The /models refresh path resolves the same chain (inline apiKey ->
apiKeyEnv -> env sub-table); a provider whose variable is unset lands in
failed[] without affecting others, and open-platform providers keep the
env declaration instead of persisting the resolved secret.
Custom registries: applyCustomRegistryProvider consumes the api.json env
field — imported providers get apiKeyEnv, the legacy inline apiKey is
dropped, and a hand-edited apiKeyEnv survives refreshes (tracked via
source.envKey). kimi provider add --api-key and the TUI registry token
field are now optional so public registries import without a key.
* docs: document api_key_env provider credentials
* fix(oauth): preserve hand-edited apiKeyEnv on registry refresh and address review nits
* fix(oauth): validate apiKeyEnv conflicts during refresh and address review nits
* fix(oauth): mirror apiKeyEnv in the klient provider contract and harden refresh validation
* fix(oauth): stop auto-binding registry env names to provider credentials
* fix(oauth): preserve hand-edited apiKeyEnv across registry re-imports
* fix(oauth): scope apiKeyEnv preservation to the owning registry
* fix(agent-core-v2): preserve apiKeyEnv across models.dev catalog re-imports
* fix(kap-server): drop apiKeyEnv when the provider update route receives a replacement key
* fix(agent-core-v2): replace provider credential fields on TOML writeback
* fix(kap-server): normalize whitespace-only api_key in the provider update route
* refactor(oauth): unify provider credential rules into shared primitives
A new provider-credential module in the oauth package owns the credential
precedence chain, the mutual-exclusion conflicts, the user-facing messages,
and the write-side tri-state reconciliation; the refresh orchestrator,
agent-core-v2 (model-auth, catalog, auth readiness), and the kap-server
provider routes compose it instead of re-deriving the rules per layer.
applyOpenPlatformConfig now takes credential: {apiKey}|{apiKeyEnv} and
merges onto the existing record, so open-platform refreshes no longer drop
hand-written fields (customHeaders, the env sub-table) on the inline-key
path either, and the refresh's write-then-undo block is gone.
Registry imports converge on removeCustomRegistryEntries +
applyCustomRegistryEntries (prior-snapshot provenance): the exported
capture/restore helpers are gone, the CLI persists a re-import with a
single setConfig instead of per-id removeProvider RPCs, and
default_model/default_provider survive a re-import whenever they still
resolve. kap-server's importCustomRegistry keeps the purge-then-rewrite
two-phase replace the config service's raw-merging writeback requires.
kap-server create/replace routes accept api_key_env (tri-state, mutually
exclusive with api_key), and GET /config plus the provider catalog item
expose the binding. A declared-but-unset api_key_env now fails readiness
checks with config.invalid (was auth.token_missing), matching request-time
behavior. Registry 401/403 without a key prints an authentication hint.
* fix(cli): persist registry re-import removals on both harness generations
The v2 harness's setConfig deep-merges per domain (retaining omitted keys
and skipping undefined defaults), so a shrunk re-import left removed
providers and aliases in config.toml and kept a cleared default dangling.
The CLI and TUI registry flows now share persistRegistryImport: on v2 the
removal phase and the rebuilt records go out as two atomic
replaceConfigSections writes (the writeback merges alias fields from raw
TOML, so the purge must be persisted first); on v1 the removeProvider RPC
cascades the removals before one merge write. defaultModel/defaultProvider
snapshots are captured before any removal so the restore works on both
paths.
* fix(oauth): reject oauth before the legacy env fallback and persist cleared thinking on re-import
refreshProviderModels' env sub-table fallback now throws the same
apiKey+oauth conflict the chat path rejects, instead of authenticating
with the fallback key and rewriting the provider to drop oauth.
persistRegistryImport writes the cleared thinking section through the
replacement payload, and drops the v1 fallback branch — registry imports
always persist through replaceConfigSections, so the removal phase no
longer needs per-id removeProvider RPCs.
* chore: consolidate provider credential changesets into one entry
* chore: trim changeset to the api_key_env entry
* fix: harden provider credential status and registry imports
* fix(oauth): expose browser-safe provider credential rules
* fix(node-sdk): replace serialized provider credentials
* fix(oauth): reject credential conflicts during refresh
* fix: harden custom registry re-imports
Reject reserved provider collisions and credential endpoint drift before mutation. Replace registry-owned sections atomically with compare-and-set guards, and carry the replacement contract through klient and node-sdk.
* refactor(registry): centralize custom registry import ownership
Route CLI and TUI imports through the engine via the typed SDK and Klient operation. Keep registry replacement policy inside the engine and preserve credential edits made while fetching the registry.
Cover the shared entry point with real configuration storage and memory/IPC transport tests.
* test: trim redundant api_key_env and registry-import coverage
* fix(cli): reuse the stored registry key on keyless re-import
Both first-party clients encoded "no key given" as an empty string, but
the engine's fallback chain (options.apiKey ?? registryKeyFromExisting)
only reuses the key parked in the provider's source blob when the option
is undefined. A keyless re-import of a private registry therefore went
out without an Authorization header and failed with 401 from the CLI
and the TUI dialog, while the kap-server REST route (which passes
undefined) reused the stored key fine.
Pass undefined from the CLI handler and the TUI import dialog instead
of normalizing to ''. The dialog's result type now marks apiKey
optional, matching the engine contract.
* test: pin CAS rejection and credential-conflict behavior
The coverage trim went past redundancy in three places, leaving the
PR's headline guards untested. Restore the compare-and-set rejection
path (guarded value changed on disk -> retry error, no write) and the
absent-section guard normalization in config.test.ts, the
apiKey+apiKeyEnv / apiKeyEnv+oauth conflict rejections at runtime
resolution (modelAuth) and at the refresh orchestrator (discovery,
with per-provider failure isolation), and the open-platform refresh
through api_key_env proving the resolved secret never lands in the
provider record. Also add the first direct unit tests for the shared
declaredProviderCredential / reconcileProviderCredentialUpdate
primitives.
* refactor(oauth): share reserved provider id rules between import and refresh
* Delete .changeset/registry-import-concurrent-credentials.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* test: trim cross-layer duplicate registry and credential coverage
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(agent-core-v2): add skill scopes and mark custom-theme as tui-only
Builtin skills can now declare a scopes whitelist (tui | web); skills
without scopes stay visible everywhere. The value is carried through
SkillSummary, the klient RPC contract, and the kap-server REST wire,
and the TUI slash commands filter by it. HTTP clients (code-app) drop
skills whose scopes exclude their own UI mode, so TUI-only skills like
/custom-theme no longer leak into app/web.
* fix: filter scoped skills from the acp and vscode palettes
ACP slash commands and the VS Code GetSlashCommands handler also
project SkillSummary lists; both now drop skills that carry a scopes
restriction so tui-scoped skills stay out of non-tui clients.
- scrub all ambient KIMI_CODE_* env vars in the agent-core-v2 and
kap-server test setups so developer shell env cannot leak in
- drop the load-sensitive stepRetry fake-timer cases and the three
FSEvents-based skill catalog watch cases
- give node-sdk, klient and the vscode extension project an explicit
15s testTimeout matching their integration-test nature
- retry temp-dir removal in taskManager teardown to absorb async
terminal-effect writes
* refactor: remove the legacy agent-core v1 package
Delete packages/agent-core (v1 engine) and migrate node-sdk fully onto
agent-core-v2:
- node-sdk owns its public contract (error protocol, KimiConfig schema
and file IO, process-global logging facade, telemetry client
interface); event and interaction types re-export from
@moonshot-ai/protocol or agent-core-v2
- createKimiHarness is now the single harness factory, backed by the
v2 client; the v1 SDKRpcClient is deleted
- CLI and vscode run agent-core-v2 unconditionally;
KIMI_CODE_LEGACY_FLAG and useAgentCoreV1 are removed
- the wire.jsonl replay fold moves into agent-core-v2's replayBuilder
(foldWireRecords), and the v2 journal now persists tool-call display
so replay restores diff/todo displays
- TUI engineV2 branches collapse to the v2 path
* test(vscode): retry temp-dir teardown in replay-resume integration test
* fix(migration-legacy): faithfully migrate kimi-cli user data into kimi-code
* fix(migration-legacy): faithfully migrate kimi-cli user data into kimi-code
* fix(vscode): keep current and root work dirs visible in directory picker
* refactor(vscode): migrate the webview data layer from ahooks to TanStack Query
Concurrent openSession/attachResumedSession calls for the same webview
both missed the sessions map before either wrapped the SDK session, so
one Session facade got two SessionRuntimes. The overwritten runtime
leaked and kept broadcasting, doubling every streamed delta and tool
call in the view. Queue opens, attaches, and detaches per webviewId so
the second caller sees the first one's runtime.
* fix(kimi-code): persist picked thinking effort up to the model's default effort
The persistence gate kept the model's top declared effort session-only
unconditionally, so users whose delivered default_effort is the top tier
(e.g. max) could never save an explicit pick of it. Compare the pick
against the model's default_effort instead, using support_efforts as the
strength ordering: picks above the default stay session-only, picks at
or below it persist. Models without a declared default keep the
historical top-tier rule. The same change lands in the VS Code
extension's mirrored logic.
* docs(kimi-code): document the effective-default ceiling for effort persistence
Clarify in both apps' comments, the changeset, and the config docs that
the persistence ceiling is the model's effective default effort, whether
declared via the catalog / overrides or synthesized by the protocol
profile inference (Claude models resolve to high, so an xhigh pick is
session-only there). Pin the inference path with tests in both apps.
* fix(vscode): resolve the save-config model with its provider type
Mirror the TUI's effectiveModelForHost: without the provider type the
Anthropic fallback profile (e.g. claude-latest) never matches, so the
inferred default effort that gates persistence was missed and an
above-default pick could persist where the TUI keeps it session-only.
* Delete .changeset/persist-effort-up-to-model-default.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(vscode): seed the persisted effort with the effective-default ceiling
* fix(vscode): project webview models with the provider type
* fix(kimi-code): apply a session-only effort pick to the runtime in the /provider flow
* fix(vscode): update the persisted-effort seed on model-switch saves
* docs(kimi-code): drop the default_effort persistence-ceiling note
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(kimi-code): add China/International region selection for OAuth login
- Add region profiles (cn/overseas) and resolver in @moonshot-ai/kimi-code-oauth:
env override → persisted login host → install-channel marker → default cn
- /login now offers Kimi Code (China) / Kimi Code (International); the CLI
login entries (kimi login, kimi acp --login) accept --region cn|overseas
- Update/plugin/site/telemetry endpoints derive from the selected region;
plugin trust list covers both .com and .ai hosts
- kap-server: POST /oauth/login accepts an optional region; new GET /oauth/region
* fix(oauth): keep an explicit default-slot login ahead of the install marker
A China login persists no oauthHost (the default credential slot carries
no host trace), so after switching back from International the resolver
fell through to a stale overseas install marker. Treat a persisted
default-slot oauth ref (key === oauth/kimi-code) as an explicit-cn signal
that outranks the marker; getRegion() on the v2 side mirrors it.
* fix(agent-core-v2): thread the default-slot key through capability region resolution
Capability installs resolved the region from the persisted oauthHost only,
so an explicit China login (which persists no host) lost to a stale
overseas install marker. Pass the oauth ref key through as well, matching
getRegion(). Also move the region contract notes into the auth.ts file
header per the package comment convention.
* fix(agent-core-v2): honor the region-marker opt-out for the telemetry endpoint
Hosts that set KIMI_CODE_REGION_MARKER=off (the desktop embedded server)
skip the install marker in getRegion(), but the default telemetry endpoint
still consulted it, so a stale overseas marker could split the reported
region from the telemetry destination.
* feat(cli): show region site domains in login platform selector
* chore: reword oauth login changesets
* fix: honor the region marker opt-out in the CLI and capability resolvers
* refactor: rename login region values to mainland-cn and global
* fix: keep the --region help text in English
* fix: simplify the --region help text to site domains
* feat: drop the suggested login platform order
* feat: split a browser-safe region profile table out of the region resolver
* Revert "feat: split a browser-safe region profile table out of the region resolver"
This reverts commit a037b1143eeb8adcbb349264b5bd940f3ef9c0b3.
* fix: read the install marker from the bootstrapped home directory
* fix: resolve the server plugin marketplace from the active login region
* feat: expose the login region option through the klient auth facade
* fix: drop a comment from the v2 auth region test
* fix: keep scoped base-only logins on their environment for a bare login
* fix: invalidate the region cache on the provider-manager logout path
* fix: route client-config fetches through the active region profile
* fix: resolve the telemetry endpoint per flush so a login region switch applies in-process
* test: expect the telemetry endpoint resolver in the CLI init assertions
* fix: resolve the default telemetry endpoint from the bootstrapped home
* chore: reword the oauth login changeset around the two login methods
* chore: trim the oauth login changeset to the headline
* feat: let hosts override the region marker env through the server bootstrap env bag
* fix(vscode): multi-select question jumps to next after only one answer selected
* chore: add changeset
---------
Co-authored-by: gaoyuan <gaoyuan@moonshot.ai>
* feat(agent-core): unify the v1 MCP management plane
- McpServerRegistry: one config view over global (layered mcp.json),
plugin (manifests, read-only, final effective config), and caller
(SDK-injected) servers; name collisions keep both entries.
- Write plane: add/update/removeGlobalMcpServer mutate the user-level
file and push into live sessions; getGlobalMcpServer returns the
effective config; mutations of read-only entries are rejected.
- testGlobalMcpServer accepts an inline config; addSessionMcpServer
connects a server in one live session with an optional persist flag;
reconnect accepts a replacement config and re-resolves via the registry.
- One process-wide McpOAuthService shared with every session: obtained_at
stamps, offline token state, single-flight and proactive refresh, and
credential events. Sessions self-subscribe in the constructor, so even
initializing sessions see every event; token writes serialize through
the process-local OAuthTokenTransaction per credential identity.
- inspectAppMcpServers + locator-addressed begin/complete/cancel/reset
cover plugin servers; inspection output redacts env/headers to sorted
key lists; locator OAuth ops reject ambiguous shared runtime names.
- The legacy auth-status surface reads the registry (offline by default,
verify=true probes) and never mutates credentials.
- VS Code panel receives source/origin/mutable and hides mutating
actions on read-only entries.
- v2 client facade in node-sdk mirrors the surface over agent-core-v2
(plugin inventory stays v1-only for now).
* fix(agent-core): close the v1 MCP live-session reconciliation gaps
Recompute each live session's MCP target from the registry's runtime
resolution (enabled plugin > project layer > user file; caller injection
shadows everything) behind every config mutation, instead of per-path
patching: shadowed file layers recover when a plugin winner is disabled or
removed, removing a user-level entry resurrects its project-layer shadow,
disabled plugin descriptors no longer block removals, persisted session
adds validate against the session's project layer and broadcast to other
live sessions, and per-session sync failures are logged with context.
Session status entries and read-only management entries now report
redacted config views (envKeys/headerKeys instead of literal env/headers
values); core-internal reconciliation compares full configs via the
connection manager's raw-entry accessor.
OAuth: interactive flows are serialized per credential (concurrent
begins join the in-flight flow instead of clobbering its PKCE/state), a
malformed credential meta sidecar no longer aborts core start, grants
inside the refresh-ahead window refresh immediately while far-future
grants re-arm through a max-length timer, and the service shuts its
timers and flows down with KimiCore/SDKRpcClient close.
* fix(agent-core): route the proactive MCP OAuth refresh through the token transaction
refreshNow ran its /token request with the SDK default fetch, outside the
credential-serializing OAuthTokenTransaction that every other token write
uses; a slower response carrying an older rotating refresh token could
overwrite a newer grant written by a concurrent transport-side refresh.
* fix(agent-core): keep disabled MCP servers out of auth-state classification
The unified mcpServerAuthState dropped the previous enabled short-circuit,
so a disabled oauth-flagged server reported oauth-required — or was even
probed over the network — instead of not-applicable.
* fix(kimi-code-sdk): short-circuit disabled MCP servers in the v2 auth-status classifier
The v2 parity copy of v1's mcpServerAuthState missed the same enabled
guard v1 just regained; a disabled oauth-flagged entry would report
oauth-required (or be probed). The parity suite now pins the disabled
case on both engines.
* fix(kimi-code): refresh the VS Code MCP list with the workspace cwd after mutations
The add/update/remove RPCs return a cwd-less management list, so the
webview broadcast dropped project-layer entries until the next full load;
re-list with the workspace cwd after every mutation instead.
* fix(agent-core): keep SDK token saves matched to the OAuth token transaction
saveTokens stamped obtained_at onto a fresh object before calling
tokenTransaction.save, so it never matched the exact payload the
transaction recorded for a grant fetch; the consume path was dead and
every save re-wrote. Between the fetch and the SDK callback an intervening
clear could then be overwritten — the resurrected grant came back after a
reset. The write callback stamps the durable record instead.
* fix(agent-core): reject ambiguous legacy name-based MCP auth lookups
The legacy begin/reset auth RPCs took the registry's first name match,
silently starting OAuth for one entry of a runtime-name collision while
the locator path refused the same ambiguity; align them on the shared
uniqueness rule and point callers at the locator-addressed variants.
* fix(agent-core): propagate registry errors during live-session MCP sync
resolveMcpRuntimeTarget collapsed every registry failure into "no target":
a project config file that turned malformed mid-session made sync treat a
still-configured server as gone (tearing down the live connection) and
made config-aware reconnects report "no longer configured" instead of the
actionable config error. Absence still resolves to undefined; malformed
config now propagates — per-session sync logs and keeps the entry, and
reconnect surfaces config.invalid.
* fix(agent-core): close the remaining registry-error and ambiguity gaps
The management guard lookup mapped every registry failure to "absent",
so a malformed project config let a persisted session add write a
user-level entry over an unknown state; only not-found is a miss now. And
the name-only connection test now shares the auth paths' uniqueness rule
instead of probing the first match of a runtime-name collision.
* fix(agent-core): probe the enabled MCP entry under a disabled-name collision
The name-only connection test counted enabled matches for its ambiguity
guard but still probed the first registry match, and the file layers list
before plugins. With a disabled file entry shadowing an enabled plugin of
the same runtime name, Test probed the disabled entry instead of the one a
live session would run. Select the sole enabled match, falling back to the
first entry only when every match is disabled so it reports as disabled.
* fix(agent-core): let session-local MCP adds shadow plugin entries
Caller injection shadows every registry source at session start, plugins
included, and reconciliation leaves caller entries untouched; the live
non-persist add path rejected plugin-owned names anyway, so SDK clients
could not apply the same per-session override without a restart. Gate the
plugin-source rejection on persist: session-local adds connect as caller,
while persisted adds stay rejected as user-level writes behind a read-only
owner.
* fix(agent-core): normalize session MCP names before connecting
The persisted store trims server names, but addSessionMcpServer used the
raw name for the live connect and cross-session reconciliation: a padded
name persisted under the trimmed key while the requesting session ran and
reconciled the raw one, and a blank name connected with no identity at
all. Normalize once up front (rejecting blank) so the store write, the
session entry, and reconciliation agree on the same server.
* fix(agent-core,node-sdk): close the collision-selection and probe-freshness gaps
The legacy name-only auth resolver started from the first registry match,
so a disabled file-layer shadow plus an enabled plugin of the same runtime
name was misread as an ambiguity conflict; select the sole enabled match
before judging ambiguity, exactly like the test probe path. On the v2
client, addSessionMcpServer connected the raw name while the store wrote
the trimmed key — normalize once for both, and route the verify-triggered
auth probes through the per-call OAuth service instead of the cached one
whose providers snapshot tokens at construction, so a grant saved after
the first probe is honored.
* fix(agent-core): normalize global MCP mutation names and guard disabled reconnect swaps
The global add/update/remove mutations guarded and reconciled with the raw
server name while the store persisted the trimmed key, so a padded name
left live sessions unreconciled and could slip past the plugin read-only
guard; normalize once before lookup, persistence, and reconciliation. And
a config-carrying reconnect assigned the replacement before the disabled
check fired, leaving a connected entry that reported the disabled config;
reject disabled replacements before mutating, keeping the same error.
* fix(agent-core): skip proactive refresh while an interactive flow owns the credential
refreshNow reset the shared provider's flow state before and after the
token request; when a proactive timer (or a manual refresh) fired while
beginAuthorization was waiting on the browser callback for the same store
key, that wiped the redirect URL, PKCE verifier, and state the in-flight
flow needed — complete() then failed the exchange even though the user
authorized. Refresh now skips when an interactive flow is active for the
credential: the flow delivers fresh tokens on completion, and the 401
transport path is the backstop if it fails.
* fix(agent-core): allow global MCP adds over disabled plugin descriptors
A disabled plugin entry is absent from the runtime target, but the
read-only guard still treated it as the owner, so a user-level fallback
could only exist if it predated the plugin disable. Relax the shared
guard: disabled plugin descriptors never block mutations (disabled
project entries still shadow the user file and keep their rejection).
* fix(node-sdk): close the v2 session-MCP parity gaps
A v2 reconnect with an explicit enabled:false replacement config used
connect()'s upsert semantics — closing the live client and reporting
success where v1's manager reconnect rejects before applying anything;
reject disabled replacements up front with the same error. And a persisted
v2 session add never consulted the workspace config, so a same-named
project-layer entry was silently shadowed: the user-level write never
takes effect while the direct workspace-manager upsert displaces the
project config for every live session. Resolve the workspace layers and
reject like v1's read-only rule.
* fix(agent-core): keep __proto__-named MCP servers through config parsing
A z.record() parse rebuilds its output via property assignment, so a
server literally named __proto__ hit the prototype setter and vanished
before validation; the layer merge then repeated the same trap with plain
object accumulators. Parse the server map entry-by-entry over the JSON own
keys and accumulate into null-prototype maps, so session startup and the
unified registry keep the declared server and its origin.
* fix(node-sdk): begin v2 MCP auth against a fresh OAuth service
The v2 begin path ran through the cached globalMcpOAuth, whose providers
snapshot tokens at construction: a grant another process saved (or reset)
after that cache materialized was invisible, so begin could open a browser
flow over a valid grant, or report already-authorized off a removed one.
Build the service per call — the read path and the verify probes already
do — and route the status list through the same helper. The test fixture
grows a real token endpoint honoring one rotating refresh token; the
regression fails against the cached-service implementation on v2.
* fix(agent-core): broadcast SDK-driven MCP token invalidations to live sessions
* test(agent-core-v2): give the no-op reconnect test runtime plumbing
The branch added the case against a bare McpConnectionManager, but #2961
made stdio connects resolve the runtime through runtimeResolver, matching
every other case in the file.
The extension now runs on the agent-core-v2 engine by default. The
interface, sessions, and workflows do not change. Two rollback paths
exist, and one function makes the decision
(config/vscode-settings.ts):
- the kimi.useAgentCoreV1 setting (temporary; a window reload applies
the change);
- the KIMI_CODE_LEGACY_FLAG environment variable, which wins over the
setting and has the same semantics as in the CLI.
An engine startup failure shows an explicit error that names the
rollback setting. There is no silent fallback. CI runs the extension
test suite on both engines: the sharded run covers the default v2
engine, and a new test-vscode-legacy job reruns the suite with
KIMI_CODE_LEGACY_FLAG=1.
To keep the v2 path identical to v1 for every method the extension
uses, this change also completes the v2-backed SDK client and the v2
engine:
- Implement session deletion in the v2 SDK client.
- Implement fork truncation at a turn index in the v2 engine, with the
same rules as v1, and reject a fork while the source session has an
active turn.
- Stop the session-level /init run when the turn is cancelled, as v1
does.
- Read session metadata without the archived field as not-archived, so
sessions written by the v1 engine open correctly.
The SDK parity suite now covers session deletion, cancel, and fork
truncation. The known-difference list for the methods the extension
uses is empty.
* refactor(oauth): make X-Msh-Platform an explicit host identity field
X-Msh-Platform was hardcoded to kimi_code_cli in createKimiDeviceHeaders,
so non-CLI hosts could not state their own platform and the desktop had
to patch the header after the fact. KimiHostIdentity now carries a
required platform (every host declares its own value; the CLI constant
stays the fallback only for direct createKimiDeviceHeaders callers), and
userAgentProduct is renamed to productName so the transport identity
uses one name everywhere.
All in-repo identity constructions pass platform explicitly; the wire
value for CLI and VS Code hosts is unchanged (kimi_code_cli).
* feat(agent-core-v2): carry the host identity in the bootstrap snapshot
Replace the flat clientVersion field with a required clientIdentity
(KimiHostIdentity) so every consumer reads the same host identity
object: OAuthToolkitService now passes it to the OAuth toolkit, which
means the OAuth device-flow endpoints (device authorization, token
polling, refresh) on the kap-server path finally send the full X-Msh-*
device headers instead of none, and the telemetry cloud appender reads
client_version from the same source. A built-in CLI fallback keeps bare
bootstrap() calls in tests working; composition roots must pass their
own identity.
The session export manifest grows an optional desktopVersion field
(payload plumbed through; filled by kap-server in a follow-up).
* feat(agent-core): thread the host identity into the managed auth facades
The v1 managed auth facade constructed its OAuth toolkit without an
identity, so token refreshes from inside the core went out without any
X-Msh-* device headers. createManagedAuthFacade now takes an optional
KimiHostIdentity and every call site supplies one:
CoreProcessService._defaultOAuthTokenResolver forwards the core
process's options.identity (the same source _defaultKimiRequestHeaders
uses), and the DI-held services (oauth / auth summary / model catalog)
read it from a new optional identity field on IEnvironmentService. The
library-level "no identity, no device headers" contract is unchanged.
* feat(kap-server)!: require the host identity and derive request headers from it
ServerStartOptions.hostIdentity is now a required ServerHostIdentity
(KimiHostIdentity + optional prompt display fields), replacing both the
old optional HostIdentityOverrides (renamed to PromptIdentityOverrides,
its productName field now displayName) and the version option (renamed
to serverVersion — it is the engine version reported as server_version,
while the host product version travels in hostIdentity.version).
The server now feeds bootstrap's clientIdentity from hostIdentity and
derives the default outbound headers (User-Agent + X-Msh-*) from it via
createKimiDefaultHeaders, so kap-server-hosted OAuth flows and model /
WebSearch requests carry the real host identity instead of a hardcoded
kimi-code-cli fallback UA. Explicit header seeds still win as an escape
hatch.
Session export manifests record the host product version: kimiCodeVersion
now carries hostIdentity.version (the engine version no longer appears),
and desktop exports (desktop: true) are additionally stamped with a
desktopVersion field. The instance registry keeps its host_version wire
field for compatibility (kimi-inspect reads it); only the in-memory name
changed to serverVersion.
* feat(cli): wire the CLI host identity into the kimi web server
kimi web now passes createKimiCodeHostIdentity(version) as the server's
hostIdentity, so web-UI OAuth flows and the engine's outbound requests
carry the explicit CLI identity (productName + version + platform). The
explicit hostRequestHeadersSeed is dropped — kap-server derives the same
headers from hostIdentity — and buildKimiDefaultHeaders goes away with
its only consumer.
* test(klient): drop clientVersion from the bootstrap contract parity list
* chore: add changesets for the host identity unification
* feat(cli): tag kimi web requests with a (web) User-Agent suffix
kimi web shares the CLI product token and platform, so its outbound
requests were indistinguishable from direct CLI runs upstream. Its host
identity now carries userAgentSuffix 'web', putting web-UI traffic at
kimi-code-cli/<version> (web) while X-Msh-Platform stays kimi_code_cli.
* fix(klient): keep the env() clientVersion wire field after the bootstrap identity switch
The bootstrap snapshot replaced the flat clientVersion scalar with
clientIdentity, which broke klient's env() fan-out (RPCError: method not
found). The wire surface keeps clientVersion — now sourced from
clientIdentity.version — and bootstrapService gains a clientIdentity
read (registered in envContract with an object schema) for consumers
that want the full identity.
* feat(oauth): send the product User-Agent on OAuth requests
The OAuth endpoints used to receive only the X-Msh-* device headers
(undici's default UA otherwise), which left the OAuth host unable to
distinguish runtime surfaces — notably kimi web, whose platform matches
the CLI and whose only distinguishing mark is the (web) UA suffix. The
toolkit now feeds the full identity headers (User-Agent + X-Msh-*) into
every device authorization, token polling, and refresh request; the
request-header type widens from DeviceHeaders to OAuthRequestHeaders.
* feat(vscode): report kimi_code_vscode as the extension's platform
The VS Code extension inherited the CLI's hardcoded X-Msh-Platform value;
with platform now an explicit identity field it declares its own, so the
managed endpoints and OAuth host can tell extension traffic apart from
CLI runs.
* refactor(agent-core-v2)!: require the client identity at the composition root
The bootstrap fallback identity fabricated a kimi-code-cli/unknown host
for any caller that forgot to pass one — the same silent-misreport
pattern this series set out to remove, and it made "required" a lie.
BootstrapInput.clientIdentity is now required, so a missing identity
fails at compile time instead of being papered over. Test and example
callers pass a shared fixture (klient examples and test engines get one
each); the node-sdk v2 client asserts its host identity with the oauth
helper. Also folds DeviceHeaders from an interface into a type alias so
it stays assignable to the widened OAuthRequestHeaders record.
* feat(oauth)!: require and validate the platform in device headers
Drops the quiet CLI fallback in createKimiDeviceHeaders (the same
silent-misreport pattern removed from the bootstrap identity): platform
is now a required option, validated with the same required-ASCII rule as
the version — empty or all-non-ASCII values throw instead of emitting a
blank X-Msh-Platform, and header-unsafe characters are stripped rather
than sent raw.
* fix(node-sdk): seed the host request headers on the v2 client path
The interactive v2 engine path (experimental flag) bootstrapped without
a hostRequestHeaders seed, so managed vendor calls went out with the
SDK's default User-Agent (OpenAI/JS) and no X-Msh-* at all — v1 passes
the full identity headers on the same requests. The v2 client now seeds
the headers from its asserted host identity, and a test pins the seed.
* chore: simplify the CLI changeset wording
Chat components subscribed to the entire chat store, so every
streaming delta re-rendered every assistant message. Subscribe to
narrow store slices and memoize ChatMessage so settled messages no
longer re-render on each delta. Cap getImageDataUri at 10MB, matching
the media picker, so oversized local images are not inlined into the
webview DOM.