- Degrade plugin consumption reads to empty when installed.json fails to
load, surface plugin.load_failed with a repair hint on management calls,
and recover after an explicit reload; serialize the initial load and
mutations so concurrent first callers share one load.
- Clean up zip temp dirs on every failure path, report the original
source in zip/github manifest errors, and roll back to the previous
managed copy when an install or persist fails.
- Restore managed Kimi endpoint env injection for stdio plugin MCP
servers.
- Check plugin updates concurrently with per-repo failure isolation and
10s timeouts, track branch installs by commit SHA, and stop false
update reports for tag/SHA pins.
- Throw plugin.not_found from getPluginInfo and the manager's
not-installed paths.
- Count plugin skills through the real skill discovery path.
- Re-sync context injection positions after silent wire replay so cold
resumes do not duplicate injections, and fire plugin session-start
reminders only when the plugin skill source finishes refreshing.
- listen messages accept a service name; kap-server resolves the Service
via resolveService and subscribes through its onUpperCase member
- add listen_result acknowledgement and per-listen error reporting
(onDidListenError) so failed subscriptions surface to the client
- support onWill-style events: payload carries eventId/signal/waitUntil,
the client replies with event_result and the server can event_cancel
- klient proxy maps onUpperCase members to channel.listen; WsChannel
shares one remote subscription across first/last listeners
- channel.call now forwards the complete argument array
The v1 WS broadcaster only re-emitted event.session.status_changed(running)
on turn.started and never emitted the idle/aborted transition on turn.ended.
kimi-web treats that event as the single source of session status (its
turn.ended projector deliberately does not synthesize idle), so a session
stuck at 'running' after the turn finished — most visibly for background
tasks, where ISessionActivity keeps reporting non-idle while the detached
task lives and even a REST pull never corrected it.
Re-emit event.session.status_changed after turn.ended on the same dispatch
queue, mapping reason cancelled/failed/blocked to aborted and otherwise
idle (previous_status 'running'), matching v1's _computeStatus. Update the
broadcaster tests and harden the wsV1Resync test helper so non-matching
frames no longer strand a waiter's timeout.
- task: idle terminal notifications now use activeOrNewTurn admission,
launching their own turn instead of waiting for the next user prompt
(matches v1 turn.steer)
- workspaceRegistry: createOrTouch rejects missing or non-directory roots
with fs.path_not_found, so a phantom cwd never reaches session creation
- kap-server: map FS_PATH_NOT_FOUND to protocol error 40409 on the session
and RPC surfaces
- server-e2e: migrate the v2 smoke test from the local ServerClient to the
typed Klient
- misc: switch loop/prompt clear() iteration to .slice(), align terminal
event handler names, and tidy klient examples
The v2 boot path (kimi server run with the experimental flag) now seeds the CLI's Kimi identity headers (User-Agent + X-Msh-* device identity) into the engine through kap-server's seeds option, so outbound model, WebSearch, and FetchURL requests carry the same identity as direct CLI runs. kap-server's own package version is 0.0.0, so the identity has to come from the CLI.
WebSearchProviderService now passes the host's IHostRequestHeaders (User-Agent + X-Msh-* device identity) as default headers to the Moonshot search provider, mirroring v1's kimiRequestHeaders.
When the managed Kimi provider has an oauth ref, WebFetchService builds a MoonshotFetchURLProvider (bearer token + host identity headers) with the local fetcher as fallback, re-reading login state on every call; logged-out setups keep the local fetcher.
- GET /api/v1/models now awaits refreshProviderModels({ scope: 'all' })
before returning the model list, so the response always reflects the
latest provider model metadata
- refresh failures are logged and swallowed, falling back to the
persisted catalog instead of failing the request
- add WsSocket: persistent /api/v2/ws transport with hello handshake,
heartbeat answers, per-call timeouts, and auto-reconnect that
re-subscribes active listens; bearer token rides the
kimi-code.bearer.<token> subprotocol for browser compatibility
- add WsKlient / WsChannel exposing core/session/agent scopes and
listen(event, handler) over the shared socket
- add Klient#ws() lazy singleton with WebSocketImpl injection
- bind global fetch in HttpChannel to avoid "Illegal invocation" in browsers
A kimi -p run settled the moment the main agent's turn ended (end_turn),
so a goal created mid-run was cancelled during cleanup and a scheduled
cron task never fired in the same run.
- runPromptTurn now re-evaluates completion when the main agent goes idle
and stays alive while a goal is still active (the goal driver runs the
continuation turns) or while cron tasks with a future fire remain (their
fire steers a fresh turn). A ref'd handle keeps the event loop alive
during the wait since the cron scheduler tick is unref'd.
- a terminal goal.updated (e.g. the driver blocking a goal on a hard
budget, which emits no further turn.ended) also re-evaluates so the run
cannot hang.
- add getCronTasks RPC and Session.getCronTasks() so the print flow can
enumerate pending cron tasks.
- add queued turn and step lifecycle handles with explicit admission modes
- move continuation and retry scheduling behind the loop service
- consolidate legacy prompt scheduling into the prompt domain
- align kap-server routes and tests with the new loop contract
Tool-role messages reached the snapshot/messages REST projection with
their content flattened to text, dropping image/video/audio parts, so a
ReadMediaFile result rendered as an image while streaming but fell back
to a generic tool card after a reload. Pass the raw content parts
through when a tool result carries media, matching the live tool.result
event shape the web client already parses.
- make loop admission, cancellation, and completion own turn execution
- extract step retry into a loop error recovery service
- preserve failed-step context and expose retry delay events
- add commit-align.md subskill: triage one main-branch commit against v2
(aligned / partial / missing / not-applicable) and link it from SKILL.md
- delete docs/di-scope-domains.puml and the rendered svg, and drop the
keep-the-map-in-sync requirement from verify.md, align.md, commit-align.md,
and packages/agent-core-v2/AGENTS.md
- add translateProviderError in app/protocol/errors and apply it once in
ModelImpl.request: raw provider failures become coded KimiErrors with the
raw error preserved as cause and HTTP fields in details; abort shapes pass
through untouched
- move provider-error mapping out of _base/errors/serialize so _base no
longer imports llmProtocol; toErrorPayload/fromErrorPayload now round-trip
cause chains recursively, capped at depth 8
- move context.overflow from LoopErrors to ProtocolErrors (wire code
unchanged); move LoopError and the max-steps helpers into loop/loop.ts
- consolidate isAbortError into _base/utils/abort, dropping the duplicates in
retry, cloudTransport, and the question/subagent task tools
- add unwrapErrorCause; classify retryability and HTTP status on the
unwrapped cause in llmRequester and full-compaction
- align task-notification tests with enqueue-only delivery: drain the loop
queue with one turn and assert on task.notified instead of prompt steer
- protocol: add recursive cause to KimiErrorPayload with a lazy zod schema
- add StepRequest / StepRequestQueue: the loop drains one batch per step,
folding mergeable requests (steers) into the driver's step; a step that
ran tools enqueues a ContinuationStepRequest, a plain message enqueues
none, so the turn completes when the queue empties
- replace AfterStepContext.continue with explicit enqueue; a failed step is
retried by head-inserting its driver request
- move prompt's private steer queue onto the loop via PromptStepRequest /
SteerStepRequest / RetryStepRequest, which materialize their context
messages at pop time (image-compression captions reroute to reminders
on materialization)
- goal and externalHooks orchestrate continuations by enqueueing requests
instead of setting ctx.continue; a request's message only lands when the
loop pops it, so skipped or aborted launches leave no orphan messages
- remove the now-unused CancellationError
- delete the reworked turn tests (turn.test.ts, turn-ready.test.ts) and the
cron test suites
- replace per-method actionMap (resource:action) with a channel registry: each Service registers once by decorator id and all methods are invoked by reflection
- routes move from /api/v2/:sa to /api/v2/:service/:method across HTTP routes and the WS protocol
- add @moonshot-ai/klient: typed core/session/agent client over the HTTP channel that reuses agent-core-v2 service interfaces
- register klient in flake.nix and pnpm-lock; refresh kap-server tests, e2e, and the apiSurface snapshot
- add Session-scope ISessionInitService that spawns the coder subagent,
mirrors the run onto the main agent, reloads AGENTS.md and appends an
init-variant system reminder, then flushes records
- add SESSION_INIT_FAILED error code and register the sessionInit domain in
the layer map, package index and DI dependency graph
- drop the stale "flat (no subdirectories)" rule from the agent-core-dev skill
The v2 engine emits background-task lifecycle as `task.started` /
`task.terminated`, but v1 consumers (kimi-code TUI / `kimi -p`, node-sdk)
only handle `background.task.*` and silently dropped every task event when
talking to server-v2, while kimi-web handles the native spelling and has the
legacy one registered as known-but-unhandled.
Fan the legacy spelling out next to the native event in
SessionEventBroadcaster, reusing the same volatility so replay, journal and
the per-agent filter stay coherent between the two. kimi-web keeps the native
event and ignores the alias; the native /api/v2 stream is left unchanged.
Add a SessionEventBroadcaster test asserting both spellings are emitted.
- remap tool_calls finishReason to 'other' when the provider emitted no tool call structure
- prevents re-issuing the model call until maxSteps on a bare tool_calls signal
- add loop test covering the v1 'unknown' turn-lifecycle behavior
Previously a goal whose hard budget was reached mid-turn was only
flipped to blocked while the turn kept running unbounded: the loop
continues unconditionally after a tool-calls step, and steer flushes or
Stop hooks could extend the turn indefinitely past the budget.
Now, when the over-budget step requested tool calls, a goal_budget_stop
system reminder is appended after the tool results telling the model the
goal is blocked (resumable via /goal resume), to stop immediately, that
further tool calls will be rejected, and to write a brief final status
message. The model gets exactly one grace step, during which tool calls
are answered with a soft rejection instead of executing. After the grace
step - or when the over-budget step had no pending tool calls - a new
AfterStepContext.stopTurn flag ends the turn, honored by the loop with
precedence over tool_calls and hook-set continue so nothing can extend
past the stop. The grace grant also respects maxStepsPerTurn so it can
never turn a budget stop into a max-steps turn failure.
Turn launch is gated as well: a prompt arriving while the active goal is
already over budget (e.g. after resuming an exhausted goal) blocks the
goal before the turn is marked goal-driven, so turnsUsed no longer
drifts, no spurious goal_continued telemetry fires, and the prompt runs
as an ordinary turn with the blocked-goal note injected.
This deliberately diverges from agent-core v1, which hard-stops with
zero grace and answers a prompt on an exhausted goal with a synthetic
model-less turn: budget overshoot is now bounded at one closing step in
exchange for consumed tool results and a user-facing wrap-up message.
The per-turn-boundary reminder test predates the goal continuation
driver and never passed: its second explicit prompt raced the
auto-launched continuation turn, which correctly holds the turn lane.
Treat the continuation turn as the second boundary and end it
deterministically by completing the goal through UpdateGoal, keeping
the once-per-boundary (never per-step) assertion.
Since d5e1d76fc every wire append rides the async persist queue whenever
a blob service is registered, but AgentWireRecordService.flush() only
awaited the log store. Callers - including the session-close path -
could complete a flush while records were still in flight on the queue,
and record-order assertions in tests raced it. Await the wire service
flush, which drains the persist queue, before flushing the log.
* docs(changelog): sync 0.23.5 from apps/kimi-code/CHANGELOG.md
* chore: update config model doc
* docs: update config-files example with new models and services
---------
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
* refactor(kosong): rename select_tools capability to dynamically_loaded_tools
Rename the `ModelCapability` bit from `select_tools` to `dynamically_loaded_tools` everywhere it is declared, detected, catalogued, and forwarded: kosong `ModelCapability`/catalog, agent-core capability resolution and the `toolSelectEnabled` gate, the SDK catalog-to-alias mapping, and the built-in catalog pruner's keep list.
The old `select_tools` spelling is removed outright rather than kept as an alias — no catalogued model or shipped configuration used the capability, so there is nothing to migrate. Client-side vocabulary (the `select_tools` builtin tool and the `tool-select` experimental flag) is intentionally untouched.
* chore: shorten changeset description
---------
Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
The web client received two sessionStatusChanged events per turn
transition: one projected client-side from the raw turn.started/turn.ended
stream, one mapped from the daemon's event.session.status_changed. After
the tag scheme in #1479 keyed the completion notification by prompt id,
the second (redundant) idle event lost the cached prompt id and fell back
to a Date.now() tag, so every turn end popped a second "Turn finished"
notification and replayed the completion sound.
Stop projecting sessionStatusChanged from the raw turn stream (turn.started,
turn.ended, and the in-flight snapshot seed). The daemon's
event.session.status_changed is the single source of status transitions:
it is computed from live daemon state (covering awaiting-approval /
awaiting-question / aborted), carries the authoritative previousStatus and
currentPromptId, and is deduped per real transition server-side. The turn
stream keeps its content responsibilities (message finalization, usage,
duration); seedInFlight keeps seeding the partially-streamed message while
status comes from the snapshot's authoritative session record.
* fix: refuse unsupported image formats instead of poisoning sessions
Images in formats providers reject (AVIF, HEIC, BMP, TIFF, ICO) used to
pass through to the API, and the resulting HTTP 400 repeated on every
later turn because the image_url stayed in the session history.
Add a single format policy (accepted set: PNG/JPEG/GIF/WebP) enforced at
every ingestion point: ReadMediaFile refuses with a per-OS conversion
command; MCP tool results, REST uploads, and ACP prompts replace the
image with a text notice; and turn.prompt/steer gates as the last-funnel
backstop so the SDK/RPC path cannot poison a session either. Accepted
MIME aliases (image/jpg, case/whitespace) are forwarded in canonical
form, and data URLs carrying MIME parameters can no longer slip past the
gate. Remote image URLs pass through (no bytes to inspect).
* fix: canonicalize accepted data URLs with MIME parameters
The format gate compared only the MIME token when deciding whether to
rebuild a data URL, so an accepted image carrying MIME parameters
(`data:image/jpeg;charset=utf-8;base64,...`) was forwarded with its
original header. The Anthropic provider splits the data URL and
exact-matches the full header against its whitelist, so the part still
poisoned the session. Rebuild to the byte-exact canonical URL whenever
the original differs, covering aliases, case/whitespace, and parameters
with one comparison.
Addresses review feedback on PR #1536.
* fix: parse data URLs case-insensitively in the image format gate
An uppercase `;BASE64,` marker is legal (RFC 2045 encoding names are
case-insensitive), but the parser required a lowercase match and
returned null, so the gate treated the URL as remote and forwarded it:
an unsupported image could still land in the session history, and the
Anthropic provider's lowercase-only split then threw on every turn.
Match the scheme and marker case-insensitively; the canonical rebuild
emits the lowercase form.
Addresses review feedback on PR #1536.
* fix: harden image format handling against mislabeled and legacy images
Two more ways an unsupported image could reach the provider are closed:
- Bytes, not labels, decide the format. A data-URL image whose declared
MIME disagrees with its magic bytes (e.g. AVIF bytes an image search
tool labels image/png) is now gated on the sniffed format at every entry
point (MCP results, ACP, SDK/RPC prompt, REST inline and file uploads),
so a mislabel cannot slip past the gate.
- A poisoned image already in the session history no longer kills the
session: a server image-format 400 (or kosong's client-side image
rejection) now retries once with every media part replaced by a text
marker, mirroring the 413 media-degraded recovery. The recovery also
fires during compaction, and the transient-retry fallback no longer
burns the retry budget on image-format errors before the dedicated
recovery can run.
* fix: reject remote image URLs ending in an unsupported extension
Remote image URLs (MCP resource_link, REST `kind: 'url'`) carry no bytes
to sniff, so a link ending in `.avif` (or `.heic`, `.bmp`, `.tiff`,
`.ico`) would pass through and be fetched server-side — and rejected.
Reject such URLs by their path extension instead (query/fragment
ignored, case-insensitive); extensionless or accepted-extension URLs
still pass through to the provider and the 400 recovery.
* fix: tighten image format handling for parameterized MIMEs and recovery scope
Address two review findings on PR #1536:
- A declared media type with parameters (e.g. image/jpeg; charset=utf-8)
is no longer misread as unsupported: normalizeImageMime now strips
parameters, matching the data-URL parser, so an accepted image with
parameters is forwarded instead of dropped.
- The image-format recovery predicate is narrowed to specific
format/data rejection phrases, so a 400 about image count, size, or
image-input support no longer triggers a media-stripped resend that
would let the model answer blind to the user's images.
* fix
* fix: scope image format recovery to images and flag remote SVG URLs
- The media_type/mime_type recovery match now requires the message to
mention an image, so a video/audio media_type rejection surfaces
instead of triggering a blind media-stripped resend.
- unsupportedImageMimeFromUrl flags .svg URLs as image/svg+xml without
touching the shared suffix map (SVG stays text for the file tools),
so remote SVG images get the intended notice instead of a provider
rejection.
Addresses review feedback on PR #1536.
* fix: reject remote MCP images by their declared MIME type
An MCP resource_link with an extensionless or signed URL gives the
extension gate nothing to work with, and convertMCPContentBlock was
discarding the declared mimeType — an honestly-declared AVIF/HEIC link
from an image search tool still became an image_url and poisoned the
session. Reject on the declared MIME when the server provides one:
unsupported declarations become a text notice that keeps the URL so the
model can fetch and convert it; accepted declarations pass through as
before.
Addresses review feedback on PR #1536.
* fix: keep image format recovery image-specific and preserve dropped URLs in notices
- Drop the bare `media` alternative from the image-format recovery
patterns so audio/video media rejections ("unsupported media type",
"invalid media type") can never be misclassified as image errors and
blindly media-stripped; every pattern now mentions "image" literally.
- Remote image URLs rejected by their extension now keep the URL in the
replacement notice (gateImageFormatParts and the REST url path), so the
model can still fetch and convert the image — matching the declared-MIME
resource_link path.
Addresses review feedback on PR #1536.
* fix: drop malformed data URLs at ingestion instead of letting them poison the session
A `data:` URL that fails to parse (missing `;base64,` separator, empty
MIME, …) was treated like a remote URL and passed through the format
gate; the provider then rejects it on every turn, and the read-side
media-stripped recovery keeps paying that round-trip until compaction.
Detect unparseable `data:` URLs in gateImageFormatParts and replace them
with a (truncated) notice at ingestion, covering the MCP/ACP/SDK/turn
paths that share the gate.
Addresses review feedback on PR #1536.
micro-compaction only exists in legacy agent-core; v2 has no such mechanism. Update two comments that still cited it:
- fullCompactionService: the real reason not to project here is that llmRequester already projects once.
- swarmService: context.spliced consumers no longer include micro-compaction bookkeeping.
- sessionLifecycle: get/list no longer return a session whose cold resume is
still in flight, so callers never observe a half-initialized handle; resume
remains the way to await a fully restored handle
- messageLegacy: reduce the transcript from the main agent's in-memory wire
journal instead of re-reading wire.jsonl; AgentWireRecordService now keeps
the journal current with live dispatch so cold and live sessions both read a
consistent, full transcript
* fix(web): hide injected system asides in user message bubbles
* fix(web): preserve literal <system> tags in user prompts
* chore: fold duplicate web changeset into caption-hiding entry
- agent/task/manager.test.ts -> taskManager.test.ts (no manager.ts in
agent/task; disambiguate from taskService.test.ts).
- app/cron/persist.test.ts -> cronTaskPersistenceService.test.ts (mirrors
the module it covers; agent/task/persist.test.ts mirrors
agent/task/persist.ts, so it is left as-is).
- agent/contextMemory/message.test.ts -> message-history.test.ts (its
describe is 'message history (IAgentContextMemoryService)'; the dir
already names the domain).
The fuzz-style invariant test now drives the full v1 fallback ladder
([2000, 1000, 768, 512, 384, 256]) for over-budget inputs, which takes
longer than the default 5s boundary in this environment. Give it 30s.
It round-tripped restore over a hardcoded local dataset
(kimi-code-mini-bench/.vitest-results) that is not in the repo, so it
vacuously passed everywhere else. Removed at the original author's
request.
Fast catch-up merge; no conflicts. Brings in tool-args JSON Schema format
validation (adds ajv/ajv-formats deps), cron tool-source cleanup, and test
file tidy. No overlap with the goal-parity work.