perf(agent-core): drop dynamic timestamp from system prompt to restore prefix-cache hits - #2533
perf(agent-core): drop dynamic timestamp from system prompt to restore prefix-cache hits#2533daofazhiran wants to merge 33 commits into
Conversation
🦋 Changeset detectedLatest commit: 824ffdb The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ce756c37e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| The current date and time in ISO format is `${now}`. This was captured when the session started and does not update as the session continues, so in a long or resumed session it may be hours or days stale. Treat it only as a rough reference; whenever the real current time matters (web-result freshness, age or expiry checks, anything time-sensitive), get it fresh from the environment — for example by running `date` if you have a shell tool — instead of trusting this value. | ||
|
|
||
| ## Working Directory |
There was a problem hiding this comment.
Preserve time lookup guidance without the timestamp
For time-sensitive requests, deleting this whole section removes more than the dynamic ISO value: it also removes the only default-prompt instruction that the agent must refresh the real time from the environment. I checked the remaining default prompt and the only date mention left is the Bash tool's command list, so cases like expiry/age checks or “what is today?” can be handled from model priors or stale context instead of querying the host. Keep a static, cache-friendly reminder to check the current time when it matters, and mirror it in the legacy prompt too.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 61389c6ab — kept ## Date and Time as a static reminder ("get it fresh from the environment, e.g. via the date command") and mirrored it in the legacy template too. No per-session value remains, so byte-prefix caching is unaffected. The added tests assert the rendered prompt is byte-identical across different now values.
…eminder The ISO timestamp injected into the system prompt changes on every new session, breaking DeepSeek's byte-prefix cache from that point onward — including the ~16.8k-token tools definition that follows it. First-turn input drops from ~19.5k tokens cache-miss to ~88 after the change (measured 99.6% cache hit on the built artifact). The Date and Time section is kept as a static reminder to fetch the real current time from the environment (e.g. via the `date` command) when it matters.
…rompt Also add the required changeset (@moonshot-ai/kimi-code patch) for the prompt-cache fix.
6ce756c to
9218660
Compare
…nshotAI#2552) * refactor(agent-core-v2): code all domain failure modes as Error2 - wrap bare throws across agent/session/app/workspace/os/wire/kosong/mcpCore domains in coded Error2, keeping messages verbatim and moving structured data into details with the original error as cause - add new wire codes (agent.already_exists/already_running/not_a_subagent/not_owned/type_not_allowed/max_tokens_exceeded, task.limit_exceeded, cron.expression_invalid, web.invalid_url/private_address/fetch_failed, mcp.oauth_failed, skill.parse_failed/nested_too_deep, wire.migration_missing) to the protocol KimiErrorCode union and the kap-server zod schema; register shell.git_bash_not_found and session.plan_mode_invalid - re-base domain error classes onto Error2 (SkillParseError, UnsupportedSkillTypeError, HostFolder*, AgentFileParseError, NestedSkillTooDeepError, AlreadyAuthorizedError, HttpFetchError) keeping class names and instanceof consumers intact - convert caller-bug and unreachable guards outside _base to BugIndicatingError - fix the agent tool's task-limit remap never firing by branching on the task.limit_exceeded code instead of a stale message string * refactor(kosong): make ChatProviderError family born-coded via Error2 - move the provider/context code string constants to kosong/contract/errors.ts and compute each class's wire code at construction (status code / finish reason) - move sanitizeStatusErrorMessage to the contract and fold status details (statusCode / requestId / traceId) into Error2 details at birth - slim translateProviderError down to the abort guard plus the foreign-error fallback; ProtocolErrors keeps registering the domain via re-exported constants - update errors.md conventions and tests for the pass-through behavior * feat(storage): add permission_denied and disk_full error codes - extend StorageErrors with storage.permission_denied / storage.disk_full (both non-retryable, with user-facing actions) - map errno at the backend boundary in toStorageIoError: EACCES/EPERM, ENOSPC, unexpected ENOENT → not_found, everything else io_failed; the message now carries the mapped reason - register the two codes in the KimiErrorCode protocol union and the kap-server zod schema, and document the mapping in errors.md * fix(protocol): mirror all KimiErrorCode values in kimiErrorCodeSchema the zod enum lagged the type union by 35 codes (agent.*, os.fs.*, os.process.*, storage.*, wire.*, skill/task/mcp/cron/web additions), so protocol consumers could type the new codes but rejected them at runtime validation; spotted by Codex review on MoonshotAI#2552
…ade engine (MoonshotAI#2551) * feat(agent-core-v2): add lifecycle ledger, dynamic registry, and cascade engine - add `_base/lifecycle` Ledger: ordered registrations with strict reverse serial teardown, sync/async dual-track disposers, uninterruptible rollback, effect return forms, child ledgers, introspection tree, and teardown-reason propagation - delegate Disposable/DisposableStore/MutableDisposable, Scope.dispose, and InstantiationService.dispose to the ledger; retire _constructionOrder - ServiceCollection: entries carry uid/pinned/recipe, delete(), and a per-token availability event; add provide/unprovide to IInstantiationService - add a persistent dependency graph recording constructor-injection edges with affectedSet/topo queries, plus a per-container cascade engine (contagion teardown/rebuild transactions, five unit states, pending index, abort hook, history ring); TestInstantiationService.set routes via provide * fix(agent-core-v2): cascade instance replacements, tolerate abort rejections - provide/unprovide of pre-materialized instances now runs as a cascade transaction too, so live dependents are torn down and rebuilt instead of holding a stale dependency; re-affirming the live instance stays a no-op - a rejecting onWillCascade promise is logged and the cascade proceeds (best-effort), matching the synchronous-throw handling * feat(agent-core-v2): propagate cascades across scopes along instance edges Implements the revised D9 (the firewall design was dropped): instance edges are scope-tagged on both ends and point child -> parent only, the dependency graph is shared by the whole scope tree, and each change runs as one tree-wide transaction orchestrated by the submitting scope's engine — contagion computed over the global graph, teardown in global reverse topological order (deepest first), rebuild in global topological order, with each scope's engine executing its own units. Descendant scopes dying mid-transaction are skipped idempotently; the request queue, in-flight set, and settle waiters are tree-shared so cross-tree transactions are serialized by the orchestrator; shadowed tokens stay outside the contagion set.
…MoonshotAI#2553) The secondary_model section did not state whether spawned subagents are forced onto the secondary model or only default to it, nor the full override precedence. Make the semantics explicit in both locales: - spawning resolves the model in order: explicit tool-call model -> profile model_preference -> configured secondary model (default) - the tool's model parameter accepts only "primary" / "secondary" - "primary" means the model the main agent is currently running, not necessarily default_model - the user has no per-spawn switch; overriding is the main agent's decision or a profile setting Also unify secondary-model terminology and the [models] alias wording across the config-files, agents, slash-commands, and env-vars pages.
…I#2559) The "Already logged in. Model configuration refreshed." confirmation was rendered with the default dim text color, so users easily missed it and assumed /login did nothing. Render it with the theme's success color, matching the success styling used by the login spinner's "✓ Logged in." line. Co-authored-by: Mira Bot <mira-bot@moonshot.cn>
…ds (MoonshotAI#2558) * feat(agent-core-v2): add lifecycle hook events and enrich hook payloads New hook events: - TurnStarted: fired from the turn.started bus event, covering queued turns, stop-hook continuations, and background/system turns that UserPromptSubmit misses - UserPromptQueued: fired when a prompt cannot launch immediately, carrying the queue length - TaskStarted: fired from the existing task.started bus event, so background tasks no longer only produce a completion-time Notification - SessionHeartbeat: per-session 60s liveness beat, armed only when the event has hooks registered, letting hook consumers distinguish a session hanging on a long permission wait from a crashed one Payload enrichment: - client_type (host platform identity) on every event - session_title on every session/agent-scoped event - model and profile on SessionStart - SessionEnd reason is now 'exit' or 'archive' instead of a hardcoded 'exit' - SubagentStart/SubagentStop now carry session_id/cwd like every other event * fix(agent-core-v2): re-sync SessionHeartbeat timer on hook-index reloads The heartbeat timer was armed once after the runner's initial load, so a SessionHeartbeat hook contributed later by a plugin reload never produced beats for existing sessions. The runner now exposes onDidReload (fired after every index build), and the session adapter re-syncs on it: arming when a heartbeat hook appears, disarming when none remains. * fix(node-sdk): keep the v1 PluginInfo contract assignable with v2-only hook events The v2 hook-event union is now a superset of v1's, which broke the node-sdk type projection in two places: - the klient contract's hookDefSchema rejected plugin manifests using the new events (TurnStarted, UserPromptQueued, TaskStarted, SessionHeartbeat) at validation time — accept them - getPluginInfo returned the v2 PluginInfo where the SDK contract promises the v1 shape — project manifest.hooks through the v1-known event list (read from the legacy HookDefSchema), mirroring how the config mapper drops domains v1 does not know
…oonshotAI#2562) - move the v1 message protocol and projection out of the engine into kap-server and delete the engine-side messageLegacy edge adapter - add a shared message history loader that folds the main agent's wire journal into full history across compactions, backing both the messages routes and the snapshot endpoint - drop the disk-reading SnapshotReader fast path; assemble snapshots from engine services for cold and live sessions, removing the KIMI_SNAPSHOT_READER, KIMI_SNAPSHOT_TIMEOUT_MS and KIMI_SNAPSHOT_CACHE_LIMIT knobs - collect persisted wire record rebuild helpers in the transcript service
…oonshotAI#2572) * feat(config): add deprecation mechanism and rename loop retry limit - agent-core-v2 config: declarative section `deprecations` (deprecated TOML keys are ignored and report a warning diagnostic; the file is never rewritten) and env binding `deprecatedEnv` (old var still resolves as a fallback with a warning), surfaced via the new `IConfigService.onDidChangeDiagnostics` event - loop_control: rename `max_retries_per_step` to `max_attempts_per_step` and `KIMI_LOOP_MAX_RETRIES_PER_STEP` to `KIMI_LOOP_MAX_ATTEMPTS_PER_STEP`; `max_steps_per_run` moves onto the same mechanism (no longer silently mapped) - kap-server: push the global `event.config.warning` WS event to every connection whenever the config warning set changes - TUI: show config diagnostics in warning yellow at startup instead of the dim startup notice - docs: config-files/env-vars (en+zh), regenerated config manifest, and the agent-core-dev config guide * feat(cli): validate config.toml against v2 section registry in doctor - add v2/validate-config.ts: validate config.toml with the agent-core-v2 ConfigRegistry, reporting registered-section schema failures as errors and unknown top-level keys / deprecated keys and env vars as non-fatal warnings - route `kimi doctor` config validation through the v2 validator when the KIMI_CODE_EXPERIMENTAL_FLAG master switch is on (lazy dynamic import, keeping the v2 module graph off the default path) - let doctor checks surface non-fatal warning messages on OK results * chore: downgrade loop-control changeset to patch
…eep their tools (MoonshotAI#2567) * fix(agent-core): replay v2 profile.bind records so resumed sessions keep their tools Sessions created by the v2 engine (CLI 0.31+, wire protocol 1.5) persist the profile binding, including the tool allowlist, as a profile.bind record. The v1 replay path had no branch for it and silently dropped the record, so a session resumed by a v1 host (e.g. the VS Code extension via kimi-code-sdk) never called setActiveTools and sent requests with no tools at all (observed server-side as tools_count=0; the model emits reasoning only and stops with empty content). v1 replay now maps profile.bind onto config.update + setActiveTools when activeToolNames is an array, skips the record otherwise so the resume-time default-profile fallback still applies, and treats tools.reset_active_tools as a no-op. * fix(vis): handle v2 profile records in context projection * fix(agent-core): avoid synthetic replay for v2 profile binds * fix(vis): render v2 profile wire records
* feat(agent-core-v2): add AGENTS.md discovery reminder behind experimental flag When a tool call touches a directory whose AGENTS.md was not part of the injected instructions (the init-time load only covers the project-root to cwd chain), append a system reminder to the tool result suggesting the model read it, at most once per file per agent. The new agentsMdReminder domain (Agent scope, gated by the agents-md-reminder experimental flag, default off) hooks toolExecutor.onDidExecuteTool: Read/Edit/Write contribute their path, Glob/Grep their search root, and Bash its structured cwd plus literal directory operands extracted from the command's syntax tree via the in-repo bash parser (listing commands only, bare names, conservative skips). Probing walks projectRoot to the touched dir with the same candidate rules as the init-time load (shared helpers in profile/context), skipping fully-known directories and blank files. The known-set is seeded by profileService after each successful bind/apply/refresh and by sessionInit after /init, claimed synchronously per discovered file, and published as a whole value only after the reminder is attached, so parallel calls never duplicate a reminder and failures leave files eligible for the next touch. * fix(agent-core-v2): keep the AGENTS.md reminder on visible results and seed restored agents A same-step duplicate vetoed by toolDedupe carries a placeholder result that the dedupe hook swaps for the original's deferred result; attaching the reminder there discarded it while the file was already counted as reminded, and the telemetry still claimed it was shown. Skip the placeholder (the call id sits in toolDedupe.syntheticCallIds until the dedupe hook consumes it) so the reminder, telemetry, and known-set only advance on results that reach the model; the original call then carries the reminder for both by the time the deferred resolves. Session resume and forks commit an already-rendered system prompt (AGENTS.md content included) without going through bind/apply/refresh, so no seed point fired and the known-set lagged behind the injected set, producing false "not part of the injected instructions" reminders. The first qualifying call of a never-seeded agent now re-runs the init-time discovery with the same inputs (agent cwd, os home, brand home) and seeds from it, once per agent; a discovery failure leaves the agent unseeded so the next touch retries. * refactor(agent-core-v2): fold agentsMdReminder inline rationale into file headers The package comment convention keeps rationale in the top-of-file block only; move the inline blocks' unique increments there (hook-order fallback mechanics, synthetic-key existence condition, frozen-vs-live Bash base, operand-less vs failed-resolution listings) and derive AGENTS_MD_BASENAMES from AGENTS_MD_PLAIN_NAMES so the candidate names stay single-sourced. * fix(agent-core-v2): use resolved accesses for AGENTS reminders * feat(agent-core-v2): drop the agentsMdReminder experimental gate * fix(agent-core-v2): harden AGENTS reminder tool outcomes * fix(agent-core-v2): preserve actual tool execution outcomes * Persist AGENTS.md paths across profile restoration * test(agent-core-v2): update useProfile snapshot for agentsMdPaths
…nd measured anchors (MoonshotAI#2563) * feat(agent-core-v2): add tokenCounting service with strategy config and measured anchors - add IAgentTokenCountingService as the single owner of token counts: context size, full-request size, and estimate primitives, replacing the scattered contextSize/tokenEstimate/fullCompaction paths - add [token_counting] config section with strategy = measured+estimated (default) / measured / estimated, plus the KIMI_TOKEN_COUNTING_STRATEGY env override; measured zeroes all estimates, estimated ignores anchors - keep a live measured-anchor ledger in TokenCountingModel: each LLM exchange writes a real anchor, undo truncates the ledger so the surviving prefix restores its REAL measured size instead of a re-estimate, and compaction rebases to a single anchor that blends the compaction exchange's measured summary output tokens - skip writing an anchor when the stream reports no usage event instead of anchoring emptyUsage() zeros, which zeroed the context size and silenced compaction for providers without usage reporting - return the strategy-resolved size (not measured) from rpc getContext so the tokenCount contract stays correct under the estimated strategy - migrate all consumers (contextMemory, fullCompaction, llmRequester, rpc, mirrorAgentRun, sessionLegacy, kap-server legacyStatus, node-sdk, kimi-inspect) to the new service; edge bridges no longer read the wire model directly - document [token_counting] and KIMI_TOKEN_COUNTING_STRATEGY in the bilingual config reference * fix(kap-server): omit maxContextTokens instead of pushing 0 when unknown - readLegacyStatus falls back to the default model's context limit when no model is bound, and omits maxContextTokens entirely when the limit is unknown (0 is the engine's UNKNOWN_CAPABILITY marker, not a real limit) - profileService no longer emits maxContextTokens in agent.status.updated when the bound model alias does not resolve * fix(agent-core-v2): resolve token_counting strategy only at the reporting edge - keep measured anchors and heuristic estimates both recorded and feeding internal logic (compaction triggers, budgets, overflow backoff) regardless of the configured strategy - add IAgentTokenCountingService.statusSize() as the single strategy-resolved outward reading and route the WS/REST/RPC status surfaces through it - fix the context-size display falling back to provider-reported usage under the estimated strategy - fix compaction overflow backoff retrying identical messages until failure under the measured strategy (the strategy-gated estimator read as 0)
…oonshotAI#2571) * feat(acp): add agent-core-v2 ACP server - add ACP session lifecycle, configuration, permissions, and event bridging - expose the experimental kimi acp-v2 command with terminal authentication - add integration coverage and workspace build configuration * test: use neutral example domains in test fixtures and docs - replace placeholder hostnames (evil.com, foo.com, internal.corp, real.corp) with example.test / example.com in agent-core-v2 and kap-server tests - replace fixture emails (x@y.com, a@x.com) with example addresses in minidb tests and README * fix(acp): align acp-server with agent-core-v2 interfaces and address review - add missing appendText to AcpHostFileSystem (IHostFileSystem drift) - replace IAgentPromptService.prompt with inject - use Turn.cancel() instead of abortController - gate FS reverse-RPCs on client capabilities, fallback to local FS - return PROTOCOL_VERSION constant instead of echoing client version - remove misleading mcpCapabilities from initialize response - dispose old session wrapper before replacing on load/resume - fix object stringification lint error in convert.ts - add acp-v2 to expected CLI sub-command list in test * fix(acp): use enqueue for prompt submission, stop advertising unimplemented builtins - replace IAgentPromptService.inject with enqueue so onBeforeSubmitPrompt hooks (prompt-blocking policy) are not bypassed - stop advertising builtin slash commands (/help, /status, etc.) until builtin command execution is implemented - add comment explaining appendText stays local (ACP has no append RPC) - update skills test to match new availableCommands behavior * fix(acp): filter turn events by turnId, surface auth failures as auth_required - track turnId in driveTurn and ignore events from unrelated turns, preventing queued prompts from settling on the running turn - reject prompt requests with auth_required when turn fails with an auth-related error code, enabling ACP client re-auth flow * fix(acp): gate acp-v2 behind experimental flag, filter sessions by cwd - add acp-v2 experimental flag (KIMI_CODE_EXPERIMENTAL_ACP_V2) and gate CLI command registration behind it - filter session/list results by requested cwd instead of returning sessions from all workspaces - detect hook-blocked prompts via PromptHandle.state and add TODO for streaming block messages once the hook context exposes them * refactor(acp-server): rewire ACP server onto the klient facade - replace direct agent-core-v2 scope/service access (ISessionLifecycleService, ISessionIndex, IEventBus, ISessionInteractionService, etc.) with the Klient facade: klient.global.sessions / klient.session(id) / agent('main') handles - drive turns via agent.prompt() + session-level agent event subscriptions instead of per-prompt IEventBus wiring; settle on turn.ended - route approval/question bridging through session.interactions events - hide the thinking config option and skill catalog behind KLIENT-GAP markers until klient exposes those surfaces - acp-fs: pass realpath through to the local inner backend - klient: session.restore() rejects both null and undefined handles * feat(agent-core-v2): add session delete and ephemeral per-session MCP servers - add ISessionLifecycleService.delete: close a live session first, then remove its persisted data, evict the index read-model entry, and append a deleted tombstone to session_index.jsonl; unknown ids raise session.not_found - add CreateSessionOptions/ResumeSessionOptions.mcpServers: session-owned MCP overlay merged over the workspace manager via MergedMcpConnectionView (an ephemeral name shadows a workspace server), never persisted, released when the session scope tears down - return PromptLaunchResult from activateSkill so callers get the launched turn id and activation failures (unknown skill, busy) surface - add ISessionSkillCatalog.list() as a wire-friendly catalog snapshot - add ISessionIndex.remove for read-model eviction on delete * feat(klient): expose session delete, per-session MCP, skills, and stream events - session lifecycle contract: delete, resume/restore options, and CreateSessionOptions.mcpServers (ephemeral per-session MCP servers) - add the session skills contract and facade accessors for the wire-friendly skill catalog snapshot - register tool.call.delta, tool.progress, and compaction.* agent stream events so consumers can subscribe with typed payloads * feat(acp-server): align ACP v2 server with acp-adapter capabilities - complete the klient-facade rewire: ACP client connection holder and the terminal/* reverse-RPC runner routed through the Agent scope - negotiate the protocol version on initialize instead of pinning v1 - compress oversized prompt images at the ACP ingestion point with a format gate, caption, and persisted originals; a cancel arriving mid-compression settles the prompt as cancelled without a turn - stream tool call args via tool.call.delta (lazy pending create, cumulative replace, started upgrade) and refresh titles via tool.progress status updates - report compaction progress and results after /compact via the compaction.* events - answer unknown slash commands locally instead of sending them to the model - accept legacy "<id>,thinking" model ids and legacy approve / approve_for_session approval option ids - keep sessions without cwd metadata in cwd-filtered session/list - sanitize wire errors: auth codes map to auth_required, turn.agent_busy to invalid_request, everything else to a fixed internal-error message - bump @agentclientprotocol/sdk to ^1.3.0 * fix(cli): drop stale registerServerCommand call and sherif ACP SDK split - commands.ts called registerServerCommand, which no longer exists on current main (the deprecated `kimi server` shim is registered via registerWebCommand), breaking typecheck, build, and every CLI test that builds the program - sherif rejects the @agentclientprotocol/sdk major split between acp-adapter (^0.23.0, production kimi acp) and acp-server (^1.3.0, experimental); the two hosts legitimately target different SDK majors, so ignore the dependency in the sherif invocation * test: update fixtures for acp-v2 flag and domain rename, refresh nix deps hash - kap-server origin.test: two CORS cases still used foo.com after the whitelist moved to foo.example.com, so the origin was no longer whitelisted and the expected CORS headers were withheld - node-sdk config.test: expect the new acp-v2 experimental flag in the harness feature metadata - flake.nix: update the fetchPnpmDeps hash for the @agentclientprotocol/sdk 1.3.0 lockfile change * fix(acp): widen the ACP v2 auth gate beyond OAuth-only providers The gate consulted only auth.summarize(), which iterates providers declaring an oauth section — configurations that authenticate with a plain apiKey or provider env-bag credentials (no OAuth at all) were rejected with auth_required even though the default model is fully usable. - klient: expose authSummaryService.ensureReady on the global auth facade (the contract already declared it) - acp-server: gate on the engine's own readiness probe for the default model — config apiKey / env-bag / OAuth token all count, matching how the model is actually used — and fall back to "any logged-in OAuth provider" (the legacy adapter's first branch) - test: an apiKey-only config passes the gate with auth enforcement on; the OAuth logout regression is unchanged * fix(acp): reject concurrent prompts instead of displacing the in-flight turn A second session/prompt while a turn is running overwrote the session's only TurnDriver: the engine quietly queues plain prompts submitted during an active turn (the launch resolves undefined, indistinguishable from a hook-blocked launch), so the first prompt never settled and both turns' events went unattributed. Guard both model-bound launch paths (plain prompt and skill activation) with a synchronous in-flight check and reject with invalid_request (turn.agent_busy), matching the legacy adapter's busy semantics. Local slash handling (builtins, unknown-command answers) is unaffected.
…lash commands (MoonshotAI#2583) * fix(acp): preserve cancels that arrive before the turn id is known A session/cancel landing between prompt submission and the launch round-trip found driver.turnId undefined and was dropped entirely; the turn then ran to completion and the prompt resolved end_turn despite the client's cancel. The engine's cancel payload makes turnId optional (an empty call cancels the active turn — the same contract kap-server's cancel route relies on), so cancel() now issues an unaddressed cancel in that window and flags the driver; the launch handler re-issues a precisely addressed cancel once the id lands, and a no-launch outcome settles cancelled instead of end_turn. * fix(agent-core-v2): shut session MCP overlays down on service teardown The ephemeral per-session MCP overlay was only shut down by the session handle's dispose wrapper, but the DI container disposes session scopes directly on workspace/app teardown, bypassing the wrapper — so overlays of sessions still live at shutdown leaked their MCP connections and stdio child processes. Track live overlays in the lifecycle service: the handle wrapper deletes-then-shuts-down (atomic, so close and service disposal can never double-shutdown), and the service's own dispose shuts down whatever is still tracked. * feat(acp-server): bridge questions via elicitation and support host slash commands - route AskUserQuestion through `elicitation/create` for form-capable clients (native multi-question + multi-select), falling back to the `request_permission` bridge on RPC failure - add a `slashCommands` resolver option so hosts can merge their own command palette and skill aliases into `available_commands_update`; `/help` now lists the merged palette - bridge `appendText`/`writeBytes` through client text capabilities (read-modify-write append, UTF-8-checked byte writes) with local filesystem fallbacks - defer `available_commands_update` until after the lifecycle response settles so clients like Zed do not drop the notification - propagate plan-toggle errors from `setMode` instead of silently reporting the new mode; make server `close()` idempotent * style(acp-server): satisfy oxlint eqeqeq and await-thenable rules * test(node-sdk): assert v1-v2 tokenCount parity for imports after eager counting
…onshotAI#2585) * fix(kap-server): accept question ids containing colons on resolve Some OpenAI-compatible providers emit tool_call ids like `AskUserQuestion:0`, which the question service adopts as the question id. The action-suffix parse then rejected the bare resolve POST as an unsupported action (40001), so clients could never submit answers. When the suffix parse fails, fall back to matching the full tail against the pending question list before emitting 40001. Also add maxRetries to the test home cleanup to absorb the async query-store shard flush (ENOTEMPTY on macOS), matching fs.test.ts. * fix(kap-server): preserve 40902 on duplicate resolve of colon-id questions A retried bare resolve of a colon-bearing question id re-entered the invalid-suffix fallback after the question settled, found no pending match, and returned 40001 — bypassing the recently-resolved idempotency window. Accept the tail in the fallback when it is recently resolved so the shared duplicate-resolve path emits 40902 as documented.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: /fork no longer switches to the forked session Forking used to switch to the new session, which closed the source session and force-stopped its background tasks (and canceled any in-flight turn). /fork now creates the copy and stays in the current session; the fork can be opened explicitly via /sessions. * fix(tui): release fork runtime when staying current
…el picker (MoonshotAI#2591) * feat(agent-core-v2): advertise model capabilities in the subagent model picker * chore(agent-core-v2): follow header-only comment convention * docs(agent-core-v2): note the model catalog collaborator in the AgentSwarmTool header
…oonshotAI#2458) * feat(tui): lazy-create the session on first use with the v2 engine Start the interactive TUI session-less under the v2 engine and create the session on first use (message, bash input, or a session-requiring slash command) instead of at startup. Skills and plugin commands are resolved from the workspace/app-global catalogs without a session, and the footer shows config defaults (model, permission, plan mode, thinking effort, context cap) until the session exists. * fix(tui): serialize lazy session creation and carry session-only thinking Concurrent first-use triggers (double Enter, a slash command right after a prompt) both observed `session === undefined` and created their own session, letting the later setSession close the first one mid-dispatch. Share an in-flight creation promise instead. A session-only thinking choice (model picker Alt+S) made before the first session exists only updated appState, so the lazy-created session fell back to the engine default while the footer showed the chosen effort. Carry it as a first-session thinking override and clear it on creation. * fix(tui): don't re-enter plan mode when creating the lazy session The v2 engine applies config.defaultPlanMode at session create time (sessionLifecycleService), so passing the pre-filled appState.planMode as the create override entered plan mode twice and threw 'Already in plan mode' on the first message. Pass only the explicit CLI --plan intent for session-less v2 creation; the config default stays footer-only. * fix(tui): re-check the busy gate after lazy shell startup, keep /settings session-less A bash command submitted while the first prompt is still being lazy-created shared the in-flight creation promise but resumed past handleUserInput's busy check, running runShellCommand concurrently with the prompt. Re-check streamingPhase after the await and queue instead. /settings is a local settings entry point: opening it must not require or create a session, so it no longer triggers lazy creation; its session-requiring sub-items keep their own errors. * fix(tui): re-check busy state after lazy command creation, make /plugins session-free A skill/plugin or idle-only slash command submitted while the first prompt was still being lazy-created shared the in-flight creation promise but resumed past the availability check resolved before the await, running concurrently with the prompt's turn. Re-check the busy gate after the shared await in the skill, plugin-command and builtin dispatch paths. /plugins is app-global on the v2 engine, so a session-less startup no longer creates a session (or fails with LLM-not-set) just to manage plugins: the harness now exposes the global plugin API and the command routes through it until a session exists. * fix(tui): keep the read-only /add-dir forms session-less The bare and `list` forms already tolerate a missing session, but the blanket lazy-create gate forced a session (or failed with LLM-not-set) before they could run. Only the path-adding form needs a live session, so it now lazy-creates inside the handler instead of in the dispatch preflight. * fix(tui): reflect pending dirs in /add-dir list, refresh plugin commands after reload /add-dir list looked only at session.summary, so pending startup additionalDirs were reported as absent until a session existed; fall back to appState when session-less. /plugins reload updated the app-global service but left the TUI's plugin slash-command map stale, so new or re-enabled commands kept parsing as prompts; rebuild it from the reloaded service (which also covers the app-global path before the first session exists). * test(tui): add pluginCommandMap to the MessageDriver interface * fix(tui): hydrate lazy defaults on sessionless reload, guard /plan re-entry /reload refreshed only the model/provider dictionaries while session-less, so defaults edited externally (or a newly added default model) left appState.model empty/stale and the first lazy-created session failed with LLM-not-set or used the old value. Reuse the startup default-hydration path for the no-session reload case. /plan on as the first command with defaultPlanMode=true re-entered plan mode after the engine had already applied the config default at create, throwing Already in plan mode. Skip the call when the session is already in the requested mode. * test(tui): widen the getConfig mock return type for the reload case * fix(tui): clear stale lazy defaults when the default model disappears hydrateLazyConfigDefaults only patched model when the reloaded config still had a default, so removing defaultModel from config.toml left appState.model stale and the first lazy-created session passed it explicitly instead of failing or following the engine's current defaults. Reset model and context cap when the default is gone. * fix(tui): reset a removed permission default, don't re-enter plan on --plan A removed defaultPermissionMode left appState carrying the old elevated mode, which createSessionFromCurrentState then passed explicitly to the first lazy-created session; reset to manual when no CLI permission flag is present. With both defaultPlanMode and --plan set, the create payload passed planMode: true even though the engine already entered plan mode from the config default, throwing Already in plan mode on the first prompt. Track the config default separately and suppress the --plan override when it is already active. * fix(tui): keep read-only and mode commands session-less Read-only views (/status, /usage, /mcp, bare /title) already degrade gracefully without a session, so forcing lazy creation made them fail with LLM-not-set or create an unused persisted session; they no longer trigger creation, and /title <name> lazy-creates only for the mutation. /permission, /auto and /yolo are pending-mode choices: with no session on v2 they now record the mode in appState (which the lazy create path passes to the engine) instead of failing or creating a session just to pick a mode. The runtime permission is applied once a session exists. * fix(tui): wait out lazy assembly in ensureSession, expose workspace MCP setSession assigned host.session mid-assembly, so a follow-up prompt or command in that window skipped the shared creation promise and dispatched against a session whose subscription and runtime sync had not finished. Check the in-flight promise before the assigned-session fast path. /mcp required a live session even though the v2 connection set is workspace-scoped, so it errored until an unrelated prompt created one. Route it through a workspace-level MCP view (harness passthrough over the handler's shared connection manager) before the first session exists. * fix(tui): await workspace MCP readiness in the session-less /mcp list * fix(tui): hydrate the model default thinking effort for the session-less picker * style(tui): trim verbose comments in the lazy-default fixes * fix(tui): hydrate lazy defaults after login, serialize /new with lazy creation * fix(tui): re-check the busy gate after lazy /add-dir session creation * fix(tui): let Shift-Tab lazy-create the session on a v2 session-less start * fix(tui): hydrate session-less defaults on model-less login, refresh workspace commands on reload * fix(tui): re-check the idle-only gate for /new after waiting out lazy creation * fix(tui): wait out lazy creation before accepting model/effort switches * fix(tui): wait out lazy creation before switching sessions from the picker * fix(node-sdk): read session-less skills from the workspace skill catalog * feat(tui): show a session-less notice on v2 startup
…I#2594) * docs: polish the 0.32.0 changelog and fill 0.32.0 doc gaps * docs(changelog): note SessionEnd archive and KIMI_TOKEN_COUNTING_STRATEGY for 0.32.0 * docs(changelog): move the 0.32.0 token_counting entry from Features to Polish
…ntries (MoonshotAI#2595) * docs(changelog): shorten the 0.32.0 loop_control and token_counting entries * docs(changelog): tighten the 0.32.0 loop_control and token_counting entries further * docs(changelog): reword 0.32.0 Polish entries from the user perspective
…tup (MoonshotAI#2586) * fix(agent-core-v2): make MCP initial connect non-blocking during startup * Delete .changeset/mcp-nonblocking-startup.md Signed-off-by: 7Sageer <sag77r@hotmail.com> * fix(agent-core-v2): wait for MCP readiness before first turn * fix(klient): wait for MCP startup before listing * fix(klient): keep MCP server listing non-blocking * test(acp-server): allow pending MCP snapshot --------- Signed-off-by: 7Sageer <sag77r@hotmail.com>
…shotAI#2407) * feat(agent-core-v2): add built-in capabilities (kimi-cu, kimi-webbridge) with REST routes Add a capability domain holding a closed registry of built-in product capabilities. Each entry owns layered readiness detection and idempotent install orchestration: binary runtimes from fixed official CDN URLs (KimiCU.app + launchd service + TCC permission state; the WebBridge daemon with start-if-down semantics for Kimi Work coexistence) plus agent wiring through the plugin service. The WebBridge wiring un-shadows stale user-source skill copies (user priority beats plugin priority). kap-server exposes the domain as GET /api/v1/capabilities, GET /api/v1/capabilities/{id}, and POST /api/v1/capabilities/{id}:install with client-polled progress and new wire codes 40418 / 40922 / 40923. The plugin marketplace gains an official kimi-webbridge entry (browser-control skills) packaged by the existing CDN build. * fix(agent-core-v2): rename the webbridge wiring plugin to kimi-webbridge-skill An official kimi-webbridge guide plugin (install/remove setup skills, v3.0.4) already exists at the marketplace path the capability installer pointed at — a different artifact owned by another release line. Give the browser-control usage-skill plugin its own id/path instead of colliding with (or overwriting) the guide plugin. The capability entry's detect/install now tracks kimi-webbridge-skill; a machine with only the guide plugin correctly reports the skill layer as missing. * feat(agent-core-v2): shelf installs auto-complete capability binary layers Two changes to make the plugin marketplace a first-class install path: - Marketplace gains kimi-cu (sourced from the CU team's CDN zip — no repackaging) and the kimi-webbridge usage-skill plugin now claims the kimi-webbridge id at v4.0.0, deliberately superseding the WebBridge guide plugin (v3.0.4, install/remove guide skills): guide users get a version upgrade onto the real usage skill. - The capability service subscribes to IPluginService.onDidReload: when a capability's wiring step flips to ok through ANY install path (shelf, TUI, CLI), it auto-completes the missing binary layers (KimiCU.app + service, or the WebBridge daemon). Triggers only on the false→true edge so completed installs with still-missing manual steps (TCC permissions) never retrigger heavy downloads on later reloads. * fix(plugins): keep kimi-webbridge plugin version aligned with the upstream skill The plugin version tracks the bundled official usage skill (1.11.3) so version drift against the WebBridge release line stays visible, instead of minting an independent 4.0.0. * fix(agent-core-v2): never report the webbridge installer-script version as the product version The on-disk ~/.kimi-webbridge/bin/kimi-webbridge.version file tracks the installer's own lineage (3.1.x, bumps on every install/upgrade run), not the product version (v1.11.3 — daemon, extension, and skills all share it). A downed daemon would have shown the misleading installer number; report no version instead (live /status remains the source of truth). * chore(plugins): list kimi-cu on the marketplace without a pinned version Marketplace versions are optional by schema: rows display the version detected from the installed plugin's manifest, and update prompts only fire on a valid semver latest > local comparison. A hand-maintained number would drift just like the guide plugin's did. The locally built kimi-webbridge entry keeps its manifest-stamped version (1.11.3). * fix(agent-core-v2): fire onDidReload on plugin mutations, not just explicit reload installPlugin / setPluginEnabled / removePlugin changed the catalog silently — consumers listening to onDidReload (session skill-catalog convergence, the capability shelf-install hook) only converged on an explicit reloadPlugins(). Fire the same summary-shaped event on every mutation (added:[id] / [] / removed:[id]) so every install path converges. This also unbreaks the shelf-install hook on real hosts: its unit tests passed against a fake emitter that fired on installs, which the real service never did. * feat(kap-server): add plugin management and marketplace REST routes Expose the App-scope plugin service over the wire so non-CLI hosts (desktop, web) can manage plugins end to end: - GET /api/v1/plugins/marketplace — catalog (pluginMarketplaceUrl server option / KIMI_CODE_PLUGIN_MARKETPLACE_URL env / production default) merged on demand with live install state; updateAvailable only on strict semver catalog > installed (no semver dependency) - GET /api/v1/plugins, POST /api/v1/plugins {source} - POST /api/v1/plugins/{id}:{enable,disable,remove} - New wire code 40419 plugin.not_found Mutations flow through IPluginService, so they serialize with other install paths and fire onDidReload (session skill catalogs and the capability shelf-install hook converge). * feat(agent-core-v2): surface a machine-key note from capability installs CapabilityEntry.install now resolves an optional note exposed through CapabilityInstallProgress.note (wire-visible). The webbridge entry returns 'user-skill-migrated' when it replaces a pre-existing user-source skill (from the official installer) with the plugin-managed copy — clients can localize the migration instead of the skill silently disappearing from the user's directory. * feat(tui): let the real WebBridge marketplace entry win over the pinned promo The hardcoded Web Bridge row was built when WebBridge had no plugin package — it pinned above the Official tab and shadowed any catalog entry with the same id (open-in-browser only). Now that the marketplace carries the real kimi-webbridge plugin, flip the precedence: the catalog entry renders and installs normally, and the pinned promo becomes a loading/error/legacy-catalog fallback only. Footer counts keep their old semantics (catalog-only; the promo row is never counted). * fix(tui): dim the installed state so it stops reading as the install action Both badges shared a near-identical green-ish treatment in the same column, making a quiet fact look like a clickable action. States now recede (installed → textDim) while actions stay loud (install → primary, update → warning). * feat(agent-core-v2): converge plugin state across processes sharing a home Multiple hosts share one KIMI_CODE_HOME (CLI, desktop, other agents), but each PluginService kept a private in-memory snapshot: a plugin installed or removed in one process stayed invisible to every other live process until its next restart — new sessions there kept offering stale plugin skills/MCP, and the capability shelf hook never saw peer installs. Watch <home>/plugins for installed.json changes and reloadPlugins (debounced, echo-suppressed around our own mutations) so all consumers converge in well under a second: session skill catalogs, plugin MCP mounts, and the capability shelf-install hook alike. * fix(agent-core-v2): un-shadow webbridge user skills in BOTH user dirs kimi-code resolves user-scope skills from two roots (~/.kimi-code/skills and ~/.agents/skills), both at priority 20 — a stale copy in either shadows the plugin-managed wiring (priority 5), and also keeps the capability working after the plugin is removed, which reads as 'uninstall did nothing'. Migrate copies in both dirs during install; other runtimes' dirs (~/.claude, ~/.codex) remain untouched. * feat(tui): show live runtime-setup progress for capability installs Installing a capability plugin (kimi-cu, kimi-webbridge) from the /plugins shelf kicked off a silent background binary install — the row flipped to installed while megabytes of runtime downloaded invisibly. Route capability entries through the capability surface instead: the panel's inline installing line now mirrors live progress (step + percent) until the install settles, and the transcript reports ready / failure-with-retry / still-running accordingly. Capability removal prints an explicit note that runtime binaries are deliberately left untouched (the capability keeps working), since that read as 'uninstall did nothing'. Plumbs the capability service through klient's global facade ('capabilityService' decorator resolves in-process) and the node-sdk v2 client; Session exposes it with a structural feature-detect so v1 engines fail clearly. * docs(plugins): keep the kimi-cu marketplace blurb accurate for every client Only the capability-aware clients auto-install the KimiCU.app runtime; older builds still get wiring-only (the wrapper's error message then points at the official setup script). Don't overpromise in the catalog text every version reads. * feat(agent-core-v2): install capability wiring from client-bundled plugin copies The kimi-cu / kimi-webbridge wiring plugins ship inside the client release instead of the marketplace catalog, binding their visibility to the client version. Capability installs now resolve the bundled copy (env override, then npm-layout and source-checkout probes from the module) and install it as a local path, replacing the two CDN zip URLs. A missing bundle fails the wiring step with a clear reinstall-or-upgrade message. * build(cli): bundle the capability wiring plugins into client releases Vendor the official kimi-cu plugin (v0.5.4, from the CU team's plugin zip) next to kimi-webbridge under plugins/official, copy both into apps/kimi-code/bundled-plugins at build time, and ship them in the npm package (files) and the native SEA blob (a new bundled-plugins asset set extracted into the native cache at startup, published to the engine via KIMI_CODE_BUNDLED_PLUGINS_DIR). Desktop points the same variable at its extraResources copy. The .gitignore build-output entries are anchored so sources under src/native and test/native stop being silently ignored. * revert(plugins): remove the kimi-cu and kimi-webbridge marketplace entries Both capabilities now distribute with the client (bundled wiring), so the catalog drops back to kimi-datasource / superpowers / vercel-plugin. Older clients never see the entries; current clients install from the Built-in section. This also reverts the marketplace blurb commit 0635e99. * feat(tui): add a Built-in capabilities section to the plugins panel The Official tab now opens with a Built-in section fed by the engine's capability registry (kimi-cu / kimi-webbridge): per-row install state (install / finish setup / ready), Enter runs the full capability install with live progress, and unsupported rows hide (kimi-cu off macOS). The WebBridge promo fallback only remains for v1 engines — on v2 the real built-in entry wins. Rows double as the reinstall path: a client upgrade ships newer wiring, and installing again upserts from the new bundle. * docs(plugins): document the Built-in section and refresh the capability changeset * build(nix): stage bundled capability plugins into the SEA build The native SEA blob now embeds the bundled-plugins asset set, so the nix derivation needs the plugins tree in its src fileset and the staging step alongside copy-web-assets before build:native:sea. * revert: drop the client-bundled wiring distribution Built-in visibility is simpler to get by injecting the two capability entries into the marketplace catalog at load time; the wiring plugins themselves keep installing from their fixed official CDN zips. Removes the vendored kimi-cu plugin, the bundled-plugins npm/SEA packaging and flake staging, the engine bundle resolver, and the plugins panel's Built-in section. Keeps the /agents/ and /native/ gitignore anchors so sources under src/native and test/native are not silently ignored. * feat(cli): inject the built-in capability entries into the marketplace catalog The kimi-cu / kimi-webbridge entries are appended by the client at catalog load time instead of being served by the remote marketplace.json, binding their visibility to the client version (older clients never see them). No version is pinned — reinstalling upserts the wiring — and ids the catalog already carries always win. In a source checkout the webbridge entry installs the repo's own plugin copy; packaged builds use the official CDN zip. This reverts the docs paragraph about the Built-in section, which the simpler approach makes unnecessary. * test(tui): select the catalog's own first row in marketplace install tests The client-injected capability entries suppress the WebBridge promo and append after the catalog rows, so Kimi Datasource now leads the Official tab — the extra down-key landed on kimi-cu instead. * feat(cli): surface the built-in capabilities as client-injected marketplace entries The kimi-cu / kimi-webbridge entries are injected into the marketplace catalog by the client (v2 engine, default catalog only) instead of being served remotely, binding their visibility to the client version; injected rows mask same-id catalog rows, so what these ids mean stays decided by the client release — a future official listing only reaches older clients, whose fix is to upgrade. The /plugins panel shows capability readiness on the rows (setup incomplete / installing…), platform-gates kimi-cu to macOS, and Enter finishes the runtime setup with live progress; v1 keeps the plain plugin install path and the WebBridge promo fallback. Capability and plugin calls move from the ad-hoc REST routes onto the typed klient contract (capabilityService next to pluginService), so the public REST surface returns to its pre-feature shape. Detection is presence-only — version pins removed: the current version is always read live (Info.plist, daemon status, install records), installs are detect-first and idempotent so an interrupted setup can be retried, and reinstalling pulls the latest managed artifacts (the passive upgrade path). * ci: retrigger checks * fix(cli): recognize Computer Use CDN plugins as official * fix(cli): keep built-in entries on catalog outage and isolate detector failures Two review follow-ups: the client-injected entries no longer disappear when the marketplace catalog is unreachable (they are not served by it), and a single capability's failing detect probe degrades to a failed step on that entry instead of rejecting the whole listCapabilities call. * refactor(cli): simplify built-in capability integration * refactor(cli): source built-in catalog rows from the engine and tighten detect probes The injected marketplace entries are now derived from the engine's capability registry (listCapabilities) instead of hardcoded client-side copies — the util only owns the mask/append mechanics, and capability ids are no longer pinned in the CLI (the remove note resolves them through the registry too). kimi-cu's detect-path probes (service-status, xpc-ping) get a 3s timeout — they answer in milliseconds when healthy but run on every status listing, so a wedged binary must degrade quickly instead of stalling the panel. Document the Official tab's built-in capability rows in the plugins guide. * fix(cli): answer capability id membership without running detectors listCapabilities() runs every entry's detect probes (seconds on a wedged binary), so using it to decide whether to print the post-remove hint made every plugin removal pay a full detection round. The id set is part of the client/engine contract (mirrored in the klient schema), not product data that drifts — restore the closed-set check. The injected catalog rows keep flowing from the registry. * fix(agent-core-v2): make capability setup recover from disabled, partial, and wedged states Three review follow-ups on the install path: setup now re-enables the wiring plugin when a previous disable survived installPlugin's upsert (detection requires enabled, so it would otherwise strand the capability at partial); the webbridge daemon-binary step verifies the executable bit on POSIX, so an install interrupted between rename and chmod re-downloads instead of failing start with EACCES; and kimi-cu's detect degrades wedged CLI probes (service-status, xpc-ping) to failed steps instead of throwing, keeping the detect-first install able to repair the remaining layers — with the probe timeout injectable for tests. * fix(agent-core-v2): abort capability downloads whose byte stream stalls downloadToFile had no inactivity deadline: a CDN connection that stops producing bytes hung the background install forever, wedging the capability in a permanent installing state (retries rejected as in-progress) until the process restarted. An idle watchdog now fails the download after 30s without a chunk; slow but flowing downloads are unaffected. * fix(tui): stop offering capability setup on unsupported platforms An installed wiring plugin whose capability is unsupported on this OS/arch (kimi-cu off macOS, webbridge on an unknown arch) was treated like a partial setup: the Installed tab showed setup incomplete and Enter routed to installCapability, which the service always rejects. Setup actions are now gated to actionable states (not_installed / partial); unsupported renders as a dim fact and Enter opens details. * fix(agent-core-v2): cover the two remaining install wedge modes Review follow-ups: the KimiCU app step now requires an executable binary, so a ditto interrupted mid-copy reads as missing and the next setup re-copies instead of failing EACCES forever; and downloadToFile's idle budget now also covers the response-header phase via an AbortSignal on the fetch itself, so a connection that never completes headers fails the install (clearing the running state) instead of hanging it. * fix(tui): render capability rows independently of the catalog fetch While the marketplace catalog was loading or unreachable, the Official tab showed only the pinned WebBridge promo — built-in runtime setup was blocked by an unrelated remote fetch, and Enter opened the browser instead of installing. Locally-known capability rows (from the engine registry) now render and install in every catalog state; the promo remains only as the v1 fallback. * fix(agent-core-v2): keep KimiCU cleanup timeouts best-effort stopOldProcesses is documented as || true, but runCommand propagates timeouts: a wedged old binary made kimi-cu uninstall exceed the command timeout and the reinstall died before ditto could replace the app. Cleanup commands now swallow failures (the timeout already attempts a kill) so the replacement always proceeds; the command timeout is injectable for tests alongside the probe timeout. * fix(cli): inject built-in entries only for the default marketplace catalog Injection is part of the default catalog experience: any explicit replacement (slash-command source or KIMI_CODE_PLUGIN_MARKETPLACE_URL) now opts out wholesale — its same-id rows are never masked by the built-ins, and an unreachable custom catalog surfaces its own failure instead of being silently replaced by a built-in-only tab. * refactor: align capability row rendering on the source marker and drop conditional spreads Marketplace-row capability enrichment (status, badges, issue details, platform filtering) now keys on the capability:<id> source marker — the same condition Enter uses to route installs — so a custom catalog row that merely reuses a built-in id renders and installs as a plain plugin. Also replaces the conditional-spread optional fields with direct undefined-valued assignments per the repo coding rules. * refactor(agent-core-v2): move capability comments to the file headers The domain's comment convention allows only the top-of-file block: responsibility and scope context for the recent hardening (detect-first idempotent install, executability gates, probe-failure degradation, best-effort cleanup, download watchdog, per-entry detection isolation) now lives in the module headers, and inline narration beside statements and members is removed. * fix(tui): follow an in-progress capability install instead of restarting it Opening /plugins while a capability setup is already running showed the installing… row, but Enter called installCapability again and the service's duplicate-start rejection (40922) surfaced as a fake failure. The panel now checks the live status first and, when an install is already running, skips the start call and just polls for the existing progress. * fix: align two more replacement paths with their contracts The EXDEV daemon-binary fallback now stages on the target filesystem and atomically renames over the destination instead of opening a possibly-running binary for write (ETXTBSY on Linux). And the panel's fallback capability rows (catalog loading/error) now follow the same default-catalog condition as the loader injection, so an explicitly overridden marketplace fully replaces the Official tab. * fix(tui): make the built-in row marker unforgeable The capability:<id> source string was the trust signal for routing rows into capability installs, but any catalog can write that string — a custom marketplace could smuggle a row past the third-party trust path into an official runtime install. Injected rows now carry an internal builtIn flag that the field-by-field catalog parser never produces; rendering and install routing key on the flag, and the source string is purely diagnostic. * fix(agent-core-v2): include MCP server enablement in capability readiness A user who disabled the kimi-cu stdio MCP server (/plugins mcp disable) got a ready capability with no Computer Use tools in new sessions: the plugin step only checked the plugin toggle, and installPlugin's upsert preserves per-server state. Readiness now requires every declared MCP server enabled (reporting e.g. mcp 0/1 enabled), and setup re-enables disabled servers alongside the plugin toggle. * fix(agent-core-v2): shell-quote ditto paths in the elevated KimiCU copy The elevated fallback escaped paths only for the AppleScript string delimiters, not for the /bin/sh command line inside do shell script: a TMPDIR with spaces broke the install, and shell metacharacters in the temp path could inject commands into an administrator-privileged script. Paths are now POSIX single-quoted first, then the assembled command is AppleScript-escaped. * fix(agent-core-v2): never break a working KimiCU on a failed update The reinstall stopped and uninstalled the old service before the downloaded archive was unpacked: a corrupt or captive-portal zip then tore down a previously ready setup. The archive is now staged and unpacked first, and the app step additionally requires the bundle's Info.plist, so a partially copied bundle reads as missing and gets re-copied instead of failing registration against a corrupt bundle. * fix(agent-core-v2): limit the fetch deadline to the header phase The 30s AbortSignal stayed attached for the whole request, so a slow-but-healthy download of a large archive was aborted at 30s total even while chunks kept arriving — exactly what the per-chunk idle watchdog was meant to allow. The header phase now uses an AbortController cleared once headers arrive; the body remains governed by the inactivity watchdog alone. * test(tui): provide the harness plugin facade in the capability command fakes The lazy-session refactor routes session-less plugin calls through host.harness; the fake host now mirrors that shape.
…ool output (MoonshotAI#2596) MCP tool results were narrowed to {content, isError}, dropping the spec-defined structuredContent field and _meta metadata. Servers that return structured contracts in these fields (validated against outputSchema, or namespaced metadata such as browser-handoff payloads) were invisible to the agent. Surface them as a serialized <mcp-structured-result> block appended to the tool output, still subject to the existing text budget. Co-authored-by: zouying <zouying@moonshot.cn>
…nshotAI#2564) * feat(agent-core-v2): announce date changes via a system reminder * refactor(agent-core-v2): hide profile rendering behind service * refactor(agent-core-v2): normalize profile prompt rendering at registration * chore: retrigger ci
… update (MoonshotAI#2609) * feat(mcp): carry an absolute expiresAt on the OAuth authorization-url update The authenticate flow waits for the OAuth callback with a fixed budget, but the authorization-url tool update surfaced to embedding hosts did not say when that window ends — hosts had to hardcode a mirror of the 15-minute constant to render countdowns. Include the absolute deadline (now + effective wait timeout) in the update payload for v1 and v2. Resolve MoonshotAI#2607 * fix(protocol,kap-server): accept expiresAt in the OAuth authorization-url update schemas The zod validators mirrored the pre-expiresAt payload shape and would strip the new field at the kap-server boundary. --------- Co-authored-by: zouying <zouying@moonshot.cn>
…tAI#2608) v1 lets a remote MCP server combine static headers with OAuth via the auth:'oauth' config marker; v2 neither parsed the field nor honored it, so any configured headers were treated as static credentials — first authorization never started and an expired refresh token left the entry permanently failed. Add the field to the v2 remote config schemas and mirror the v1 guard in shouldMarkNeedsAuth. Resolve MoonshotAI#2605 Co-authored-by: zouying <zouying@moonshot.cn>
…MoonshotAI#2600) * fix(mcp): drop protocol-reserved _meta keys from model-visible output Follow-up to MoonshotAI#2596. The MCP spec reserves _meta key prefixes whose labels include "modelcontextprotocol" or "mcp" for protocol use; those entries carry host/protocol plumbing rather than model-facing data, so filter them out before serializing the <mcp-structured-result> block. Unprefixed and vendor-prefixed keys still pass through — their semantics belong to the server. Also moves the v2 implementation commentary into the module header per the agent-core-v2 comment convention. * fix(mcp): reserve _meta prefixes only when a label follows mcp/modelcontextprotocol Per the spec's key-name rules a prefix is reserved when a modelcontextprotocol or mcp label is followed by at least one more label; a trailing reserved word (com.example.mcp/) is a legitimate vendor namespace and now passes through. --------- Co-authored-by: zouying <zouying@moonshot.cn>
…oonshotAI#2604) * perf(minidb): skip idle everysec fsyncs and add lifecycle stats - everysec WAL now fsyncs on the timer only while dirty (tracked by a write/sync generation watermark); close() keeps its unconditional final sync, and background sync failures surface via walFsyncErrors plus a sticky lastWalFsyncError instead of being silently swallowed - add WAL queue/group-commit counters (walQueuedBytes, walMaxQueuedBytes, walGroupCommits, walGroupCommitFrames) and lifecycle phase stats: recovery bytes/frames/duration, index/text rebuild durations, compaction total/snapshot/rotation/postings durations, rotation pause, and query candidates/decoded/sorted rows; add a syncIntervalMs open option threaded through compaction WAL rotation - rewrite the bench on fixed-seed synthetic data with a stable machine-readable JSON report (cold open 10k/50k/100k, word/ngram search, idle-fsync acceptance, 100k compaction, event-loop delay, peak heap/RSS per scenario) and pin the schema in test/bench-json.test.ts; add app-side baselines with loose complexity budgets in sessionIndex and searchService tests - fix ClusterDb lock-pool closeAll() leaking in-flight shard opens and drain the query store's async close on server shutdown, eliminating the ENOTEMPTY directory-teardown race * perf(minidb): bound startup rebuild and steady-state hot paths - rebuild all derived indexes in one shared store walk: a single decode per record fans out to staged builders, dt rebuild reads record metadata only, and index-less opens no longer decode at all - rank full-text results with a bounded min-heap plus a stable key tie-break instead of sorting every candidate - remove/overwrite text docs via a docID -> delta-terms reverse map instead of scanning the whole delta vocabulary - validate unique batches incrementally against touched postings instead of copying the full per-index owner map - reap due TTL entries from the expiry heap on the write path instead of a full-store sweep per write * feat(session-index): add minidb read model with keyset pagination - add ISessionIndex read-model lifecycle (prepare/status, ready/degraded states) behind the persistence_minidb_readmodel experimental flag - add ISessionIndexMirror write side recording fresh summaries into a bounded, coalescing queue after the authoritative document is durable - replace the offset cursor with before/after keyset pagination; rename list/countActive to listRecent/count - extend IQueryStore with ordered columns and pageByColumn, plus getMany/listKeys/dropCollection - wire the read model through kap-server routes and start, and update the klient sessions contract - index every session for global search instead of the 500 most recent * feat(kap-server): bound search sync lifecycle, pagination, and query budgets - split search requests from sync work: searchIndex() no longer awaits runSync/reopen/reindex; a single-flight sync coordinator with debounce and backpressure runs in the background, stale generations keep serving with explicit stale/degraded state, and refresh/sync/reindex failures surface via lastRefreshError instead of being swallowed - scope file-meta keys by session id (\0meta\file\<sessionId>\<hash>) with lazy + one-shot background migration from the legacy hash-only keys, so one session sync only touches its own meta rows - make authoritative scans incremental (mtime/ino/size rescan conditions, unchanged files no longer rewrite meta) and read wire deltas in 1 MiB chunks instead of whole-file buffer + split - replace offset pagination with versioned v2 keyset page tokens (fingerprint + index generation + sort boundary); generation changes fail old tokens with invalid_page_token, legacy v1 offset tokens are served once and upgraded, and pages collect via bounded top-K instead of full sort + offset skip - add query budgets enforced at the postings/score stage: max query terms, literal length cap, postings visit budget (minidb searchBounded/maxVisits with prefix decoding that never fabricates hits and skips the postings LRU), candidate caps, deadline and text budget; truncation is reported via incomplete reasons candidate_cap/postings_budget/deadline - reopen read-only dbs by opening the next handle before closing the previous one so a failed refresh keeps the old generation serving; failed opens now self-heal through search traffic 100k-message bench: first page p95 < 300ms and page-100 cost on par with page 1; event-loop delay during queries stays sub-millisecond. * fix(minidb): poison, roll back, and recover the WAL on write failures - give WAL writes a commit point: a failed flushBatch poisons the WAL (WAL_POISONED, tracked separately as walWriteErrors vs walFsyncErrors), rejects queued frames in reverse enqueue order, and stops scheduling further batches; everysec background sync failures stay non-rejecting per stage-1 semantics - recover in place to a known-safe point: a serialized recovery chain truncates the WAL back to the first un-acked frame, rebuilds size/nextOffset, and clears the poison; writes queue behind the recovery gate (zero-cost when idle), a failed truncate flips the instance into an explicit writeDisabled state, and a stale truncate offset (WAL file replaced by a rotation) skips the truncate - roll failed flush groups back as a unit: frames are stamped with their batchId, MiniDb keeps per-group earliest pre-state, and the first rejection restores every key of the group (rejected writes no longer reappear after reopen, and in-memory state matches reopen for any failure interleaving); the per-op seq guard remains for cross-group and rotation-retry races - wrap applyOp and the following in-memory mutations so a contract violation poisons the WAL and rolls the group back instead of escaping as a half-commit; frames never enqueued (seal race) roll back per-op without poisoning - tag errors past the commit point with ambiguous: true so callers can distinguish "definitely not applied" from "maybe applied but revoked" - close() waits for the recovery chain to go idle and backup() fences behind in-flight recovery before copying files Controlled A/B bench (22 alternating iterations, 100k concurrent sets): write-path throughput regression is within the 2% budget. * fix(minidb): turn the file lock into an instance-owned serialized lease - distinguish lock ownership by instance instead of pid: every acquire mints a pid:uuid token carried by lock/bid/watch files, inspect().mine compares tokens, liveness still follows pid, tokenless legacy files keep the old stale-takeover path, and hasLiveForeignWatch excludes self by token so same-process contenders see each other (closing the double-win takeover and the cross-instance release); a live same-pid lock is still respected, and re-acquiring a held lock is idempotent - serialize acquire/renew/release through a per-instance promise-chain mutex: renew re-checks held inside the chain and release waits for an in-flight renew, eliminating the renew/rename-after-unlink ghost lock - make MiniDb.close() a state machine (open/closing/closed) with a shared closePromise: cleanup runs per-resource try/catch in dependency order (text indexes, store, valueReader, WAL, lock), aggregates every cleanup error into an AggregateError, stays in 'closing' on failure so a retry finishes the cleanup, and no longer leaks the lock when the WAL close fails; a rejected in-flight compaction no longer escapes the cleanup pass * fix(minidb): keep readers on one consistent file generation - add an internal persistent-files module as the single source of truth for the persisted file set (snapshot, WAL, sidecars, postings pattern, fingerprint subset); lock-pool fingerprints, persistentFiles, open stale-tmp cleanup, and backup/restore filtering all derive from it, and fingerprints upgrade to dev:ino:size:mtimeMs so compound sidecar changes can no longer hide from cluster readers - pair snapshot and WAL generations during recovery (transitional stat-pairing until stage-5 manifests): each pass anchors the fds it scans, re-stats afterwards, tolerates append-only WAL growth, retries bounded times on generation churn with a clean store reset, and throws RECOVERY_GENERATION_CHURN when churn exceeds the budget; the disk-mode ValueReader attach re-validates inodes so stale offsets never read a replaced file - make the rotation directory fsyncs strict: failures abort the rotation through the existing rollback path instead of being swallowed, while platforms without directory fsync degrade once with a warn and stats.dirFsyncUnsupported * fix(minidb): serialize index-definition sidecar mutations, persist before publish - extract the promise-chain mutex into a shared createSerializer() and give each sidecar family (secondary/compound/text) its own chain: create/drop run uninterruptibly (memory change + rebuild + persist), different families stay independent, and the data write path never shares these chains - reverse the publication order to staged -> persist -> publish: a create stages the definition, rebuilds via the staged builder, persists the sidecar including the new definition, then publishes atomically; any failure discards the staged state leaving live and sidecar untouched (no phantom indexes, retry-safe); a drop persists the sidecar without the definition before removing it live; text index create/drop adopt the same pattern, replacing the hand-rolled unwind, and a dropping marker keeps compaction postings rebuilds out of the persist window - feed staged indexes from the incremental write path (add/remove/ checkUnique/checkUniqueBatch visit live+staged) so writes landing in the persist window are not lost at publish; queries still see live only - harden writeFileAtomic: instance-unique tmp names (.tmp-pid-seq), a strict fsyncDir after rename so a successful persist is crash durable, and whitelist-based stale-tmp cleanup that never touches lock tmp files * fix(minidb): validate writes before any side effect, canonicalize values once - canonical value at the write boundary: the json codec re-parses the encoded bytes once and every downstream consumer (unique checks, secondary/compound/text indexes, dt extraction) sees exactly the persisted representation, so getter/toJSON/Proxy documents can no longer diverge between the index view and the storage view - reorder the set/batch pipeline so every fallible check happens before any visible side effect: prepare (key/ttl checks, encoding, canonical decode, index field extraction, tokenization) -> unique checks -> ensureMemoryFor eviction -> commit; a constraint failure now leaves the database untouched (no more evicted victims on rejected inserts), and applyOp is structurally pure against pre-validated data - tokenize at the prepare boundary: TextIndex gains prepareAdd/ addPrepared and the buildQueue carries validated key+tokens mutations instead of raw docs, so a throwing custom tokenizer can no longer poison the live view or the queue, and custom-tokenizer output is rejected per token over 0xffff bytes before it can permanently break postings rebuilds; prepared tokens are keyed by index instance so a same-name drop+create mid-write re-tokenizes instead of crossing tokenizers - strict batch structure validation: scanBatchOpRefs/decodeBatchOps reject unknown op types, out-of-bounds lengths, and trailing bytes (offset must equal body length), so a valid-CRC but malformed batch is skipped as a unit and counted via RecoveryInfo.corruptBatches instead of being partially applied Bench vs the stage-1 baseline: json write throughput regression is within the 5% budget (median ~2-4% depending on the measurement). * feat(minidb): add OpTracker drain primitive and atomic backup, harden tests - introduce the internal OpTracker (close gate + in-flight counter with enter/leave/close/whenIdle and reference-counted pause/resume) and drive every shutdown/drain path from it: WAL background syncs are tracked so close() waits out an in-flight sync before closing the fd, cluster lock-pool closeAll() closes the gates and drains busy callbacks before closing handles, and MiniDb writes pass a write gate - make backup() atomic with a defined linearization point: pause the write gate, drain in-flight writes (every acknowledged write is now included), copy to a sibling temp dir with per-file fsyncs, write the manifest last as the commit marker, and rename into place; failures clean up and leave no partial backup, and concurrent writes are rejected with BACKUP_IN_PROGRESS - reap emptied compound-index groups on remove (the groups map no longer grows monotonically), move the open-time mkdir behind the readOnly check so a read-only open of a missing directory fails with ENOENT instead of creating it, and never run a destructive rebuild for a read-only open failure (explicit or onLockFail fallback) - consolidate every review fault-injection repro into the formal suite behind deterministic barrier helpers (programmable writev/sync/ rename/tokenize hooks) and convert the six timing-based tests to barrier/tick-driven assertions; the .tmp repro scripts are removed The converted timing tests and the full suite pass 50 repeat runs (including under CPU load injection) with zero flakes. * feat(minidb): persist derived indexes as atomic generations, open from WAL delta - checkpoint the store, dt/secondary/compound indexes, and text dictionary/postings/docs into immutable generations under generations/g-NNNNNN published atomically (tmp build, per-file checksums and fsyncs, dir rename, CURRENT swap, strict dir fsyncs); the manifest records the format version, WAL/snapshot checkpoint anchors, per-index definition hashes, and codec/value-mode compatibility - open now loads the published generation and replays only the WAL delta after its checkpoint: no full value decode, corpus tokenization, or postings rewrite on a normal reopen (warm opens are 3.5-13.8x faster at 100k/1M records); a definition change rebuilds only the affected index, and corrupt generation files fall back to the previous generation or the legacy full recovery without ever touching the authoritative snapshot/WAL - build generations transactionally with compaction (rotation plus derived state publish as one unit, replacing the synchronous rebuildTextPostings tail), capture concurrent writes through a sealed op queue with byte/op caps, hard-link clean postings and the snapshot into the new generation, and repoint every live text base into the CURRENT generation after publish - cluster/read-only refresh watches CURRENT and the WAL watermark: pure generation publishes keep readers on incremental catch-up while rotations reopen onto the new generation; writers building the next generation never disturb readers of the current one - legacy databases open through the old path unchanged and gain their first generation in the background; OpenOptions.indexGenerations: false fully restores the pre-generation behavior * feat(minidb): workerize text-index builds and split MiniDb into facets - split the monolithic src/index.ts into facet modules (mini-db, types, value-codec, memory-guard, backup, query-engine, text-registry, wal-group, generation-builder/loader, write-path, read-path, index-admin, lifecycle, stats) and move text-index.ts to text-index/ - run corpus-scale text-index builds off the main thread via the bounded worker engine (src/worker/), exported through the new worker-runtime subpath, with inline fallback for small corpora and rollback switches - defer the open-time fallback text rebuild into a maintenance task; searches on a not-yet-committed base raise TextIndexBuildingError - add the unified maintenance scheduler, bounded async read surface, and a maintenance bench - kap-server search: switch to searchBoundedAsync and serve the building page while the index base rebuilds after fallback recovery - kimi-code: install the SEA-bundled minidb text-build worker at startup, bundle it via the native asset scripts, and add the startup-trace util plus the KIMI_TUI_INPUT_LATENCY debug probe * fix(minidb): treat win32 EPERM as unsupported directory fsync - extract isUnsupportedDirectoryFsyncError and cover win32 EPERM - drop the one-shot console.warn; stats.dirFsyncUnsupported carries the degraded state * fix(kap-server): harden search-index dispose and drain lifecycle - dispose() now closes an OpTracker gate and drains in-flight sync/refresh passes before closing the db, so no background write can hit a closed handle; the deleteSessionDocs loop and trailing stats write skip once the gate closes (review MoonshotAI#20) - drainGlobalSearchDisposals loops to a fixpoint so disposals registered while a drain is in flight are also awaited (review MoonshotAI#21) - pin the post-open failure semantics with a regression test: a failed text-index setup closes the handle and the next open reacquires the writer lock instead of self-locking read-only (review MoonshotAI#19) - export OpTracker from the minidb root for the search service's drain * chore: fix oxlint type-aware lint errors
* refactor(agent-core-v2): simplify context tags and shared copy
Rename the context-injection tags to `<skill-loaded>` and
`<plugin-instructions>`, drop the product prefix from the CronCreate tool
description and the default agent description, and point the MCP OAuth
callback page back to "your terminal" instead of naming one client.
The callback page is shared by the ACP host, the web UI, and embedding
hosts, so naming a single client was inaccurate there. The tags and the
two descriptions read exactly the same without the prefix. Verified no
runtime consumer matches the old tag names; the updated snapshots cover
the tool descriptions that changed.
* feat(agent-core-v2): add a switch for the product-documentation skills
Five builtin skills document this CLI itself — `update-config`,
`custom-theme`, `mcp-config`, `check-kimi-code-docs`, and
`import-from-cc-codex`. Their names and descriptions sit in the system
prompt on every turn, which is dead weight for runs that will never
reconfigure the CLI.
Add a top-level `builtin_product_skills` field (also settable through
`KIMI_CODE_BUILTIN_PRODUCT_SKILLS`) to drop them. On by default, so
nothing changes unless it is set; the trade when off is that the model
loses the guided flows for those tasks.
Filtering happens where the catalog is assembled — a later filter would
leave the skills advertised to the model. The whole section is one
scalar, so it exercises the section-level env binding branch and needs
its own strip: `stripEnvBoundFields` only walks object fields, so an env
override would otherwise be written back into `config.toml`.
* feat(agent-core-v2): add custom agent identity
Add an `[identity]` config section (`name`, optional `slug`, both also
settable through `KIMI_CODE_IDENTITY_NAME` / `KIMI_CODE_IDENTITY_SLUG`)
that sets the identity the agent presents: the name it calls itself in
the system prompt, the `User-Agent` product token sent to third-party
providers, and the client name announced to MCP servers. Leaving it
unset changes nothing.
Until now every one of these was fixed, which left no way to run the
agent as part of another product — an internal deployment, a fork with
its own branding, an embedding host.
The identity resolves inside the engine rather than being seeded by each
host, so it applies to every launch surface — including headless runs,
which today seed no display name at all and fall through to the built-in
default.
Two deliberate asymmetries:
- The display name is a filling value with a fallback chain (config >
host-declared > the consumer's own default); the slug is a rewriting
value with two states only, so with no identity configured the
rewriting paths are equivalent to not existing.
- The rewrite happens in the outbound header assembly, the one layer
that knows which vendor it is building for. Vendors declaring
`hostHeaders: 'full'` keep the host's own product token, which that
header set is built around and which backends key on; the configured
identity applies to the third-party path.
Resolution is lazy throughout: config loads asynchronously, and a
constructor snapshot would freeze the pre-load value under some startup
orderings.
Two input edges the resolver has to absorb, since both would otherwise
reach the User-Agent builder and either break it or quietly rewrite the
header: blank and whitespace-only values read as unset in the file just
as they already did in the env, so a stray `name = ""` cannot claim an
identity; and a name that folds away to nothing under slug
normalization (a CJK-only name, say) falls back to a neutral token
rather than producing a blank product, which the builder rejects.
* fix(agent-core-v2): keep the file value when a scalar env binding fails to parse
`config.ts` documents that an env value failing its binding's `parse` is
ignored, and `applyEnvBindings` honors that for object fields by
assigning only when the resolved value is defined. `applySectionEnv`
returned the parse result straight through for whole-section scalar
bindings, so a blank or mistyped variable resolved to `undefined` and
cleared the configured file value instead of being ignored.
Nothing hit this before: every existing section either binds object
fields or is env-only. `builtin_product_skills` is the first
whole-section scalar binding, where exporting an empty or misspelled
`KIMI_CODE_BUILTIN_PRODUCT_SKILLS` would silently undo a configured
`false`.
* feat(agent-core-v2): extend the custom identity to discovery and global MCP
Two outbound paths still announced the built-in product name under a
configured identity:
- `DiscoveryService` read the host User-Agent straight from bootstrap
args when refreshing provider models, so custom registries — which are
third-party endpoints — saw the original token while chat requests to
the same class of endpoint saw the configured one.
- `SDKRpcClientV2` builds its own global `McpOAuthService` plus a
throwaway `McpConnectionManager` for server testing, neither of which
goes through the workspace-owned manager that carries the resolver.
Both now resolve the identity from the App scope.
* refactor(agent-core-v2): neutralize remaining copy and align comments
The synthetic MCP authentication tool description is injected into the
model context and still named the product; it and the OAuth callback
pages now use client-neutral wording. "Return to your terminal" was no
improvement over naming a client — both assume what the host is, and
that page serves the ACP host, the web UI and embedding hosts alike.
Comments introduced by the identity work move into their module headers,
per the domain convention. Interface field docs stay: the rule names
functions, methods and statements, and field-level docs are established
across the codebase.
The new tests gain scenario headers and dispose the scoped hosts they
create, and the `[identity]` docs state which engine reads the section.
* fix(agent-core-v2): read the product-skill switch after config is ready
`BuiltinSkillSource` is the lowest-priority skill source, so the workspace
catalog loads it first — before `IConfigService` has finished loading — and
keeps the contribution it returns for the life of the handler, with no
reload path and no change event. Reading `builtin_product_skills` eagerly
therefore stranded the startup configuration: an explicit `false` could be
ignored for the whole process. `UserFileSkillSource` already awaits config
readiness for exactly this ordering; this source now does the same.
Also record the identity collaborator in the two module headers that gained
the dependency without documenting it, and scope the
`builtin_product_skills` docs to the engine that reads it, matching the
note the identity section already carries.
* fix(agent-core-v2): apply the product-skill switch to session-less listings
`builtin_product_skills = false` only reached the scoped skill source. The
SDK's `listWorkspaceSkills` and the server's `GET /workspaces/{id}/skills`
both composed the raw `BUILTIN_SKILLS` constant, and the web app feeds its
pre-session onboarding menu from that route — so the five product skills
stayed listed until a session existed, then vanished from the session's
catalog.
Move the decision into `visibleBuiltinSkills(enabled)` next to the constant
and route every consumer through it, reading the switch via the shared
`builtinProductSkillsEnabled`. Keeping "what counts as a product skill" in
one place is the point: three copies of the predicate would drift the next
time a builtin is added. The SDK listing also awaits config readiness,
which it did not do before.
* fix(node-sdk): await config before materializing the global MCP OAuth provider
`McpOAuthService` caches providers by store key and stamps the client name
when it first builds one, and the preceding `globalMcpConfig.get()` reads
`mcp.json` directly rather than through `IConfigService`. So a
`beginGlobalMcpServerAuth` call made right after the harness is created
could resolve the identity before config finished loading, pinning the
built-in label for the rest of the process — including the OAuth dynamic
registration a third-party MCP server records.
`testGlobalMcpServer` already awaited config readiness for its own reasons;
this path now does too.
* refactor(agent-core-v2): drop the unused builtin-skill registrar
`registerBuiltinSkills` stamped the raw constant into a catalog for "edge
composition without a Session" — exactly the shape that now has to respect
`builtin_product_skills`. It has no callers in v2 and is not exported from
the package index, so it was dead code that also stood as an invitation to
bypass the switch. v1 keeps its own copy.
Every remaining path composes builtins through `visibleBuiltinSkills`.
* fix(agent-core-v2): send the configured identity on custom-registry imports
`:import_registry` fetched a user-supplied third-party URL with a
hardcoded `kimi-code-kap-server` User-Agent, so the first request to a
registry announced the product while every scheduled refresh of the same
registry announced the configured identity. The hardcoded value was wrong
on its own terms too: that token names the server, and this path also runs
in the CLI.
Both services now project the identity through `identityUserAgent`, which
carries the two guards (no host header, or no identity) once instead of
per caller. The model catalog keeps an inline copy on purpose — kosong is
a foundational layer and must not import an app domain.
Sweeping the remaining outbound User-Agent sources found no further gaps:
WebFetch deliberately sends a Chrome-like UA, the models.dev catalog fetch
sends none from the CLI, and kap-server's `user-agent` reads are inbound.
* docs: scope the identity env vars and condense the changeset
The environment-variable reference advertised all three new variables
without noting that only the agent-core-v2 engine reads them; the
configuration page already carried that note. Added in both locales.
The changeset had grown into two paragraphs of implementation detail,
which is what would land in the CLI release changelog. `gen-changesets`
asks for one short sentence plus at most a one-line usage hint.
* docs(agent-core-v2): describe the identity as what the agent calls itself
The module headers had drifted into describing the feature by what it
keeps off the wire rather than what it configures. Reworded so they state
the capability: the identity is the name the agent uses for itself, and
the unset case is a no-op rather than something "safe". The product-skill
switch excludes skills rather than hiding them.
Wording only; behavior and structure unchanged.
* test(agent-core-v2): cover the identity on custom-registry imports
The import path switched from a hardcoded `kimi-code-kap-server` token to
the host User-Agent projected through the identity, but nothing asserted
it. Two cases pin both halves: a configured identity reaches the request,
and an unconfigured one leaves the host header intact — the second matters
because a single case would also pass if one hardcoded value had simply
replaced another.
Both fail against the previous implementation.
* fix(node-sdk): guard every global MCP OAuth path behind config readiness
`McpOAuthService` caches providers by store key and stamps the client name
when it first builds one, so any path that can materialize a provider has
to run after config has loaded. `beginGlobalMcpServerAuth` awaited
readiness, but `resetGlobalMcpServerAuth` reaches the same cache through
`invalidate()` -> `getProvider()` without waiting: resetting auth right
after the harness is constructed pinned the built-in client name, and the
await added to the begin path could not help because it then reused that
cached provider.
Rather than add the missing await, the accessor is now async and holds the
guard itself, so the service cannot be obtained before config is ready and
a future entry point cannot forget. The remaining `configReady` in
`testGlobalMcpServer` stays — that one is for its own `[mcp]` section read.
* fix(agent-core-v2): send the configured identity on models.dev requests
The directory fetch behind `listModelsDevProviders` / `getModelsDevProvider`
still hardcoded a `kimi-code-kap-server` User-Agent, so browsing or importing
from models.dev announced the built-in product — and claimed to be the server
even when running in the CLI. Only the custom-registry import had been fixed.
`getModelsDevCatalog` now takes the User-Agent from its caller: the module is
plain module-level state with no container access, and the value depends on
the host and the configured identity, which only the calling service can see.
All four third-party fetches in that service share one helper.
Where the host states no User-Agent, a neutral token stands in rather than
dropping the header — these are directories the service chooses to call, so
there is no host intent to preserve, unlike the provider requests the model
catalog assembles.
Both new tests fail against the previous hardcoded value.
* test(agent-core-v2): assert the product-skill set literally
The expected sets were derived from the same `productSpecific` field the
production filter reads, so a builtin silently losing its marker would just
move between sets and leave every assertion green — while staying visible to
the model once the switch is off. The five names are now literal, with a test
asserting the marked set matches them exactly.
Dropping the marker from one skill now fails four tests instead of none.
Also states the App scope in the identity contract header, per the domain's
comment convention for contract files.
* fix(agent-core-v2): normalize the host-declared display name too
Blank and padded values were normalized on the config side but not on the
host fallback, so an embedding host passing `displayName: " "` rendered
"You are ," into the system prompt, and a padded name kept its padding.
Same rule now applies to every source of the name.
Also names `agentIdentity` as the collaborator in the request-headers
adapter header, which described the value it obtains without saying which
domain resolves it.
The three new cases fail against the previous implementation.
* fix(agent-core-v2): keep the configured slug when the host sends no User-Agent
The neutral fallback added for hosts that state no `User-Agent` discarded a
configured identity along with it: `identityUserAgent` returns `undefined`
as soon as there is no host header to rewrite, so `?? DEFAULT_IDENTITY_SLUG`
sent the literal `agent` even when `[identity].slug` was set — precisely the
case that fallback exists to serve. The configured slug now stands on its
own, with the neutral token reserved for having neither.
The four combinations of (host header, configured slug) had three tests; the
missing one is the one that was wrong. It now fails without this change.
`outboundUserAgent` also awaits config readiness before reading the identity,
so a browse issued right after bootstrap cannot send the pre-load value — the
guard lives in the accessor rather than at its four call sites, matching how
the same race is handled elsewhere in this branch.
Both headers here and in `discoveryService` now name `agentIdentity` as the
collaborator resolving that token.
* test(acp-server): follow the renamed skill-activation tag
`acp-server` arrived on main after the tag rename, so its two assertions
still expected `kimi-skill-loaded` and failed once the branches met. Also
updates the web app's CSS comment, which named the old tag from the start
of this branch — a comment, so nothing ever failed on it.
Found by CI: the merge verification only ran agent-core-v2's suite, and
this package is neither a dependency nor a dependent of it.
* fix(agent-core-v2): present the configured slug on registry refreshes too
The previous round taught the import path to fall back to the configured
slug when the host states no `User-Agent`, but left the scheduled refresh
of the same registry on the bare projection — so one registry could see
`acme` on import and the runtime default on refresh.
Extracting `identityUserAgent` had made the two paths share a function
without sharing the policy. The choice itself is now the shared piece:
`identityUserAgentOrDefault` always yields a value, for the directories
this process chooses to call, while `identityUserAgent` stays the form
that rewrites only what the host already sends — what a provider request
needs, where the host's silence is its own choice.
* docs(agent-core-v2): move new member docs into the module headers
The domain's comment convention is absolute — comments live solely in the
top-of-file block — and I had read the "functions, methods, or statements"
clause as leaving interface members out. It does not: only 25 of 734 v2
sources carry an indented block, so the members I documented were the
exception, not the pattern.
Seven members across six files move into their headers. `types.ts` had no
header at all, so it gains one.
* fix(agent-core-v2): connect session MCP overlays after config is ready
The shared manager reaches `connectAll` through `initialize()`, which awaits
the config domain first; `sessionOverlay` called it straight away. A session
carrying ephemeral `mcpServers` created right after bootstrap therefore
resolved the client name before config had loaded and initialized under the
built-in one.
The blast radius is wider than that one connection: a remote server sends
the overlay through `hasTokens()`, which materializes an OAuth provider on
the *shared* service and caches it by store key — so the early name outlives
the connection that raced. The overlay now connects behind `mcpConfig.ready`,
leaving the returned readiness promise unchanged.
* fix(agent-core-v2): reload builtin skills when their switch changes
The workspace catalog keeps each source's contribution for the life of the
handler, so a `builtin_product_skills` toggle never reached an existing
handler's sessions. That was harmless while every surface read the same
constant — but routing the session-less listings through the config made the
two views disagree, since those read the switch on every call.
Follows `ExtraFileSkillSource`: subscribe to the owning section and fire
`onDidChange`, which the catalog already turns into a source reload. The
test asserts an unrelated section does not trigger it.
* fix(agent-core-v2): apply the identity to self-configured web services
`[services.moonshot_search]` and `[services.moonshot_fetch]` name their own
`base_url`, so both services can point at an endpoint the user chose — but
each forwarded the host request headers verbatim, sending the built-in
product token there under a configured identity.
Only the services-config path is rewritten; the managed OAuth path keeps the
host headers as they are, being the endpoint the session authenticated
against. The distinction is the same one the model catalog draws per vendor.
`identityHeaders` carries the rewrite across a whole header set, so this is
the fourth caller sharing the projection rather than repeating its guards.
A pair of tests pins both halves.
My earlier sweep classified these two as official by their names instead of
asking who chooses the URL, which is why they were missed. The contract
header is also condensed here, per the convention below.
* docs(agent-core-v2): condense the identity headers to their contracts
The comment convention is one sentence with two halves — comments live only
in the top-of-file block, *and* that block states the module's role without
narrating implementation. Moving the member docs up last round satisfied the
first and broke the second: the headers ended up spelling out the slug
folding algorithm, the strip mechanics, and the load order.
Kept what a caller or the next editor would get wrong without it (why the
value is read rather than snapshotted, what `undefined` obliges a consumer
to do, why this source waits for config). Dropped what the code already
says. 22/12/12/13 lines, against 53 in `catalogService.ts` — length was
never the problem.
* fix(agent-core-v2): rebuild active prompts when the builtin skills change
Reloading the catalog on a `builtin_product_skills` toggle left existing
agents holding the old listing: `AgentProfileService` refreshes the prompt
only for the plugin source, so a disabled switch kept advertising skills
that were gone, and enabling it left them missing until an unrelated
refresh.
The plugin source is special because it also contributes prompt sections
(MoonshotAI#2314), and the file-backed sources are left out for cost — their fs
watches would rebuild every agent's prompt on each edit. The builtin source
has no watch: it changes only when its config switch is toggled, so it
belongs with the plugin source rather than with the file ones.
Subscribing to the catalog rather than the config section is load-bearing.
The catalog fires after the contribution is replaced, whereas a config
subscription would race the reload, and `resolveSkillListing` only awaits
the catalog's *initial* readiness — so the rebuilt prompt could read the
listing it was meant to replace.
The source id is a named constant now, so the subscription does not match
on a bare string.
* refactor(agent-core-v2): freeze the agent identity for the process lifetime
The identity is announced outward (MCP initialize, OAuth registration,
provider request logs) and cannot be re-announced, so mid-process changes
could only ever apply partially. Resolve it once when config first loads
and hold it for the life of the process: IAgentIdentity now hands out a
frozen snapshot via resolved()/current(), carrying finished products
(outbound User-Agent variants, rewritten header set) so call sites stop
composing host headers with the slug themselves. The kosong host-headers
port carries two finished layers and the catalog only picks one; consumers
gain no invalidation obligations because the value can never change after
the freeze. [identity] edits take effect on the next start (documented).
* fix(agent-core-v2): locate the User-Agent header case-insensitively
HTTP header names are case-insensitive, but the snapshot builder looked up
'User-Agent' by exact key: an embedding host spelling it 'user-agent' got no
third-party UA and kept its own product token on the services path even with
an identity configured. The builder now locates every case variant and
rewrites each in place, keeping the host's spelling. Also corrects the two
web-service headers that still described both paths as sending the bootstrap
headers, naming agentIdentity as the collaborator behind the config path,
and documents that a resumed session keeps its recorded system prompt.
* fix(agent-core-v2): attribute header provenance from the finished third-party layer
Inspection reconstructed the non-full host layer from the raw headers with an
exact-case 'User-Agent' lookup, so a host spelling the header 'user-agent'
got a resolved User-Agent with no provenance entry even though the runtime
sends the rewritten value. buildModel now captures the port's finished
third-party layer in the trace and attribution reads it, keeping inspect()
on the same resolution pass as get(). Also condenses the identity contract
header to its external role, and documents that an existing MCP OAuth
authorization keeps the client registration it was granted under (reset the
server's auth to register under the new identity).
* fix(agent-core-v2): keep web tool backends from racing the identity freeze
An env-configured [services] endpoint is visible before config finishes
loading, and FetchURLTool / WebSearchTool materialized their backends at
construction — so a fast bootstrap could hit the identity snapshot's
pre-freeze guard during agent creation, and the composed backend pinned
config and login state for the agent's lifetime against the service's
documented per-call resolution. Both tools now resolve their backend per
invocation, the WebSearch activation gate checks presence alone through the
new hasWebSearchProvider() (no provider composition, no identity read), and
bind() awaits the identity freeze before materializing the model, whose
resolution reads the identity through the host-headers port.
Related Issue
Resolve #2532
Problem
What changed
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.