fix(agent-core-v2): harden plugin load, install, and update check paths#1576
Open
wbxl2000 wants to merge 633 commits into
Open
fix(agent-core-v2): harden plugin load, install, and update check paths#1576wbxl2000 wants to merge 633 commits into
wbxl2000 wants to merge 633 commits into
Conversation
…into kimi-code-v2
- write `kimi version <version>` to stderr first in text mode - emit a `system.version` meta line first in stream-json mode - gate behind isKimiV2Enabled() so the default v1 path is unchanged
…cap to 3000px (#1460) * fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px Image compression now reports original dimensions in the decoded (EXIF-rotated) space, matching the coordinate system of the sent image and of ReadMediaFile region readback; previously portrait JPEGs (orientation 5-8) got swapped width/height in captions. The longest-edge downscale cap rises from 2000px to 3000px, and the default jimp resize path is documented as the anti-aliased area-average one so it is not accidentally switched to a point-sampled interpolation mode. * test: shrink oversized image fixtures to fit CI timeouts The 3600x3600 fixtures introduced for the 3000px edge cap nearly doubled the pixel area jimp has to decode and deflate, pushing the slowest compression tests past the 5s vitest timeout on CI runners. 3600x1800 keeps every fixture over the cap while restoring roughly the workload of the old 2600x2600 fixtures that CI handled comfortably. * test: pin anti-aliased downscale quality with executable guards A 1px checkerboard probe pins the compressor to full-coverage averaging at integer and fractional ratios, with jimp's point-sampled BILINEAR mode kept as the executable aliasing counter-example (it collapses the 50%-gray pattern to solid black at 4:1). Also guards the other classic downscale bugs: transparent-pixel color bleed, mean-brightness drift, iterative recompression degradation, and zero-size collapse on extreme aspect ratios. * fix(agent-core): report decoded EXIF-rotated dimensions in ReadMediaFile notes The media note derived its original-dimensions line from the header sniff, which reports pre-rotation values for EXIF orientation 5-8 JPEGs. The sent image and region readback both live in the decoded (rotated) space, so portrait photos got axis-swapped coordinate guidance. Once a decode has happened — compression or crop — its dimensions now overwrite the sniffed ones. * fix(agent-core): improve handling of EXIF orientation in image dimensions and metadata * fix(agent-core): sniff EXIF orientation and step budget fallback through 2000px Two follow-ups to the EXIF and 3000px-cap changes: sniffImageDimensions now reads the JPEG EXIF Orientation tag (pure header parse, both byte orders) and reports display-space dimensions for orientations 5-8. Passthrough images — never decoded — previously kept the pre-rotation header size in compression results and media read notes, disagreeing with the decoded space that region readback uses. encodeWithinBudget steps the over-budget fallback through 2000px before the 1000px last resort. Raising the cap to 3000px had left a regression window: an image whose 2000px encode fits the byte budget was sent at 1000px where the old 2000px cap used to send it at 2000px. * fix(kimi-code): record pasted image dimensions in display space The TUI paste path recorded attachment and original dimensions from its raw header parser, which ignores EXIF orientation. For a portrait JPEG the submit-time caption then contradicted the sent image's aspect and region readback coordinates were axis-swapped. Dimensions now come from the compression result, which reports display space on both the compressed and passthrough paths; parseImageMeta remains only the format/mime gate. * feat(agent-core): add image compression and crop telemetry Every image ingestion path now reports an image_compress event — outcome (compressed / passthrough fast, guard, unsupported, unhelpful, error), input/output formats, byte and pixel sizes, EXIF transposition, and duration — and region readback reports an image_crop event with a failure classification and the region's share of the original area. Wiring is per call site via a new CompressImageOptions.telemetry option, so the outcome split and timing are measured inside the compressor while each caller only names its source: ReadMediaFile (tool construction, like GrepTool), MCP tool results (McpOutputOptions), server prompt ingestion (ICoreProcessService now exposes the host telemetry client), ACP prompts (session track adapter), and TUI paste (host.track adapter). Properties are numeric/enum only — never paths or content — and a throwing client can never affect the compression result. * fix(agent-core): run the full JPEG quality ladder at fallback sizes The fallback rescales encoded only at quality 20, so a JPEG whose ladder failed at the fitted size collapsed straight to the lowest quality even when the smaller size left budget headroom for a higher rung (the realistic window is the 1000px step, where the 4x pixel drop pays for q80/q60). Each fallback edge now walks the same q80-to-q20 ladder as the fitted size. * test: shrink heavy JPEG fixtures and add explicit timeouts The fallback-ladder test runs ~11 pure-JS JPEG encodes and the EXIF paste test decodes, rotates, and re-encodes a 6.5MP frame; both sat at the edge of the 5s vitest timeout on CI runners. Narrower fixtures cut the pixel area (the ladder test keeps its width above 2000px so the full fallback chain still runs) and explicit 15s timeouts absorb runner variance. * fix(server): scope prompt image compression telemetry to the session The prompt-ingestion image_compress events were emitted with the bare host telemetry client, while every agent-side source inherits a session-scoped client — so prompt_inline/prompt_file events could not be correlated with their session. The route now wraps the client with withTelemetryContext({ sessionId }) like rpc/core-impl does for session telemetry. * chore(changeset): consolidate image compression changesets One entry covering the cap raise and the EXIF dimension fix, listed for both the CLI and the SDK so the SDK changelog's compression description (previously pinned at 2000px) stays accurate.
Align progressive tool disclosure with the discard-on-compaction model: compaction no longer rebuilds loaded dynamic tool schemas. The boundary announcement re-lists every loadable name, the model re-selects what it still needs, and a from-memory call to a no-longer-loaded tool is rejected by preflight with select guidance. This removes the keep-all rebuild and its half-trigger budget heuristics entirely: the post-compaction floor is back to users + summary, which is structurally outside the auto-compaction trigger band, and the guard baseline degenerates to summary + reinjected reminders. Every downstream mechanism already treated the empty loaded set as its consistent base state (ledger scan, pending clear at the compaction boundary, deferred extras, preflight wording), so this is a strict simplification. Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
Headless (`kimi -p`) failures could exit with code 0 when the event loop drained during the shutdown cleanup (e.g. telemetry's unref'd retry backoff when the network is blocked), because the rejection never reached the process.exit(1) call. Set the failure exit code before any await in both the run-prompt catch and the main catch, and keep the cleanup timeout ref'd so the loop stays alive long enough for the rejection to propagate.
* feat(web): redesign cron reminder as a message bubble Restyle the cron trigger notice as a right-aligned user-style message bubble that shows the scheduled prompt in full (wrapping across lines), with a small meta row beneath it for the schedule, status, job id and run time. Extract a shared MessageTime component used by both user messages and the cron reminder so the timestamp format and click-to-expand behavior stay consistent, and give the CronCreate/CronList/CronDelete tools distinct calendar icons. * refactor(web): render cron reminders only as standalone turns Remove the embedded cron block path from the web transcript projector so cron reminder fires always render through the standalone right-aligned bubble path. * chore(web): simplify cron redesign changeset
) * fix(web): composer model switch also updates global default model The composer model switcher still switches the active session's model via POST /sessions/{id}/profile (awaited, so the model pill reflects the result), and additionally fires POST /api/v1/config with { default_model } as a fire-and-forget side effect so new sessions inherit the chosen default. The config request is skipped when the model already matches the current default. * fix(web): route ModelPicker overlay selection through the default-model update The overlay opened from the composer's "More models" row (and /model) is a continuation of the same switch flow, so its selection now also bumps the global default model instead of only switching the active session. * fix(web): only persist the default model after a confirmed session switch setModel now returns whether the switch was accepted (true for the draft path), so the composer flow no longer writes a stale or invalid model alias into the global config when the session-level switch failed and rolled back.
- port probeLoginShellPath/mergeLoginShellPath/applyLoginShellPath into _base/execEnv/loginShellPath.ts as a pure helper (no DI) - export execFileText from environmentProbe for reuse by the probe - run applyLoginShellPathFromNode concurrently with the host probe in HostEnvironmentService, mirroring kaos LocalKaos.create() Aligns agent-core-v2 with kaos 021786f so the Bash tool finds user-installed tools (e.g. Homebrew's gh) when kimi-code is launched from a GUI or non-login shell.
…into kimi-code-v2
…into kimi-code-v2
…into kimi-code-v2
…into kimi-code-v2
- protocol: add optional agent_filter to client_hello and subscribe - kap-server: carry per-subscription agent allowlists through the broadcaster and connection, narrowing live fan-out and replay to selected agents while keeping a single global sequence and bypassing the filter for global events - agent-core-v2: degrade MiniDbQueryStore to a no-op read model when the query-store lock is held by another process instead of crashing the host
…m from built-in commands (#1492)
…into kimi-code-v2
Since d5e1d76 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.
…ontinuation driver 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.
…exhausted 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.
- 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
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.
- 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
- 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 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
- 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 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
- 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 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
- 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
- loop: beforeStep -> onWillBeginStep, afterStep -> onDidFinishStep - toolExecutor: onWillExecuteTool -> onBeforeExecuteTool (ToolWillExecuteContext -> ToolBeforeExecuteContext) - prompt: onWillSubmitPrompt -> onBeforeSubmitPrompt - permissionMode: onChanged -> onDidChangeMode - wireRecord: onRestoredRecord -> onDidRestoreRecord, onResumeEnded -> onDidFinishResume - terminal: onData -> onProcessData, onExit -> onProcessExit
…models
- 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
Replace fire-and-forget OrderedHookSlot hooks with Emitter/Event-based notifications for consumers that only observe, never intercept: - usage: hooks.onDidRecord -> onDidRecord event - permissionMode: hooks.onDidChangeMode -> onDidChangeMode event - fullCompaction: hooks.onDidFinishCompaction -> onDidFinishCompaction event - wireRecord: hooks.onDidFinishResume -> onDidFinishResume event - agentLifecycle: hooks.onDidStopAgentTask -> onDidStopAgentTask event, announced via new notifyAgentTaskStopped() called by mirrorAgentRun Interception-capable slots (onWillStartAgentTask, onWillCompact, onDidRestoreRecord) stay as ordered hooks.
… when logged in 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.
…ests 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.
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.
- 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 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.
- 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
- 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.
🦋 Changeset detectedLatest commit: 2727720 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related Issue
None — the problems are explained below.
Problem
The
agent-core-v2plugin module (ported frompackages/agent-core) had a set of robustness and parity regressions, found by a static diff review against v1:plugins/installed.jsontook down whole sessions: the session skill catalog'sreadystayed permanently rejected (skill list and the Skill tool failed), the main agent failed every turn while rendering plugin session-start content, and the plugin load error surfaced mislabeled asMCP_STARTUP_FAILED. In v1 this degraded silently and only management calls failed with a coded error.kimi-plugin-zip-*temp dirs on failure and reported the soon-to-be-deleted temp path (instead of the user's URL) in manifest errors; a failed install or persist could also destroy the previous managed copy.KIMI_CODE_BASE_URL/KIMI_CODE_OAUTH_HOST) that v1 injected.checkUpdatesfailed wholesale when a single repo errored, and reported phantom updates for branch/SHA-pinned installs.PluginSummary.skillCountused a hand-rolled counter that diverged from real skill discovery (rootSKILL.mdfallback, nested sub-skills, frontmatter validation, name dedup).What changed
All fixes are covered by new or updated tests (30+ cases); the full package suite passes (231 files / 3090 tests), plus
typecheck(agent-core-v2 and kap-server),lint:domain, andgit diff --check.Plugin module (
src/app/plugin/):plugin.load_failedwith the v1 repair hint (Fix the file at <home>/plugins/installed.json and run /plugins reload); a successfulreloadPlugins()clears the error. The initial load is shared across concurrent callers and mutations are serialized.getPluginInfoand the manager's not-installed paths throwplugin.not_found; the contract now returnsPluginInfo(non-optional), matching the wire contract.managed/<id>copy; records are swapped only after a successful persist.KIMI_CODE_BASE_URL/KIMI_CODE_OAUTH_HOSTwith v1 precedence (explicit env >KIMI_OAUTH_HOSTlegacy > persisted provider).skillCountnow goes through the same discovery as the plugin skill source.Supporting seams outside
src/app/plugin/(each is a small, generic change — no plugin-specific logic leaks into core):contextInjectorre-syncs injection positions from the real history when a wire restore completes (onRestored), fixing duplicate injections after cold resume for all variants, not just plugins.ISessionSkillCatalog.onDidChangenow carries the refreshed source id, so plugin session-start reminders fire only after the plugin source actually re-pulls (listening toonDidReloaddirectly would race the async catalog refresh).plugin.id + name) so same-named skills from different plugins no longer shadow each other; the scanner is exported for the standalone manager's default.IProviderServicegainedreadyso the managed-env lookup can await config load.Points worth a deliberate look during review:
process.envdirectly)./api/v1plugin surface alignment.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update. (Internal v2 engine fixes; no user-facing CLI behavior change.)