* feat: scope thinking effort to the current session
Selecting a model or thinking mode in the TUI or web UI no longer
persists the concrete effort to the config; only the boolean thinking
toggle is saved. The web UI now restores and submits each session's own
daemon-reported level instead of a browser-wide per-model pick, whose
localStorage store is removed.
* feat: persist bounded thinking efforts and migrate persisted max once
Only low/medium/high/xhigh are written to the config on a pick; max and
unrecognized levels stay session-only with just the boolean toggle saved.
A one-shot migration rewrites a previously persisted thinking.effort max
to high (recorded in migrations.json, never re-run). The web UI brings
back the per-model localStorage pick as the seed for new sessions, while
a session's own daemon-reported level keeps winning for existing ones.
* feat: gate effort persistence on the model top declared level
The session-only tier is now the last entry of the model's own
support_efforts (ordered by strength) instead of a fixed name list, so
custom provider-declared levels get the same treatment; anything below
the top persists as before, and unknown model metadata keeps the
concrete effort.
* docs: align changeset wording with the top-tier persistence rule
* refactor: rename the config migration marker file to migrations-effort.json
* feat: drop the legacy global thinking pick fallback in the web UI
A raw single-level string left in localStorage by older versions was
migrated into a '*' fallback entry applied to every model; stale values
(typically max) could silently steer any session. Non-map content is now
discarded instead of inherited.
* feat: drop the web per-model thinking pick store entirely
Thinking in the web UI is now sourced only from the daemon: a session's
own reported level wins when the model declares it, and everything else
falls back to the model's catalog default. The stale localStorage key is
cleaned up on startup.
* feat: skip the effort write when the picker confirms its initial value
Re-confirming the effort shown when the model/effort picker opened is not
an explicit choice: the TUI persists only the model (no effort key, or no
write at all when nothing changed), and the web skips the global config
write the same way it already did for model switches.
* revert: keep the web global thinking write on re-confirm for now
Only the TUI skips the effort write when the picker confirms its initial
value; the web behavior stays unchanged until the interaction is
reconsidered.
* fix: carry the draft thinking pick into a newly created session
A level picked on the empty composer lived only in rawState.thinking, so
selectSession's watcher overwrote it with the catalog default and the
first prompt/skill submitted that instead of the pick. createDraftSession
now captures the draft level and seeds the new session's own entry.
* docs: simplify the changeset wording
* fix: close the remaining top-tier persistence and draft-capture gaps
The provider-add default-model flow now resolves support_efforts through
effectiveModelForHost (overrides + protocol-profile inference), so a
top-tier pick on catalog models without declared efforts no longer
persists. The draft thinking level is captured before the session
creation awaits, so a concurrent session switch can no longer seed the
new session with another session's effort.
* fix: wait for the session status fold before resolving prompt thinking
In the cold window right after a reload or session switch, the session's
own level has not landed from /status yet; resolving straight to the
catalog default would carry the wrong level to the daemon, which writes
it into the session profile. Send, steer, side-chat and skill-activation
paths now await the fold when the session entry is missing.
* feat(kap-server): enable multi-server shared home by default
- always register kap-server instances under server/instances and drop the
legacy single-instance lock (acquireLock/getLiveLock/ServerLockedError)
- remove the multi_server experimental flag and its
KIMI_CODE_EXPERIMENTAL_MULTI_SERVER env var from agent-core-v2
- discover running servers via the instance registry in server
ps/kill/rotate-token, kimi web daemon reuse, and the desktop app
- remove the pending minidb changesets
* feat(cli): add per-instance targeting to server kill and ps
- `kimi server kill [serverId]` stops only the matching instance; without
an id it still stops the longest-running one, and an unknown id errors
with the live server ids listed
- `kimi server ps` lists connections grouped per server id (`--json`
nests them under a per-server object); an unreachable instance degrades
to a per-server note instead of failing the whole listing
- update the zh/en command reference and the multi-server changeset
* feat(cli): replace kimi server with the kimi web command tree
- `kimi web` now runs the local server in the foreground and opens the
browser; the background daemon (ensureDaemon / spawn / idle-exit) is
removed, so repeated runs simply start another instance on the next
free port
- drop the OS-service lifecycle (install/uninstall/start/stop/restart/
status) together with kap-server's svc layer
- `kimi web kill [serverId]`, `kimi web ps`, and `kimi web rotate-token`
manage instances from the registry
- the TUI /web command now connects to an already-running instance
instead of spawning a background daemon
- update the zh/en command reference, dev scripts, and tests
* feat(cli): route kimi server invocations to a deprecation notice
Any `kimi server …` call — bare or with any legacy subcommand/flags —
now prints a deprecation notice pointing at `kimi web` and exits 1,
instead of failing with an opaque "unknown command". The shim is
scheduled for removal in the next major version.
* feat(cli): add the `all` keyword to kimi web kill
`kimi web kill all` stops every live instance in the registry (ULIDs
can never collide with the keyword). Each instance still gets the API
shutdown + SIGTERM/SIGKILL treatment; a failure on one instance does
not stop the sweep and is reported at the end.
* docs(changeset): drop the web-foreground-default changeset
The kimi web command tree replaces the foreground-default behavior this
entry describes: --background, daemon reuse, and the version-mismatch hint
no longer exist, so the pending entry would contradict the actual release
notes.
* docs(changeset): tighten the multi-server entry wording
* feat(cli): let /web pick a running server or start a new one
The /web picker now lists the live instances from the registry with
their versions (flagging a CLI mismatch) instead of only connecting to
the longest-running one, and offers starting a new server: that one
runs in the foreground attached to the terminal after the TUI exits,
via the restored exit-takeover wiring. formatReadyBanner is exported
and adapts its Stop hint to Ctrl+C for the attached case.
* feat(cli): skip the /web picker and start a new server when none is running
* feat(cli): run kimi web and the TUI /web command in the foreground by default
kimi web and /web now start the server attached to the terminal (Ctrl+C
stops it) instead of backgrounding a daemon; --background opts back into
the daemon behavior. kimi server run is unchanged. A foreground kimi web
reuses an already-running server instead of failing to bind, the /web
handoff prints the same ready banner as kimi web (plus the session deep
link), and the banner Stop hint adapts to the hosting mode.
* fix(cli): resolve the server token after the server is up in /web
The token lookup had moved ahead of ensureDaemon()/startServerForeground()
during the foreground-default refactor, so a first-time start (no
server.token on disk yet) opened the browser without the #token= fragment
and left the user at the auth gate. Resolve the token after the daemon is
healthy, and inside the foreground ready hook.
* docs(changeset): flag the web foreground default as breaking in the changelog
The bump stays minor, but the entry now calls out the behavior break and
the --background mitigation for scripts that expect kimi web to return
immediately.
* feat(cli): flag a version mismatch when reusing a server from an older CLI
After an upgrade, an older still-running server is reused as-is. The
lock's host_version is now surfaced: kimi web / server run print a
version-mismatch line next to the reuse notice, /web shows the same as a
status warning, and the docs plus changeset tell users a single
kimi server kill switches them onto the new version.
* fix(tui): correct YOLO and Auto permission mode descriptions
* fix: unify YOLO and Auto permission mode descriptions across CLI, ACP, web, and docs
* docs: correct YOLO and Auto mode descriptions in the interaction guide
* fix: correct YOLO mode notices in session replay and vscode extension
* feat(vscode): rename /afk command to /auto, keeping afk as hidden alias
Also correct the stale 'afk' mode reference in the built-in MCP config
skill guidance of both agent engines.
* fix(vscode): forward engine approval requests instead of blanket-approving them
The extension-level approval handler auto-approved every request when
legacy yolo/afk was on, silently swallowing the sensitive-file,
plan-review, and ask-rule prompts the engine yolo mode still sends.
Forward every request to the user and let the engine permission mode do
the auto-approving, matching TUI and web behavior.
* fix(server): allow inline styles in CSP so web math renders on non-loopback binds
The security CSP fell back to default-src 'self' for styles, which strips
the inline style attributes that KaTeX (injected via innerHTML) uses for
all glyph positioning — formulas collapsed into overlapping characters on
any non-loopback-served web UI. Shiki highlighting and Mermaid diagrams
hit the same mechanism. Add style-src 'self' 'unsafe-inline'; scripts
remain strictly restricted.
* test(server): assert the effective script-src in the CSP regression test
A negative substring check only rejects one exact string: default-src
gaining 'unsafe-inline' (inline <script> allowed via fallback) or an
explicit script-src 'unsafe-inline' would both slip through. Parse the
directives and assert the effective script policy (script-src, falling
back to default-src) excludes 'unsafe-inline'/'unsafe-eval'/data:.
* fix(web): remember the thinking level per model
Persist kimi-web.thinking as a JSON map of model id to level instead of a
single global value, and resolve the active level against the model's
catalog (stored pick when still declared, else the model default) at
loadModels, setModel, and on active-model changes via a watcher.
Fixes the empty, unresponsive thinking picker shown for a model that does
not declare a previously stored level (e.g. a max-only model with a stale
global 'low').
* fix(web): resolve a submitted prompt's thinking from its own model
submitPromptInternal and the steer path read the single active-session
rawState.thinking, so a queue drain for a background session submitted the
level of whichever session the user had switched to since enqueueing —
the same cross-model leak on the submit path. Thinking now joins model and
the per-session modes in being resolved from the prompt's own session
model (its stored pick when declared, else the catalog default), falling
back to the active value only when the model has left the catalog.
* fix(web): keep model switches from persisting derived thinking defaults
setModel routed the resolved level through applyThinkingLevel, which
writes per-model storage unconditionally — a switch to a model with no
saved pick stored the catalog default as if it were an explicit choice,
pinning the user to it across later default changes, and the rollback
path did the same write for a switch that never happened. Model switches
now update the in-memory level only; storage writes stay with
setThinking, the explicit picker path.
* fix(web): resolve thinking per target session on the BTW and skill paths
sendSideChatPromptOn combined the captured parent's model with the
active-session level, so a session switch during the startBtw await sent
the BTW first turn at the wrong model's effort — resolve it from the
parent's own model, falling back to the active value off-catalog, same
as the other submit paths.
activateSkill carries no thinking either, so the daemon ran skills at
the session profile effort, which can predate the per-model restore the
picker now shows. Persist the resolved level to the session profile
first, mirroring the new-session skill path; that path itself now
resolves against the new session's model instead of the raw active
value.
* fix(web): keep per-model thinking picks in memory as the runtime truth
The resolver re-read localStorage on every submission, letting storage —
not the displayed state — decide what the daemon receives: with storage
unavailable (policy/quota) an explicit pick reached the UI while every
submit path fell back to the catalog default, and a pick made in another
tab silently changed what this tab submits mid-session.
Per-model picks now live in an in-memory map hydrated from localStorage
at startup; explicit picks update it first and persist best-effort
(read-modify-write merge, so concurrent tabs' entries still survive).
localStorage is only hydration plus persistence — another tab's pick can
no longer alter this tab's runtime level.
* fix(web): carry the legacy global thinking pick forward as a fallback
Pre-map installs stored a single global level as a raw string; the map
parser dropped it, silently resetting the user's explicit preference to
the catalog default on upgrade. The legacy value is now carried as a
fallback for models without their own entry — validated against each
model's catalog at resolution, so effort models keep the user's pick
while a max-only model still falls through to its default and can never
be trapped by it.
* fix(web): keep the legacy thinking fallback across the first map rewrite
The first explicit pick after an upgrade rewrote the raw legacy value
into a map containing only that one model, so the next reload saw a
nonempty map and dropped the legacy fallback for every other model.
The migrated value now lives inside the map under a '*' key that no
real model id can collide with: per-model entries override it, and
rewrites persist it alongside them instead of deleting it.
* fix(web): persist only the changed thinking pick on write
Overlaying the whole in-memory map on write could revert a newer pick
made in another tab for a model this tab still held a stale copy of.
Write the changed entry alone (delta-style, like saveUnread), carrying
only the migrated legacy '*' fallback along so it survives the first
rewrite into map format.
* fix(web): abort skill activation when the thinking profile persist fails
persistSessionProfile surfaces failures itself and resolves, so awaiting
it never blocked a following activation: a failed /profile write still
launched the skill at the session's stale effort. It now resolves a
success flag; both activation paths (existing session and new-session
draft) gate on it and skip activating when the persist fails, without
reporting a second, synthetic error.
* refactor(web): persist the new-session skill profile's thinking once
startSessionAndActivateSkill persisted the resolved thinking and then
activateSkill persisted it again unconditionally — a redundant profile
update and status refresh whose transient failure would false-veto an
activation whose prerequisite profile was already applied. Thinking is
now written by activateSkill alone (the single, gated writer); the
draft patch carries only model, plan/swarm and permission.
* fix(web): throw an Error instance for the profile-persist sentinel
oxlint --type-aware (only-throw-error) rejects throwing a Symbol; the
identity-based sentinel works the same as a shared Error instance.
* fix(web): resolve an empty session model through the default before skills
session.model can be '' transiently (daemon profile echo), so
activateSkill fell back to the raw active-view level; in the new-session
flow a concurrent switch could persist another model's effort onto the
target session. Normalize '' through the configured default_model first,
same as the prompt/BTW/steer paths.
* fix(kimi-web): stop the prompt queue from ghost-sending stale attachments
Sending while a turn was running queues prompts locally. A failed flush
left entries stuck, and every later session open silently re-submitted
them with their old file attachments. Gate the drain on locally
witnessed turns, re-drive stuck entries FIFO from real events with a
failure budget, restore merged entries on steer failure, persist the
queue per session so a refresh loses nothing, and converge queues
across tabs via storage events.
* fix(kimi-web): merge cross-tab queue updates by entry id (review P1/P2)
Whole-record adoption could silently discard a prompt another tab
enqueued concurrently. Queue entries now carry a stable id and enqueue
timestamp; adoption union-merges snapshots by id, a shared TTL'd removal
set stops flushed/discarded entries from resurrecting (and being
flushed twice), an in-flight marker covers the submit window, and
manual reorders re-stamp timestamps so they survive merges. The flush
failure budget is also tracked per entry instead of per session, so
removing or reordering the head no longer hands its strikes to the
next entry.
* fix(kimi-web): single-flusher queue entries, merge order convergence, forget guard (review round 2)
Turn-end events reach every open tab and the server accepts concurrent
submissions as distinct prompts, so two tabs holding the same adopted
entry could both submit it. Entries now record their owner tab and only
the owner flushes (ownerless legacy entries flush anywhere); an idle
send adopts entries left behind by closed tabs so a stranded queue can
still drain. Cross-tab merges now keep the newer enqueuedAt copy per id
so a manual reorder converges instead of ping-ponging writes, and the
flush failure callback no longer resurrects a queue whose session was
forgotten while the submit was pending.
* refactor(kimi-web): drop the cross-tab queue persistence, keep the minimal fix
Cross-tab queue sync over localStorage is a distributed-systems problem
(claim/lease, conflict merge, ownership) that keeps generating review
findings far beyond this PR's scope. Remove the persistence/hydration/
adoption machinery wholesale; keep the bug fix proper: gated queue
drain, event-driven FIFO retry with a per-entry failure budget, steer
queue restore, and the forgotten-session flush guard. Durable queued
prompts will be designed together with the server-side prompt queue.
* chore: align the changeset wording with the reduced scope
* fix(kimi-web): never duplicate on ambiguous submit failures; advance after drop
Two review findings: (1) restoring merged queue entries after ANY steer
failure could re-submit prompts the daemon had already accepted when the
failure was a lost response — submits now report ok/rejected/uncertain
and restores + flush re-queues happen only on definitive daemon
rejections, while ambiguous failures drop the entry (the failure toast
still tells the user); (2) dropping an exhausted queue head no longer
strands the entries behind it — the new head is submitted immediately,
carrying its own failure budget.
Models occasionally stream whitespace-only thinking (e.g. a single
space). It starts a thinking draft that renders as a bare bullet line,
both while streaming and when replaying session history. Skip
whitespace-only thinking deltas before they create a draft, and skip
whitespace-only think text at the component funnel so stored
whitespace think parts never render on replay. Stored thinking is
still replayed verbatim to the model.
* feat: refresh model list for API-key providers at the managed endpoint
* docs: simplify changeset wording
* docs: add kimi-code/k3 to the default config example
* fix: clear stale default model when a refresh drops its alias
* fix: clear refresh defaults via replace; set(undefined) cannot delete
* docs: drop the auto model refresh paragraph from providers page
* docs: simplify changeset wording
* feat(tui): add /copy slash command to copy the last assistant message
The command copies the latest assistant reply (text parts only, skipping thinking, tool-call-only turns, error and internal messages) to the clipboard. Clipboard writes now also emit an OSC 52 sequence (tmux-aware) so copying keeps working over SSH and in containers where no native clipboard tool exists.
* fix(tui): gate /copy to idle and report OSC 52-only copies as unverified
During streaming the in-flight assistant text is not in getContext() history yet, so /copy would silently copy an older message; make the command idle-only like /export-md. copyTextToClipboard now returns how the text was delivered so /copy can say when only an unverified OSC 52 escape carried it.
* fix(tui): source /copy text from the visible transcript
After /compact the model context keeps only user messages plus a user-role summary, so scanning it found no assistant message even though the last reply is still on screen. Read the last assistant transcript entry instead — it matches what the user sees and survives compaction and resume.
* fix(tui): mark real model text in the transcript so /copy skips synthetic cards
Hook-result and goal-completion cards are also 'assistant' transcript entries appended after the real reply, so /copy could copy a hook card instead of the answer. Tag the single entry-creation site for genuine model text (both live and replay flow through it) and have /copy only accept tagged entries.
* feat(minidb): add ClusterDb sharding and harden engine under stress
Cluster layer:
- add ClusterDb: hash-routed keys over N MiniDb shard directories with a
per-shard lock pool (lease renewal, lockHoldMs yield, takeover on dead PID),
merged ordered scans, cross-shard index registry, live cross-process read
visibility, and crash-recovery handoff
- add cluster bench suite and multi-process test suites
Engine hardening (stress-driven fixes):
- compaction: pre-copy now gives up when the tail copy is not converging and
rotation seals the old WAL (retryable), so compaction always terminates
under sustained write storms and no committed write slips through rotation
- recovery: re-sync WAL size bookkeeping after torn-tail truncation; read-only
opens create/modify no files and never compact under a live writer
- lockfile: stale-lock takeover via atomic bid-rename + settle; release only
unlinks its own pid
- query: streaming candidates with skip/limit applied before materialization,
plus Store.rawKeys for value-free key scans
- eviction: O(1) LRU victim picking via insertion-ordered access set
- TTL: adaptive expire budget drains simultaneous-expiry storms in seconds
- store: O(1) size fast path when no TTL is set; has() no longer materializes
disk-backed values
- openOrRebuild preserves data when only a sidecar definition file is corrupt;
sidecar definitions written atomically; stale compaction temps cleaned on open
- RESP server serializes replies per connection; over-64KiB tokens can no
longer poison the full-text index
* feat(minidb): incremental WAL catch-up for cluster readers
- cluster readers: track a per-shard WAL watermark (dev/ino/offset) and
catch up from appended frames instead of fully reopening; fall back to a
full reopen on rotation, truncation, or index-definition changes
- MiniDb.catchUpFromWal applies WAL tail frames to a live instance (store
plus secondary/dt/compound/text indexes), sharing recover()'s frame
interpretation; RecoveryInfo exposes walScanEnd/dev/ino as the safe anchor
- lock-pool: incremental refresh with stats (incrementalCatchups,
catchupFramesApplied); bench/reader-catchup shows p50 read latency drop
from 130ms to 0.4ms at 10k keys and from 532ms to 0.4ms at 50k keys
while a neighbor process writes
- compaction: keep the db writable on rotation failure (fresh WAL swap,
remap once); count stats.compactions only on full success including the
onCompacted hook
- restoreKey: seq guard so a failed op never wipes a concurrently
committed value; eviction DELs retry on WAL seal
- text index: atomic build (stage then swap), createTextIndex registers
only after a successful build, open-time cleanup of db.text-*.postings.tmp
- RESP server: swallow per-connection socket errors, reset the parser
buffer after an oversized request, isolate per-command errors while
preserving reply order
* chore(minidb): add changesets for reader catch-up and review hardening
* fix(minidb): support Windows in WAL rotation and lock takeover
- compaction rotation: on Windows, renaming over an open destination is
EPERM, so replace renames with a retrying renameReplace helper and let
go of the db's own ValueReader handles for the renames (reopened right
after the pointer remap)
- value reader: hold snapshot/WAL handles only in valueMode 'disk'; in
memory mode the handles were idle and, on Windows, blocked rotation
- lock takeover: retry the takeover bid's rename on Windows, but re-check
the corpse before every attempt — a blind retry loop could land our bid
late and overwrite an already-verified winner, double-holding the lock
- lock takeover: raise the co-bidder settle window to 50ms so loaded CI
machines with tens-of-milliseconds descheduling still elect one winner
- test hardening for shared CI runners: a 30s minidb-wide timeout floor,
batched prefills instead of sequential setup loops, explicit timeouts
for process-spawning and heavy e2e tests, and Vitest 4 test() signature
normalization
Verified green across macOS (arm64), Windows Server 2022 (x64, 2-core),
and Ubuntu 22.04 (launchpad aarch64): 321 passed, 1 skipped in each.
* fix(minidb): make stale-lock takeover exactly-one under CI load
- takeover now registers a liveness watch file before touching the lock,
so every contender is visible to every other for the whole attempt;
the settle/verify loop abstains while any live foreign watch exists.
Settle-only heuristics could not survive a bidder descheduled before
its bid write on shard-parallel CI runners (observed double-holds on
ubuntu-latest and on a 2-core Windows box).
- adaptive settle scales with the attempt's own wall clock (floored,
capped), replacing the fixed window.
- keep the Windows EPERM tolerance in the bid rename, re-inspecting the
corpse before every attempt on ALL platforms, not just win32.
- lint: fix restrict-template-expressions in an e2e RESP helper.
- test: widen the cluster wait-read budget for slow CI spawns.
* fix(minidb): clean remaining CI lint and consumer-test failures
- wrap the compaction-storm error interpolation as String() so the
type-aware restrict-template-expressions lint passes
- agent-core-v2's minidb query-store corruption test now expects the
intended semantics: a corrupt index-definition sidecar is dropped while
the data survives, and the definition can be re-registered
* fix(minidb): harden ClusterDb index administration across processes
Address three multi-process administration races in the cluster layer:
- findRange now merges candidates from all shards first and only then
applies reverse/offset/count globally, instead of clipping per shard
and discarding reverse
- cluster.indexes.json mutations go through a compare-and-swap loop
(reload, re-apply idempotently, publish, verify) with an in-process
mutex, so concurrent create/drop from two processes loses neither
registry entries nor shard sidecars
- a failed createIndex/createTextIndex fan-out now rolls back exactly the
shards it already created on, so the registry and every shard agree
whether an index exists
Also make LockFile sidecars (tmp/bid/watch) unique per acquire attempt:
two lock users in the same process (independent shard pools) must never
share a path, or one user's cleanup would delete the other's in-flight
file.
* feat(kimi-inspect): add web inspector for kap-server /api/v2 surface
- new apps/kimi-inspect app: connect screen (server URL + optional bearer
token, persisted in localStorage, deep-linkable via ?url=/?token=),
workspace/session browser sidebar, per-session chat view, and live
Service panels with data and trigger buttons for Session/Agent scopes
- built on @moonshot-ai/klient (HTTP for calls, /api/v2/ws for events);
Vite dev server proxies /api to a running kap-server
- register the workspace in AGENTS.md project map and flake.nix
workspacePaths/workspaceNames
* fix(kimi-inspect): align dependency versions with the workspace (sherif)
* feat: dev /api/v1/debug RPC surface and kimi-inspect channel rework
- kimi-inspect: replace @moonshot-ai/klient with an in-app old-klient-style
channel layer (service-bound IChannel, HTTP ProxyChannel, shared /api/v2/ws
socket with ref-counted event listens), typed by agent-core-v2 interfaces;
/channels descriptors + serviceByName keep every wire protocol loaded 1:1
- kimi-inspect: local server auto-discovery (Vite middleware over the
kap-server instance registry + home token), zero-config startup connect,
and a header switcher for runtime server switching
- kap-server: wire the dormant --debug-endpoints flag to a new
whitelist-free /api/v1/debug dispatcher (every scoped service callable),
gated to loopback binds; repo dev scripts pass the flag
- kimi-inspect: probe the debug surface at connect, falling back to /api/v2
on servers without it
- tests: channel + discovery unit tests in kimi-inspect; debug RPC and
loopback-gating coverage in the kap-server rpc/debugNonloopback suites
* chore(changesets): ignore @moonshot-ai/kimi-inspect
The private dev app never ships, so it should never appear in a changeset.
Add it to the changeset config ignore list (next to vis*) and note the rule
in the gen-changesets skill.
* fix(workspace): dedupe workspaces across Windows path spelling variants
The same directory reached the workspace registry as distinct strings on
Windows (drive-letter casing, typed vs on-disk casing, slash style), and
every identity check compared exact strings, so one folder could appear
as multiple workspaces with sessions split across hash-keyed buckets.
- add workspaceRootKey (slash-normalize + case-fold Windows-shaped
paths) in agent-core, agent-core-v2, and the web app, and compare
roots by identity key everywhere instead of exact strings
- registry createOrTouch folds alias spellings onto the existing entry
instead of minting a new workspace id; session buckets reuse the
registered id via a resolver in the v1 session store
- list endpoints expand alias buckets (resolveAliasIds /
resolveAliasWorkDirs, including session-index-only spellings) so
previously split workspaces list all sessions and counts under one
merged group; session_index entries use the registry-resolved id
* fix(workspace): fold the runtime touch path and drive-root identity keys
Two gaps in the Windows path-spelling folding, both reachable in the
v1 session-create flow:
- touchWorkspaceRegistry minted the alias spelling's id outright; the
freshly persisted alias entry then became the resolver's preferred id
on the next create, splitting sessions into a duplicate bucket again.
It now folds onto the identity-matching existing entry, mirroring the
registry service.
- workspaceRootKey stripped trailing separators before testing the
Windows shape, so a drive root (C:\) collapsed to C: and escaped the
case-fold. The shape test now runs before the strip in all three
copies (agent-core, agent-core-v2, web).
* fix(workspace): unfold symmetric operations that escaped the identity key
Two asymmetric spots left the folded comparison one-sided:
- the web app matched hidden roots by folded key but cleared them on
re-add by exact string, so hiding C:\Foo and re-adding c:\foo kept
the workspace hidden forever; clearing now folds too
- registry delete (both engines) removed and tombstoned only the exact
id, so a legacy split sibling resurfaced as the directory's
representative on the next list; delete now removes every registered
spelling sharing the root's identity key and tombstones the full
alias set (registered ids plus session-index spelling mints), so the
session-index merge cannot resurrect the directory either
* fix(security): close FetchURL SSRF bypasses and DNS-rebinding window
- resolve hostnames via DNS and reject any target resolving to loopback /
RFC1918 / link-local / CGNAT / ULA ranges, including IPv4-mapped IPv6
forms (e.g. localtest.me, [::ffff:7f00:1]) — the static host denylist
only matched literals and could be bypassed by crafted domains
- follow redirects manually with full per-hop revalidation (10-hop cap)
instead of auto-following, so a public URL cannot 302 the fetcher at
internal services or cloud metadata endpoints
- pin each connection to the DNS answers the check validated (per-hop
undici Agent with a pinned lookup), closing the TOCTOU / DNS-rebinding
window between the check and the connect; skipped when a proxy is
configured or allowPrivateAddresses is set
- apply to both agent-core and agent-core-v2 providers, with SSRF /
redirect / pinning test coverage
* chore: add changeset for FetchURL SSRF hardening
* test: bridge undici type declarations in fetch pinning tests
* fix(security): keep dispatcher option lib-agnostic for DOM typecheck consumers
* fix(security): address review — pin NO_PROXY bypasses, drain oversized bodies, header-only comments
When the provider stream breaks off mid-tool-call (pause_turn, engine
overload, token limit), the step ended with the partial tool call dropped
silently: never executed, never recorded. If the response carried no other
usable content, the persisted assistant message was effectively empty and
every subsequent request — including compaction — was rejected with
"assistant must not be empty" (HTTP 400), wedging the session.
Record each unexecuted call instead (arguments sanitized to {} when the
truncated JSON is unparseable) and immediately close it with a synthetic
interrupted error result. The exchange stays wire-valid, the history stays
truthful, and the model learns the calls never ran so it can re-issue them.
Both engine implementations defaulted the export ZIP path to
<sessionId>.zip, so running /export-debug-zip or kimi export twice for
the same session silently overwrote the first archive. The default
filename is now kimi-debug-<shortId>-<timestamp>.zip (UTC, second
precision), matching the /export-md naming convention. Explicit
-o/outputPath behavior is unchanged.
- SessionMetadata.registerAgent now compares the incoming metadata with
the persisted entry (tolerating persisted-JSON artifacts: dropped
undefined keys, null parentAgentId, label order) and skips the write
entirely when nothing changed
- previously the first resume per server process rewrote state.json via
the status rollup's ensureMainAgent -> doCreate path, bumping
updatedAt and pushing the session to the top of the list
* fix(agent-core-v2): keep context size readings on the measured path
The step fold creates the assistant message in the context before the
exchange finishes (a skeleton at step.begin, filled by content.part folds
during streaming), and the input array llmRequester passes to
contextSize.measured() is that same live array — it already includes the
output. Taking input.length + output.length therefore counted the folded
output twice, storing a measured prefix length one past the live context.
The inflated length permanently failed get()'s measured fast path, so
reads silently fell back to per-message estimates (e.g. ~50 tokens shown
for a ~29k-token "hi").
Take the live context length as the measured prefix instead (input and
context are identical under the identity guard), and clamp the measured
prefix to the context length in get() so a bad record can never knock
reads off the measured path again.
Add contextSize tests driving real turns that assert the wire model,
get(), and rpc getContext against the exchange totals.
* feat(klient): add context-usage example tracing a fresh session
Polls agent.getContext()/agent.getUsage() and streams agent events for a
new session after one "hi" against a real server, printing a timeline of
when the context/token readings move, plus a final consistency check
comparing the measured tokenCount to cumulative usage. Model seeding is
optional via KIMI_EXAMPLE_* env; the server token resolves from
<kimi-home>/server.token like the v2 e2e helpers.
* chore: add changeset for the context size fix
* fix: scope Anthropic effort fallback profile to non-Kimi providers
Managed Kimi models routed through the Anthropic protocol (protocol =
"anthropic", no catalog-declared think_efforts) inherited the inferred
latest-Opus effort profile, so the UI showed reasoning effort choices the
server never declared. The fallback now applies only when the provider type
is known, non-kimi, and the effective wire protocol is Anthropic; Kimi
providers keep catalog-declared efforts only, and callers without provider
context fall back to name matching.
* fix: align v2 catalog with resolver for providerless Anthropic models
Flat models (inline base_url, no named provider) and providers without a
declared type now fall back to the model's own protocol when deciding the
Anthropic fallback effort profile, so the model catalog stays consistent
with runtime resolution.
* fix: keep flat Anthropic model effort metadata in TUI and ACP catalogs
Flat models without a named provider (inline base_url, protocol:
"anthropic") have no provider entry to look up; fall back to the
model's own protocol as the provider identity so the effort picker and
ACP catalog stay consistent with runtime resolution.
* style: move v2 anthropic fallback rationale into the modelAuth header
The v2 comment convention keeps comments in the top-of-file block only;
drop the inline explanations added beside functions and statements.
* feat(vscode): migrate extension to Node SDK
* fix(vscode): address CI failures
* fix(vis): handle token count records
* fix(vscode): keep chat toolbar and header readable at narrow widths
* fix(vscode): map yolo to core yolo permission and honor the global yolo setting
* docs(vscode): record Node SDK migration design
* docs(vscode): split breaking changes out of the 0.6.0 changelog
* fix(vscode): keep a resumed session's thinking effort instead of reapplying the default
* fix(vscode): announce session status when a view attaches so the display matches it
* fix(vscode): align webview thinking effort handling with the TUI
* feat(agent-core): align coder subagent tools with v2 and drain background tasks
- align the bundled coder subagent profile tools with agent-core-v2
CODER_TOOLS (Skill, Agent, AgentSwarm, Task* trio, plan-mode tools,
TodoList); cron tools stay declared-but-undelivered on sub agents,
matching v2
- rebuild builtin tools in setActiveTools: profile-gated capabilities
(Bash/Agent allowBackground via the Task* trio) were baked at
construction with an empty enabled set, so profiles applied later
(every subagent) silently lost them
- hold subagent completion until the child agent's background tasks
settle (print-mode drain semantics) and suppress their terminal
notifications, so no unobserved follow-up turn runs on a finished
subagent; the run's timeout/cancel signal bounds the drain
- cover with real-Session e2e (tool execution, nested Agent/AgentSwarm,
drain blocking and cancel) and update profile/subagent-host tests
* chore: add changeset for coder subagent tool alignment
* docs(agents): sync sub-agent capabilities with expanded coder tool set
- coder sub-agents can now dispatch nested sub-agents and use background
tasks, todo lists, Plan mode, and skills; drop the stale claim that
sub-agents cannot schedule nested sub-agents
- note that a sub-agent run reports completion only after its background
tasks settle
* fix(agent-core): close drain race for tasks settled before completion
A background task that terminated before the drain's suppression pass was
excluded from the active-only list, so its terminal notification escaped
suppression and could still steer an orphan turn onto the finished
subagent. Suppress every child task (including settled ones whose
notification may still be in flight) and run the pass both before and
after the settle wait; notification delivery re-checks suppression after
its async output snapshot, which makes the block deterministic. Cover the
mid-turn delivery path with an e2e case.
An explicit withThinking('off') collapsed to the same internal state as
"never configured" on chat-completions providers, so the history-based
auto reasoning_effort injection (#1616) silently switched reasoning back
on and could leak the field to models that reject it. Store the requested
effort verbatim and derive the wire encoding per request, suppress the
auto-enable for an explicit 'off', and report the accurate current effort
('on'/'off') instead of recording 'off' for both.
Remove the wire-level fallback that substituted a space for empty
thinking content when replaying assistant history to Anthropic-compatible
and Kimi endpoints with thinking keep=all. The strict endpoints that
motivated the placeholder no longer reject empty thinking, so the
placeholder only distorted replayed history.
The klient facade PR landed with its release notes planned for a later
feature release; removing the two pending changesets so the next release
does not pick up version bumps and changelog entries for it.
- awaitRun: cancel by turn id so aborts also reach turns still queued, not just the active turn
- awaitTurn: cancel first, then wait for turn.result instead of racing against the abort signal, so task settlement, the task.killed notification, and the resume guard never observe the run as finished while the loop is still unwinding
- rethrow the original abort reason so consumers keep matching it by identity
- add regression test for the manual stop -> auto-resume "already running" race
* feat(klient): contract-driven facade with http/ipc/memory transports
- add zod-validated contract sections (global/session/agent) under
src/contract and a facade exposing global.*, session(id).*, agent(id).*
- select transport once at creation via subpath entries
(@moonshot-ai/klient/http|ipc|memory); drop legacy channel/client/
httpChannel/wsChannel/wsKlient/proxy implementations
- absorb packages/server-e2e into packages/klient test/e2e suites
(dual-backend, legacy v1, v2 wire) and remove the server-e2e workspace
- expose model registry and catalog services on kap-server v2 RPC surface
* fix(klient): derive session status from agentActivityView
The engine retired its sessionActivity service in #1751 (session busy is
now derived from agent activity views), but the facade still called the
deleted wire channel and imported the deleted engine module, breaking
typecheck and every klient suite at import time.
- drop the sessionActivity contract/registry entries and mirror the
agentActivityView service instead (agent scope)
- compose session status() client-side from the pending interaction lists
and each agent's agentActivityView, keeping the retired service's
precedence and typing SessionStatus locally in the facade
- replace the deleted-channel call in the v2 smoke suite with a
sessionInteractionService probe
- fix the legacy image-file suite to wait with the harness's
waitForSessionBusy
* fix(web): align context usage display with 1024-based units and ring-only meter
- simplify the composer context meter to the ring only; the full
used/max/pct numbers live in the tooltip
- format token counts with 1024-based k/M units via a shared formatTokens
helper (256k context reads "256k", not "262k"), applied to the composer
tooltip, status panel, mobile settings sheet, model picker, goal strip,
and turn rendering
- ceil the usage percent so sub-0.5% usage still shows a sliver instead
of an empty meter
* fix(tui): render context usage with 1024-based units and ceil percent
- formatTokenCount is now 1024-based ("256k", not "262.1k"); the footer,
/status and /usage panels, subagent cards, and goal stats all share it,
replacing five local 1000-based copies
- the footer and panel percents use an integer ceil (new usagePercent
helper) so any non-zero usage shows at least 1% instead of "0.0%"
* fix(web): clamp the status panel context percent to [0,100]
ctxUsed can momentarily exceed ctxMax (estimates), which could flash a
"101%" readout — the composer and mobile sheet already clamp the same
ConversationStatus data, so apply the same clamp around the ceiled
percentage here.
* chore: merge the context usage changesets into one
* chore: reword the context usage changeset in English
- kap-server: remove event.model_catalog.changed from the v1 WS union,
broadcaster forwarding, and the zod event registry
- web: refresh all providers (POST /providers:refresh) before loading
models when the model picker opens, replacing the event-driven refresh
- keep domain publishers, the protocol schema, and the web receiver for
compatibility with older daemons
* feat: warn about context-cache loss when switching model or thinking effort
* chore: polish the switch warning copy
* feat: wrap the switch warning instead of truncating it
* chore: bold /new in the switch warning
* Revert "chore: bold /new in the switch warning"
This reverts commit a438e87cda543202a6d4726c96cdabd73b8dcf6f.
* chore: align the changeset with the warning copy
* fix: record crash telemetry for unhandled promise rejections
The crash handler only listened on uncaughtExceptionMonitor, which never
fires for a rejection that has a listener — and the TUI always registers
one, converting rejections into a silent exit(1) with no telemetry at
all. Observe unhandledRejection directly so those crashes still leave a
trace. Since registering a real listener suppresses Node's default
crash-on-rejection, rethrow when we are the only listener (print /
server modes), deduped so the monitor does not double-report.
* fix: fall back to text paste when the clipboard image handler rejects
The Ctrl+V image-paste dispatch chained off the async handler without a
rejection branch, so any failure inside it became an unhandled
rejection — which the CLI's crash path turns into a silent exit(1).
Treat a rejection like "no image available" and paste as text.
* fix: dedupe all rethrown rejection reasons at the crash monitor
Non-Error rejection reasons (plain objects, strings, null) were recorded
by the rejection handler but not added to the dedupe set, so the
uncaughtExceptionMonitor pass after the rethrow reported a second crash
for the same failure; null/undefined reasons also crashed the monitor's
own error-type extraction. Use a Set so primitives dedupe by value, add
every rethrown reason, and classify monitor crashes null-safely.
The TUI crash path (uncaughtException / unhandledRejection) logged the
error into an asynchronously-drained sink and then called process.exit()
on the same tick, so the one line explaining the crash was never written
to disk. Flush the diagnostic logs synchronously before exiting.
Surface the coded provider error the daemon already sends: a semantic
title per error code, the provider's raw message, and expandable
diagnostics (error code, HTTP status, request ID, SDK error name) with
copy support, instead of a bare text-only toast.
* fix(web): dedupe background subagent rows in the agents dock
* fix(web): seed agent identity on late task registration and prefer REST output in task fold
* fix(web): backfill terminal output to folded background subagent rows
* fix(web): sync subagent phase when the REST fold makes a row terminal
* fix(kap-server): fail closed on percent-encoded API paths in auth bypass
The auth hook checked the raw, still percent-encoded request URL against
its bypass policy, while find-my-way matches routes against the decoded
path. A raw /%61pi/v1/... URL therefore skipped the bearer-token check
yet still reached the /api/... handlers, allowing unauthenticated access
to every API route.
Decode the request path the same way the router does before matching,
and fail closed when the path cannot be decoded. Add tests covering
encoded /api/ paths, encoded meta documents, the encoded healthz probe,
and unaffected non-API bypasses.
* fix(agent-core-v2): make session fs path confinement symlink-aware
SessionFsService confined paths only lexically, so a symlink planted
inside the workspace could steer read, list, stat, mkdir, resolvePath
and resolveDownload to host files outside the session directory.
- add IHostFileSystem.realpath (Node fs.realpath semantics) and
implement it in the node-local HostFileSystem
- resolveWithin now re-verifies each candidate through realpath of the
longest existing prefix (so not-yet-created paths still work) and
rejects paths escaping the canonicalized workspace roots, which are
resolved once and cached
- cover symlink escapes for every fs entry point plus kap-server
read/download e2e; in-workspace symlink targets remain allowed
* fix(agent-core-v2): accept workspace roots given through a symlink
WorkspaceRegistryService.createOrTouch probes the root with the
lstat-based IHostFileSystem.stat, so a root given as a symlink to a
directory reported isDirectory=false and session creation was rejected
with fs.path_not_found ("not a directory").
Re-check a non-directory root through IHostFileSystem.realpath before
failing, while keeping the workspace identity lexical (v1 deliberately
never realpaths the root either). Covered by a unit test for a
symlink-form root and a kap-server e2e that creates a session with a
symlinked cwd and reads a file through it.
* feat(web): allow attaching any file type in chat
- Composer, paste, and drop no longer filter out non-media files;
arbitrary files upload as generic icon chips and are submitted as
file content parts
- kap-server materializes file parts into the session's attachments
dir and replaces them with a path reference, so the model opens the
file with the Read tool on demand instead of receiving inline bytes
- Images rejected by the provider format gate (SVG, AVIF, ...) are now
persisted and referenced by path instead of being dropped with a
notice; uploaded file names are sanitized before hitting disk
* fix(server): stop CSP from blocking web bootstrap script and fonts
- Move the anti-FOUC bootstrap from an inline <script> in index.html
to /boot.js: CSP 'self' never covers inline scripts, while a classic
same-origin script keeps the same render-blocking timing
- Allow data: in font-src — KaTeX and the Inter / JetBrains Mono
Variable fonts ship @font-face data URIs in their distributed CSS
- Set explicit form-action, base-uri, and frame-ancestors, which do
not fall back to default-src
- Add a regression test asserting the served index.html carries no
inline scripts or inline event handlers
* fix(web): normalize empty attachment MIME so extensionless files submit
Files with an empty File.type (Makefile, LICENSE, other extensionless
or unknown types) stored mediaType: '' on the chip, and the submit
fallback used ?? which does not catch empty strings — the wire schema
requires a non-empty media_type, so the prompt was rejected. Normalize
to application/octet-stream at attachment creation, adopt the
server-recorded MIME after upload completes, and make both submit
mappings use || so reloaded chips with '' are covered too.
* feat(web): render all user-turn attachments as chips
* feat(web): attach files by dropping them anywhere in the window
* refactor(web): share one attachment chip between composer and chat bubble
* fix(web): neutral attachment chips, paperclip attach icon, and clickable file chips
* fix(web): drop the extension badge from attachment chips
* fix(web): use the tabler paperclip for the attach button
* fix(web): whitelist attachment previews, reject active document types
Clicking a file chip navigated a new tab to a blob: URL of the uploaded
bytes whenever the type looked browser-renderable. blob: inherits the
web origin, so a text/html or image/svg+xml attachment would execute
same-origin script with the daemon credential (localStorage) and a live
window.opener.
Preview is now restricted to inert types (pdf, non-SVG images, video,
audio, non-HTML text), the blob is re-wrapped with the whitelisted MIME
instead of trusting the recorded content-type, and window.opener is
severed. Non-whitelisted types no longer silently download: the chip
reports 'unsupported' and the pane shows a transient hint.
* fix(web): recover file attachment chips from the server notice
The kap-server prompt route replaces file parts with an "Attached file
…" text notice before enqueueing, so after any snapshot resync the
attachment chip degraded into raw notice text leaking the absolute
server path — unlike image/video uploads, which already recover their
chip from the <video|image path> tag. Parse the notice the same way:
the materialized basename carries the file id, so the chip becomes
clickable again (and editable back into the composer); inline-base64
notices (content-hash named, no file id) still collapse into a
non-clickable chip instead of raw text.
The notice wording is now a client/server contract — flagged on
buildAttachedFileNotice.
* fix(web): preserve UUID file ids when rebuilding attachment chips
* fix(web): skip unresendable file chips when loading attachments for edit
---------
Co-authored-by: qer <wbxl2000@outlook.com>