* feat(kap-server): reclaim idle sessions
* test: trim idle session reclaim test duplication
* test: fix idle reaper fake typing
* fix(kap-server): preserve session state during idle reclaim
* fix(kap-server): resume cold sessions for tool routes
* fix(kap-server): report failed WS session resumes
* refactor(agent-core-v2): let features declare session idle retention
Move the cron, goal, and lifecycle-hook reclaim guards out of SessionManager
into a SessionRetentionContribution collection that each feature contributes,
check retention once under the session lease, and drop the dead optional
task-service branches in the tasks route.
* refactor(agent-core-v2): measure session idle from the activity view
* refactor(agent-core-v2): reclaim idle sessions silently instead of retaining hooked sessions
* fix(kap-server): only resume explicitly requested sessions in tool routes
* refactor(agent-core-v2): inline the cron idle-reclaim guard and stop retaining active goals
* fix(kap-server): reject invalid idle reclaim timeouts
* fix(kap-server): preserve rating evaluation across idle reclaim
* test(kap-server): trim idle reclaim tests
* fix(kap-server): validate idle reclaim config early
* refactor(agent-core-v2): rename the reclaim resume source and derive lifecycle literal unions
* refactor(agent-core-v2): keep the reclaim lifecycle tags off the public lifecycle interface
* fix(agent-core-v2): drop the reclaim marker once a session is archived
* Implement session unload after 30 minutes of idle time
Updated session management to unload sessions after 30 minutes of inactivity for both web and desktop applications.
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(agent-core-v2): reclaim sessions with exited terminals
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* ci: release packages (#4288)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* revert: remove default workspace session creation (#4295)
(cherry picked from commit 83c7909be9)
* chore: merge release/2026-10-20 into release/2026-10-21 (#4294)
Bring the cloud-gated model switch confirmation (#4286, #4292) and the
queued /compact (#4276, #4279) onto main via the release back-sync.
The two changesets that rode along with the original squash
(.changeset/model-switch-confirm-dialog.md,
.changeset/queue-compact-command.md) are excluded here: they were
consumed by the 2.3.0 release and must not be re-consumed on main.
(cherry picked from commit 3f80896a79)
* chore: sync web dist from code-app (#4304)
code-app: 913137036c35c608f3a487eb2c7b4a8815b53b95
The stray .changeset/web-interaction-polish.md carried by the dist
payload is excluded: it was consumed by the 2.3.0 release and never
existed on main.
(cherry picked from commit d36071ede6)
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: qer <wbxl2000@outlook.com>
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
* fix(transcript): realign rebuilt turn ordinals to wire ids and mark wedged turns
Cold transcript rebuilds numbered materialized turns 0..N while the live
projector assigns ordinal=wireId, so a session rebuilt after a restart got a
shifted axis that no longer aligned with what clients had cached live, and a
turn whose loop died before writing a terminal event was rebuilt as a fake
completed turn with no timestamps.
foldWireRecordFacts now reassigns every matched rebuilt turn to its wire turn
id (leaving holes for folded turns), restores startedAt from the
agent.turn.started / turn.prompt records, and marks a turn with no terminal
event as wedged (state failed + wedged flag) once a later turn prompt exists
in the wire; the tail turn is only wedged when it is not the live active one.
The live projector closes the previous running turn as wedged when a new turn
starts. The wedged marker is an additive optional field so released clients
keep parsing transcripts and render the turn as failed.
* feat(agent-core-v2): surface silent notification-delivery and persistence failures
The wedged-turn incident showed a main agent loop can stall for hours with
zero diagnostics: task notifications pooled in memory, a latched or hung wire
flush swallowed every subsequent record, and the running turn produced no
watchdog output.
- AppendLogStore warns once when an append is accepted while a storage
failure is latched, and warns when a flush stays pending past 30s
(repeating every 10min) with scope/key/pending counts.
- WireService routes append failures to ILogService in addition to
onUnexpectedError so they land in the daemon log.
- AgentLoopService arms a per-turn watchdog that warns after 10min without
observable progress (steps, streaming partials, machine phase), including
pending notification/pause/queue state; loop.notify and nudge consumption
log buffered/drained counts at debug level.
- AgentTaskService warns when lifecycle guards silently drop task
notifications or terminal events, and debug-logs in-process redelivery
suppression.
- EventDispatcherService warns once per episode when dispatch is stuck for
over 30s and events keep queueing behind it.
* test(agent-core-v2): quiesce the spawned notification turn before counting leaked timers
With the loop turn watchdog armed, a notification-driven turn that happens
to be running at the assertion holds a legitimate interval, making the
global timer count scheduling-dependent. Cancel any spawned turn and pump
the clock until the loop is idle before counting.
* chore: add changeset for the wedged-turn transcript fix
* feat(agent-core-v2): add cross-process session occupancy lock
Multiple clients (TUI / kap-server / VS Code / ACP) running core in
separate processes could open the same session and corrupt each
other's wire.jsonl and metadata. Each session now holds a cross-process
occupancy lock ({homeDir}/locks/sessions/{id}.lock) taken only while
writing: runs acquire it on submit / compact / undo / cron / delete /
fork / cold-archive, the append log holds it before flushing, and a
stale check compares the main agent wire's on-disk length with the
length this process knows.
Collisions surface as session.occupied (holder in details); writes by
another client detected after acquire surface as session.stale and the
caller reloads before continuing. kap-server maps them to
SESSION_OCCUPIED (40903) / SESSION_STALE (40930), reloads the session
on stale, and re-attaches WS subscriptions; the TUI shows an occupancy
dialog (retry/cancel) or reloads and replays history keeping the input;
print exits with code 4 on occupied and reloads automatically on stale;
VS Code / ACP get minimal occupancy notices. Locks are released a few
seconds after the session goes idle, on close, and on process exit;
locking I/O failures degrade to unlocked operation with a warning.
* docs: document session occupancy error codes and lock directory
* fix: address codex review on session occupancy lock
- metadata: re-read state.json on the first write of each lock-hold
epoch (holdToken), so pure-metadata changes made by another client
(title, archive) are not clobbered by turn-end lastPrompt writes
- lifecycle: hold an explicit lease across live archive(), matching
delete(), so the idle release timer cannot drop the lock mid-teardown
- tui: route the occupied dialog's Cancel selection through the same
cleanup as Esc (restore input, release recalled media)
- acp: tell stale-session users to close and reopen the session
- append-log: warn when retirement drops records kept in memory because
the session lock was unavailable
* fix: always run the staleness check in beginSessionRun
The prompt channels (AgentPromptChannel.submit/submitSteer, kap
/prompts, promptWithSkills) write prompt metadata before calling
loop.submit, and the metadata write acquires the session lock first.
beginSessionRun then short-circuited on 'already held' and skipped the
wire-length staleness check, so the main prompt path never detected
updates written by another client. Drop the already-held shortcut: the
check is a single stat and always passes while the lock is continuously
held, so every acquisition path now runs it regardless of order.
* fix(print): preserve occupied handling on the stale retry
If another client acquires the session between the reload and the
retry, the retried turn now goes through the same conflict handler:
session.occupied reports to stderr / stream-json and exits with code 4
instead of bubbling out as exit code 1.
* fix: do not mistake own in-flight writes for external modification
storage.append grows the file (writeFile) before fsync/close complete,
and knownLength only catches up afterwards; a beginSessionRun landing
in that window (steer, queued prompts, cron fires during a turn) was
misjudged as session.stale and deleted the lock file mid-turn. Track
in-flight bytes per log and compare disk against known + in-flight.
Cover the analogous rewrite window (post-rename syncDir) with an
expected-final-length marker.
* fix: bound the staleness comparison and check once per lock hold
The equality check disk !== known + inFlight moved the false-positive
window to before the append lands (mkdir/open) and into chunked
writes, so beginSessionRun during a turn (steer, cron, queued prompts)
could misjudge, delete the lock file mid-turn, and later append behind
another client. Compare known <= disk <= known + inFlight instead, and
evaluate the check only on a fresh hold (holdToken epoch): re-entrant
runs pass through without a stat, matching the RFC's 'already held
passes through' while every acquisition path still triggers the check
exactly once per hold.
Also unify the occupancy prompt copy across clients: the engine error
message is now the single user-facing text ('该会话正在被其他 Kimi
Code 进程使用;请停止其他进程中的会话或退出该进程后重试'), and the
TUI dialog, /compact /undo errors, print output, and the VS Code
fallback all display it instead of maintaining their own wordings.
* fix: address third-round codex review
- metadata: hold an explicit lease across each state.json write so the
idle release timer cannot unlink the lock mid-write
- print: preflight the session lock before goal creation; make goal
creation idempotent and emit the goal summary only for attempts that
did not fail with session.stale, so a stale reload + retry neither
duplicates the goal nor the summary
- tui: route every prompt-launch path (plain, goal steer, plugin
command, inline skill, batch steer) through one conflict handler
with the occupied dialog and stale reload + input restore
- tui: snapshot and restore the attachment store across stale reloads
so pasted media placeholders still resolve after history replay
* fix: move session occupancy to a fresh error code, use English copy, release locks on exit
SESSION_OCCUPIED reused 40903, which belonged to PROMPT_ALREADY_COMPLETED until #3747 retired it, and web clients still allowlist 40903 for that old meaning; reusing it conflated the two semantics. Move session occupancy to the never-used 40931 and update the server API reference in both locales.
The occupied error message was hardcoded in Chinese while every other engine error is English, so the TUI, print mode, VS Code, and ACP clients all rendered mixed-language output; replace it with an English equivalent.
The lock release hook ran on 'beforeExit', which never fires when the process leaves through process.exit (SIGHUP in the TUI goes through emergencyTerminalExit), leaving stale lock files that could keep a session looking occupied forever after a hostname change or pid reuse. releaseAll is fully synchronous, so hook 'exit' instead.
* fix(agent-core-v2): decide prompt-derived titles after re-reading session metadata
applyPromptMetadataUpdate decided whether to derive a title from the
prompt from the in-memory metadata cache, which is only refreshed when
the session lock is re-acquired. If another client named the session in
the meantime, the stale cache still looked untitled, so the next prompt
from this client overwrote the other client's title — both on the stale
path, where the prompt is rejected afterwards, and on the fresh path,
where the other client only changed the title on disk.
Move the untitled check into SessionMetadata.applyPromptMetadata, which
runs after ensureWriteOwnership() has re-read state.json, so the
decision is based on the latest persisted metadata. The published
SessionMetaUpdated event now carries the derived title only when it was
actually written.
* fix: address fourth-round codex review
- vscode: classify session.occupied and session.stale as preflight errors so the webview toasts them instead of silently swallowing the send, and give session.occupied the same English copy the engine emits (the toast text comes from getUserMessage in the extension host).
- engine: beginSessionRun now takes the session context and agent lifecycle service, and on the first check of each lock hold it runs externallyModified over every loaded agent's wire (scope agents/<agentId>, key wire.jsonl) instead of only the main agent's, so a fork that only touched a subagent journal is detected as stale. All four call sites (loop submit, manual compaction begin, undo, print preflight) pass the extra dependency.
- tui: keep the request in flight across a stale reload instead of dropping to idle, so input submitted during the reload queues rather than racing the closing session; park the queue around switchToSession (resetSessionRuntime would clear it) and drain it into the reloaded session afterwards; when restoring the rejected text, prepend it before content the user typed during the reload instead of overwriting it.
- kap-server: clear the in-flight turn tracker and subagent roster in detachSession so a reattached session no longer reports stale in-flight turns or running subagents in REST snapshots and WS resyncs.
* fix: harden session conflict recovery
Detect external appends that race journal reads so a later run cannot miss session.stale. Keep deferred wire migrations pending and finish them before appending, while rejecting the append if migration still cannot complete. Restore the complete plugin command after an occupied cancellation, including commands without arguments.
* fix: scope session staleness cache to session context
Replace the process-global session-id map with a WeakMap keyed by ISessionContext so closed and reloaded sessions do not retain cache entries for the lifetime of the process.
* fix: document session conflicts for metadata routes
Declare SESSION_OCCUPIED and SESSION_STALE in the response schemas for profile updates, title generation, and child creation. Cover the OpenAPI declarations and the existing HTTP 200 conflict envelopes with regression tests.
* refactor: centralize session conflict handling
Move session error mapping and stale reloads into one shared route helper, reuse shared conflict response declarations, and cover skill activation conflicts. Remove the duplicate prompt, session, and skill error mappers while preserving their existing envelopes.
* fix(sdk): restate session conflict errors
* fix(tui): keep the reloaded session open and resend after a stale conflict
* fix(agent-core-v2): lazily rebuild registered agents after a session reload
* refactor: simplify session conflict handling
* refactor: hygiene fixes from session-lock review
- kap-server: move SESSION_STALE to 40932 (collided with
DEFAULT_WORKSPACE_UNAVAILABLE at 40930) and pin ErrorCode value
uniqueness with a test
- node-sdk: fold six catch-and-restate copies into a restate() helper
- acp-server: sync mapPromptLaunchError docstring with the
occupied/stale branches
- replace hardcoded session.stale/session.occupied literals with
ErrorCodes constants
- inline the single-use sessionStaleError
- restore the strict timer-leak assertion with a warmup baseline
instead of a blanket +1 allowance
- narrow SessionState.detached to ?: true
* refactor: introduce session-level run gate
- replace the beginSessionRun free function with ISessionRunGate, a
Session-scoped service; loop/compaction/undo now inject one
dependency instead of four, and sessionLock.ts no longer imports
session-layer modules
- tryHold() absorbs the duplicated try/catch hold copies in
wireService and sessionMetadataService
- resolve IAgentLifecycleService lazily inside the gate to avoid a
construction-time cycle with agentLifecycleService
* refactor: tighten session lock and conflict recovery structure
- Narrow ISessionLockService to hold/tryHold/release/lease/held and share
the hold bookkeeping between the memory and node-fs backends; drop
holdToken and the module-level exit hook state.
- Session metadata refreshes on a new hold via onDidHold and skips agent
registration through tryHold instead of catching session.occupied.
- AppendLogStore reads sizes through IFileSystemStorageService.sizeSync and
exposes a generic write guard instead of a session-lock binding.
- kap: reload stale sessions only at the call sites that can raise
session.stale and log reload failures; error mapping stays side-effect free.
- TUI: recover from a stale conflict through the in-place session reload with
history replay, removing the same-session switch special case, the media
store snapshot, and the queue parking.
- print: bound stale reloads with a simple retry loop.
* refactor(agent-core-v2): merge session lock release into run gate
* test(agent-core-v2): reuse append log fixtures
* refactor(agent-core-v2): break run gate and agent lifecycle dependency cycle
* refactor: reload stale sessions in core instead of each host
Stale detection already lived in core, but every host re-implemented the
recovery: TUI, print, and kap each closed and resumed the session, and TUI
and web resent the rejected input themselves.
Core now exposes reloadOnStale: on session.stale it closes and resumes the
session, lets the caller refresh, then retries the call once (undo reloads
and still rejects, since its count depends on what the user saw).
- kap: prompt submit, skill activation, and compact retry in place instead
of returning 40932; undo still returns 40932 after reloading
- print: retries through reloadOnStale on the same session id and keeps the
original permission mode to restore
- SDK: run-starting calls reload and retry internally; Session gains
setStaleReloadHandler so hosts refresh before the retry
- TUI: drops its own reload/resend/queue-merge path; the stale handler
replays history and keeps in-flight messages
* docs(server-api): note that only undo returns 40932 after a stale reload
* refactor(agent-core-v2): ask each agent's wire whether it was modified externally
* refactor(agent-core-v2): clarify agent restoration helper name
* Delete .changeset/ensure-agent-lazy-rebuild.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(tui): wrap session occupied notice
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
The default workspace lived in <Documents>/KimiCode/Default. On macOS the
first chat there triggered a Documents privacy prompt, and a denial blocked
every later chat until the user found the setting. Documents is also synced
by iCloud and OneDrive, and covered by Windows Controlled Folder Access.
Use ~/KimiCode/Default on every platform instead. This drops the Windows
PowerShell lookup, the XDG user-dirs parsing, and the documents_unavailable
reason; the error path is now always present.
* feat(server): create sessions in a default workspace when none is chosen
POST /api/v1/sessions accepts default_workspace: true, exclusive with
workspace_id and metadata.cwd. The server resolves <Documents>/KimiCode/Default
on every request, creates it on first use, and reuses it afterwards. A path
blocked by a file fails with 40409 and a permission error with 40411; the
server never relocates silently.
Workspace payloads on REST and WebSocket events carry is_default.
* fix(server): fail instead of relocating when Windows Documents cannot be resolved
A failed registry lookup used to fall back to %USERPROFILE%\Documents, which
could silently create a second default workspace when Documents is redirected
(for example by OneDrive). The lookup now fails with 40409, the failure is not
cached so the next request retries, and is_default reports false meanwhile.
* fix(server): resolve Windows Documents through the shell folder API
Reading User Shell Folders with reg.exe misses the default when the value is
absent, cannot expand every variable, and is blocked where registry tools are
disabled by policy. Ask Windows for the Documents folder through PowerShell
instead, forcing UTF-8 output so non-ASCII paths survive. A reported folder
that does not exist is treated as unresolved, so garbled output can never
create a workspace in the wrong place.
* test(server): assert the existing error code for a missing Windows Documents folder
* fix(server): start Windows PowerShell by absolute path
The Documents lookup spawned the bare name powershell.exe, so Windows
resolved it through the current directory and PATH. In an untrusted
checkout that lets a repository-provided powershell.exe run before the
trust gate. Resolve it under SystemRoot (falling back to windir and
then C:\Windows) instead.
* feat(server): report an unusable default workspace with its path and reason
Creating a session in the default workspace used to surface folder problems
as generic codes: 40409 when a file blocked the path or the Windows
Documents folder could not be located, 40411 on permission errors, and a
plain internal error for anything else. Clients could not tell the default
workspace apart from an ordinary missing directory, nor explain what to fix.
DefaultWorkspaceService now throws DefaultWorkspaceUnavailableError
(default_workspace.unavailable) carrying the folder path and one of
blocked_by_file, permission_denied, missing_parent, no_space, read_only,
invalid_path, documents_unavailable or unknown. ensure() also verifies that
a file can be created and removed in the folder, so a read-only folder is
reported before a session is placed there. The server maps the error to
40930 with { path, reason } in data.
* feat(server): make the default workspace location configurable
The default workspace was fixed at <Documents>/KimiCode/Default. Users who
keep chats on another disk, or whose Documents folder is synced, had no way
to move it.
Add a default_workspace.path setting in config.toml. It accepts an absolute
path or ~/..., and on Windows only a drive path or a full UNC share; the
value is checked for writability before it is saved and is never moved
silently. An invalid value in the file is reported as invalid_path instead
of falling back to the system location.
The server exposes GET and PATCH /api/v1/default-workspace with the
effective path, the saved custom path, the system path and the folder's
health. The section is kept out of GET /api/v1/config and rejected by
POST /api/v1/config so it has a single writer. Whenever the location
changes, through PATCH or an edit to config.toml, every registered
workspace broadcasts event.workspace.updated with its current is_default.
* feat(cli): add --default-workspace and expose the default workspace in the SDK
Add kimi --default-workspace to work in the shared default workspace instead
of the current directory. Both the interactive shell and print mode resolve
the folder through the engine before the session starts and change into it,
keeping PWD in sync; when the folder cannot be used, startup fails with its
path and reason. Resume hints become kimi --default-workspace -r <id> for
such sessions.
The session picker labels sessions in the default workspace as "Default
workspace" and copies a resume command without a cd for them. kimi doctor
reports the location and whether it can be used. The stty lookup excludes
both the launch directory and the new working directory, so a binary
planted in either is never executed.
The SDK gains KimiHarness.ensureDefaultWorkspace(), getDefaultWorkspacePath()
and getDefaultWorkspaceStatus(), maps default_workspace.unavailable to a
KimiError, and exports workspaceRootKey and DEFAULT_WORKSPACE_PROBLEM_MESSAGES.
* fix(server): never remove a write probe the check did not create
createExclusive resolves to false when the probe name already exists. The
write check ignored that result and went on to remove the path, which is a
recursive delete of an entry it did not create. Report the folder as
unknown instead and leave the entry alone.
* fix(server): declare 40001 on PATCH /default-workspace and log failed broadcasts
The PATCH route returns 40001 when the body fails validation, but its
OpenAPI response union only listed success and 40930, so generated clients
could not model it.
When the default workspace location changed but the workspace catalog
could not be read, the publisher dropped the broadcast without a trace.
Log a warning so the missing event.workspace.updated can be diagnosed.
* fix(cli): point the resume hint at the session folder after the default moves
A session started with --default-workspace printed
kimi --default-workspace -r <id> on exit even if default_workspace.path
changed while it was open. Following that hint moved into the new default
folder and refused the session, which still lives in the old one.
Repeat the flag only while the session folder is still the default
workspace; otherwise print cd <folder> && kimi -r <id>.
* refactor: trim the default workspace to what clients use
Keep session creation in the default workspace, the configurable
location, the 40930 error with its reason, and `kimi --default-workspace`.
Remove what no client needs:
- write probes, ancestor walks, and dangling-link checks; saving a
location now creates the folder, and use-time errors come from mkdir
- `system_path` and `problem` in the status response
- the workspace-update publisher; clients refetch workspaces after PATCH
- special handling of `default_workspace` in GET/POST /config
- the stray FS_PERMISSION_DENIED mapping on session routes
- the session picker label, the doctor check, the extra stty exclusion,
the moved-default resume hint, and the SDK path/status methods
Fix three gaps found along the way:
- clearing the location left `path` in config.toml, so it came back on
reload; drop the section instead
- treat an empty or relative XDG_CONFIG_HOME as unset
- honor HostProcessOptions.timeout so a stalled PowerShell lookup ends
* refactor: drop the --default-workspace CLI flag
No CLI user needs it yet. Desktop and web reach the default workspace
through the server, so the flag, its resume hint, and the SDK's
ensureDefaultWorkspace() go away along with their changesets.
* refactor: drop the default workspace settings endpoint
The location is now changed only by editing default_workspace.path in
config.toml, which the config watcher picks up without a restart. Remove
GET/PATCH /api/v1/default-workspace and the engine's status() and
setCustomPath(). Session creation keeps mapping the unavailable error to
40930 with { path, reason }, and the reason list moves to the
POST /sessions docs.
* fix: require a drive or UNC share for a Windows default workspace path
Rooted paths such as \Chats resolve against the server's current drive,
so the same setting could point at different folders. Treat them as
invalid_path instead.
* feat: notify the agent of external changes after /reload
* refactor: route watch-based AGENTS.md change alerts through the change notifier
* test: pin the missed detection of trailing AGENTS.md content deletions
* fix(agent-core-v2): anchor change-notifier comparisons to delivered beliefs
* fix(agent-core-v2): diff the full subagent model pool in change notifications
* fix(agent-core-v2): mute stale-session auto notifications during session reload
* chore(agent-core-v2): regenerate manifests and complete notifier stubs for the reload fixes
* chore: update the reload-change-reminders changeset
* test(agent-core-v2): cover the announced-then-deleted AGENTS.md sequence
* fix(vis): cover the changeNotifier.snapshot wire record
* fix(agent-core-v2): harden change-notifier beliefs for legacy wires and edge baselines
* fix(agent-core-v2): unmute the stale notifier when a session reload fails midway
* fix(agent-core-v2): seed change-notifier beliefs from the rendered snapshot
* fix(agent-core-v2): await the change-notifier snapshot dispatch
* fix(vis): harden the change-notifier snapshot renderer for legacy records
* test: isolate the reload e2e from the watch channel and tighten seed assertions
* chore(agent-core-v2): regenerate the wire manifest for optional belief fields
* chore: drop the changeset for reload change reminders
* fix(agent-core-v2): diff reload reminders against persisted beliefs first
* fix(agent-core-v2): recompute the bound subagent allowlist on catalog changes
* fix(agent-core-v2): roll back change-notifier beliefs on conversation undo
* feat(agent-core-v2): inline added profile metadata in subagent change reminders
* fix(agent-core-v2): detect legacy AGENTS.md truncation and gate skill reminders
* fix(agent-core-v2): reset change-notifier beliefs on context clear
* feat(kap-server): project the reload session action into the OpenAPI document
* fix(node-sdk): keep plugin-only reloads from refreshing workspace catalogs
* fix(agent-core-v2): gate and filter subagent change reminders
* fix(agent-core-v2): evaluate reminder gates against the persisted binding
* fix(kap-server): scope reload catalog refreshes to the target workspace
* fix(node-sdk): scope reload catalog refreshes to the target workspace
* fix(agent-core-v2): reset change-notifier beliefs on compaction
* fix(agent-core-v2): hash AGENTS.md files from one atomic snapshot
* fix(agent-core-v2): close first-watch and legacy gaps in change detection
* fix(kap-server): block reload during compaction and reconcile after failed reload
* fix(node-sdk): block reload during compaction and reconcile after failed reload
* feat(agent-core-v2): let dangerous_command_guard=false disable all built-in guard ask policies
* docs(agent-core-v2): name git control-path read relaxation and scope the only-ask-rules claim
Agent-level hooks such as PreToolUse, UserPromptSubmit, Stop and PreCompact,
plus hooks fired from the event bus, did not pass a cwd, so the runner fell
back to the directory the engine was started from. Session-level hooks
already used the session directory, and the hooks documentation says the
working directory is the session's project directory. A server hosting
several workspaces, or a CLI that changes directory after the engine starts,
ran these hooks in the wrong place.
* fix(agent-core-v2): nudge the agent to keep its todo list current
- Remind only while the list still has pending or in_progress items, and
drop the first-use arming gate so compaction and /undo no longer arm the
reminder for agents that never used TodoList.
- Remind after 5 steps without an update (was 10) with text that names the
step count, progress, and the item still in progress, instead of the
gentle "ignore it if not applicable" wording.
- Replace the static TodoList write echo with a next step for the current
list state.
- Drop the churn warnings that discouraged updates from the TodoList
description, say the list is a live progress panel, and add a TodoList
paragraph to the system prompt when the tool is active.
- Track todo_list_written, todo_reminder_shown, and todo_turn_ended.
* fix(agent-core-v2): address todo reminder review feedback
- Ask the agent to mark the last open item done only once it is finished;
if it stays blocked or unfinished, keep it in_progress and say what
remains in the reply.
- Emit todo_turn_ended for turns that cleared the list, so cleared lists
count toward the stale-list ratio instead of being dropped.
* feat(tui): auto-generate session title after the first turn
* Update tui-auto-session-title.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(tui): gate auto session title behind a tui.toml auto_session_title toggle
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(google-genai): map thinking effort off to LOW for Gemini 4 models
Gemini 4 supports only LOW/MEDIUM/HIGH thinking levels and always thinks;
sending MINIMAL for effort=off makes every request fail with a 400. Degrade
'off' to LOW (thoughts still hidden) for gemini-4 models in both the
agent-core-v2 requester and the legacy kosong provider, keeping MINIMAL for
Gemini 3.
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
* fix(google-genai): drop comment in comment-free agent-core-v2 source
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
* fix(google-genai): preserve effort off in thinkingEffort for Gemini 4
LOW with suppressed thoughts is the Gemini 4 encoding of 'off', so map it
back to 'off' in the thinkingEffort getter instead of reporting 'low'.
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
* fix(google-genai): keep explicit LOW kwargs reported as low on non-Gemini-4
Only Gemini 4 encodes 'off' as LOW with suppressed thoughts, so gate the
reverse mapping on the model; a caller-supplied LOW/includeThoughts=false on
other models keeps reporting 'low'.
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
* fix(google-genai): report LOW as low in thinkingEffort again
Gemini 4 encodes 'off' as LOW with suppressed thoughts, which is
indistinguishable from an explicit LOW/includeThoughts=false kwarg, so no
reverse mapping can serve both callers. Nothing in the repo reads this
getter to persist or display effort, so keep the derive-from-kwargs
semantics; for Gemini 4's 'off', 'low' is the honest report of what runs.
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
* fix(google-genai): reject thinking effort off for Gemini 4 models
Gemini 4 supports only LOW/MEDIUM/HIGH thinking levels and always thinks,
so requesting effort 'off' now fails fast with a clear thinking-config
error (matching the always-reasons rejection other protocols already use)
instead of sending an invalid MINIMAL level or silently degrading to LOW.
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
---------
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
Co-authored-by: 7Hanrui <qihanrui@moonshot.ai>
* fix(google-genai): use thinking level for Gemini 4 models
The thinking parameter is picked by a hard-coded `gemini-3` name check, so
Gemini 4 model ids fall through to `thinkingBudget`, which the Gemini API
rejects with HTTP 400 ("Thinking budget is not supported for this model.
Use thinking level instead."). Add `gemini-4` to the same check in both the
engine copy and the legacy kosong provider; Gemini 3 and earlier keep their
current encoding.
* chore(changeset): keep the Gemini 4 entry to the user-facing outcome
* test(google-genai): use the real Gemini 4 model id in the thinking tests
---------
Co-authored-by: Selene <mahaoyang@corp.netease.com>
* feat(agent-core-v2): add global default for deferred MCP tool disclosure
* feat(agent-core-v2): capture the deferred MCP default at server connect time
* feat: support model migration metadata
* chore: revert web bundle sync
* feat: persist model upgrade metadata
* fix: resolve model upgrade targets
* fix: keep model upgrade metadata consistent
* fix: complete model upgrade metadata handling
* fix: handle aliased upgrade targets and RFC3339 variants
* fix: resolve bare model upgrade targets
* feat: prompt for model migration in the TUI
* fix(tui): defer model migration until the editor is idle
* fix(tui): preserve model migration prompts and preferences
* fix(tui): hold the model migration prompt while /new replaces the session
* fix(tui): keep the resume cache hint from replacing an open dialog
* fix(tui): keep the thinking effort across a model migration when supported
* fix(tui): strip terminal control sequences from migration notices
* fix(tui): warn about prompt cache loss before a mid-conversation migration
* docs: drop the effort persistence claim from the migration wording
* test: trim redundant migration test cases
* fix: switch to the declared replacement when a registry drops the default model
A registry retires a model by dropping it after declaring `upgrade`, but
the refresh deleted the alias together with its upgrade metadata and
cleared the default, leaving users without a model and with a misleading
login hint. Refresh and re-import now move a dropped default to the
upgrade target persisted by an earlier refresh, clearing it as before
when that target is gone too. The TUI warns about the switch and moves
the live session off the dropped model once no dialog is open.
* docs: fix the /reload-tui command spelling in config-files
* fix: resolve migration placeholders at import and keep acknowledgements out of tui.toml
Migration text placeholders were only filled in by the TUI, so the model
catalog served raw templates to every other client. The registry import
now stores the final text and the TUI renders it as is.
Migration acknowledgements are display state rather than preferences, so
they now live in model-migrations.json under the data directory instead of
tui.toml; preference saves and acknowledgements no longer rewrite each
other, and /reload has nothing to sync.
Also carry the dropped model's record in the refresh result instead of
snapshotting it in the TUI, give performModelSwitch explicit persistence
modes instead of a boolean-or-mode flag, fold the Markdown notice into the
picker's notice option, and rename waitForLazyCreation to
waitForSessionAssembly now that it also waits for /new.
* refactor(tui): move the shared model switch out of the /model command
performModelSwitch and its helpers became shared once the migration
prompt and the retired-default switch started calling them, yet they
stayed in the /model command module, leaving a controller depending on
command internals. Move them unchanged to tui/utils/model-switch.ts, which
the command module and the migration controller now both import.
* feat: surface retired default model redirects in refresh results and catalog events
A registry-side retirement only existed in the OAuth orchestrator's
in-process RefreshResult, so kap-server's scheduled refreshes redirected
the default model without any client-visible signal. The refresh response
now carries an optional retired_default { from, to, from_model }, mapped
through to the REST refresh endpoints and the global
event.model_catalog.changed broadcast, and mirrored in the klient
contract.
* refactor(tui): consume the engine's provider refresh instead of orchestrating its own
The TUI ran the OAuth refresh orchestrator in-process through its own
persistence host (including a dead v1 fallback), duplicating what the
engine's discovery service already does. KimiHarness now exposes
refreshProviderModels (via the klient facade over
IProviderDiscoveryService), so the TUI deletes its orchestration glue and
reads retired_default from the engine response. Also fix the klient
facade deriving RefreshProviderModelsResponse from the OAuth login
refresh, which hid retired_default from clients.
* feat: move model retirement follow-through into the engine
The discovery service now records retirements, emits onDidRetireModel,
and answers retiredModelReplacement; agent profiles follow a retirement
live and on restore, so the TUI only shows the notice and steers the
session-less selection. The wire retired_default drops from_model in
favor of an engine-resolved from_display_name. Removes the now-dead
supportsAtomicSectionReplace / replaceConfigSections /
removeProviderFromConfig SDK exports (no remaining consumers).
* Update model migration guidance in documentation
Prompt for migration when the TUI editor is idle and switch to the declared replacement when a registry drops the default model.
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* test(oauth): drop the duplicate dropped-default redirect case
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(agent-core-v2): distinguish session-unavailable tools in select_tools
* fix(agent-core-v2): explain tool selection failures and repeated guesses
* fix(agent-core-v2): keep failure counts honest in mixed select_tools batches
- Reuse the globalPolicy cache for denial checks instead of bypassing it.
- A successful name now only resets the consecutive-failure streak and
clears its own failure record; failed names in the same batch keep
accumulating attempts instead of being masked by sibling successes.
* fix(agent-core-v2): honor tool source in policy checks and announce empty loadable sets
- Evaluate deny globs with the registered tool's real source; only
unregistered names fall back to the mcp__ prefix inference.
- Keep select_tools callable via the disclosure check so a profile
allowlist omission no longer mislabels it as policy-denied.
- Announce the empty loadable state once for legacy sessions whose
last announcement lacks it, then stay silent.
* feat(kap-server): add remote control device management
* feat(remote-control): tag share URLs with the caller's platform source
The share link's from query parameter was hardcoded to kimi_code_cli, so
links created by the desktop app were mislabeled as CLI traffic.
buildRemoteControlUrl now takes the source explicitly and
RemoteControlOptions carries it as urlSource; kap-server threads
hostIdentity.platform through, so desktop hosts produce
from=kimi_code_desktop while CLI hosts keep from=kimi_code_cli.
* fix(kap-server): stop the local tunnel when the current device is deactivated or deleted
The device management routes only called the cloud API. When the target
device is the machine itself, the local tunnel kept running: after the
relay drops it, the client reconnects and re-registers, so the device
came back online (or was recreated after a delete) and the local status
never returned to off.
After a successful cloud deactivate/delete of the current device, the
routes now also disable the local tunnel manager.
* feat(kap-server)!: drop the remote-control device delete route
Device removal is collapsed to a single deactivate verb: the desktop UI
deactivates devices instead of deleting them, so the DELETE
/remote-control/devices/{id} route and the device client's deleteDevice
go away before this API ever ships.
* feat(kap-server): carry the lock holder in the already-running error
The enable route now returns the RemoteControlAlreadyRunningError holder
(pid, local origin, device id, url, started_at) in the error envelope's
details, documented with a details schema, so desktop can render a
structured conflict state instead of the raw message.
* feat(kap-server): expose the lock conflict in the remote control status
The manager now surfaces the holder when its last enable error is an
already-running lock conflict, and GET /remote-control returns it as a
structured conflict field, so clients can present the conflict state
proactively instead of parsing the error message.
* feat(kap-server): resolve the remote control lock conflict live in status
GET /remote-control now re-inspects the machine lock whenever the
manager is off: a live holder is reported as the conflict even without
a prior enable attempt in this process, and a stored conflict plus its
error text are dropped once the holder's process is gone. Panel state
refreshes such as after device deactivation now reflect reality.
* feat(kap-server): wait for the lock holder to release on current-device deactivation
When the deactivated device is the machine's own and the lock is held
by another live process (e.g. a CLI instance whose tunnel the relay is
about to terminate with user_requested), the route now polls the lock
for up to two seconds before responding, so a client's immediately
following status read sees the settled conflict-free state.
* style(kap-server): silence promise-executor-return in lock release wait
* test(remote-control): pass urlSource in the close-reason cases
* feat(kap-server)!: register remote control devices with the host product name
* refactor(cli): source the remote control identity from app constants
* fix(remote-control): make starts cancellable and harden logout cleanup
* fix(remote-control): close the remaining cancellation and invalidation races
* fix(kap-server): sweep remote control cleanup once more after logout
* docs: note the lock-release failure of remote control deactivation
* fix(remote-control): key cloud sessions by credential and settle cancelled starts
* fix(kap-server): block remote control starts during logout and serialize tunnel toggles
* fix(kap-server): gate local starts during current-device deactivation
* fix(kap-server): match active tunnel ids on deactivation and re-clear sessions after logout
* fix(kap-server): serialize Kimi logouts, harden lock cleanup, and whitelist device fields
* fix(remote-control): abort pending handshakes on close and answer logout before tunnel teardown
* fix(remote-control): cancel enables that are still queued behind a pending disable
* fix(kap-server): recognize foreign logout gates and retry post-logout tunnel teardown
* fix(remote-control): verify the machine lock after disable and recheck deactivation
* feat(kap-server): export loadOrCreateServerToken for host token provisioning
* feat(agent-core-v2): report fork request prefix parity in the prompt cache probe
The fork prompt cache probe only reported the child's cache usage, so a low
hit rate could not be told apart between a client-side prefix divergence and
a server-side miss, and the hit rate itself was diluted by the new tail that
can never be cached.
The requester now keeps a snapshot of each agent's last completed turn
request (the tools and system prompt hashes it already records in
llm.request, input tokens and timing). A forked agent captures its parent's
snapshot when it is created and reports tools_match, system_prompt_match,
parent_input_tokens and parent_request_gap_ms with its first request.
* fix(agent-core-v2): clear the parent request snapshot when the context is rewritten
Undo and compaction rewrite the parent context without touching the
non-persisted snapshot, so a /btw fork made before the next turn request
compared against a request whose input the child no longer inherits.
* fix(agent-core-v2): drop stale parent baselines from the fork cache probe
The probe compared the child's first request against the parent snapshot
captured at scope creation, but a parent undo or compaction landing
before the context copy left that snapshot describing a request the
child no longer inherits, and a model switch between the parent's last
turn and the fork compared across cache namespaces that can never share
a prompt cache.
The probe now watches the parent's ContextSpliced/ContextUndone events
and drops the captured snapshot once the parent context is rewritten,
and the snapshot carries the model alias so the comparison fields and
the parent input token baseline are only reported when both sides ran
the same model.
* fix(agent-core-v2): keep the turn request snapshot across conversation undo
Undo only truncates the tail of the parent context, so the request a
/btw fork inherits is still a strict prefix of the request the parent
last cached: the server cache keeps hitting and the snapshot's hashes
stay accurate. Clearing the snapshot on undo threw away a baseline
that was still valid, and made the requester depend on the undo
domain for no benefit. Only compaction rewrites the prefix, so it
remains the sole event that clears the snapshot.
* refactor(agent-core-v2): validate the fork cache probe baseline by context prefix revision
The requester subscribed to ContextSpliced solely to keep a telemetry
snapshot fresh, which made the module understand context rewrites and
grow an event bus dependency for the probe's sake. The probe's own
subscription could not cover forks created after a compaction either,
which is the only reason the requester had to listen at all.
Context memory now exposes a prefix revision that increments on every
deleting splice, the turn request snapshot records the revision it was
taken at, and the probe compares it with the parent's current revision
at probe time. Read-time validation covers both the compact-then-fork
and the fork-then-compact windows with no subscriptions, and the
requester is back to only recording facts about its own requests.
* fix(agent-core-v2): stamp the turn request snapshot with the build-time context revision
The public SDK clearContext path has no busy check and can empty the
parent context while a turn request is in flight. Reading the revision
at completion stamped the snapshot with the post-clear value, so a
later fork saw matching revisions and hashes and silently attributed
the client-side prefix divergence to the server. The request input is
fixed by recordRequest time, so capture the revision in the same
synchronous block; a mid-flight clear then leaves the snapshot on the
pre-clear revision and the probe drops the baseline on comparison.
* fix(agent-core-v2): capture the snapshot context revision where the request messages are copied
The build-time capture still ran after the awaited media resolution,
so a clearContext or compaction landing in that window stamped the
snapshot with the post-rewrite revision for a request whose messages
were copied earlier. resolveRequest is synchronous and copies the
context messages there, so the revision captured alongside the copy
always describes the same context the request was built from.
* fix(agent-core-v2): omit the parent token baseline when the parent request reported no usage
A provider that completes without a usage event left the snapshot with
inputTokens: 0, so a later same-model fork emitted parent_input_tokens: 0
and the input_cache_read / parent_input_tokens analysis divided by zero
or treated an unknown baseline as real. The field is now optional and
absent for that snapshot, which the optional telemetry property already
allows.
* feat: show NotifyUser progress in desktop and web
Give NotifyUser a one-line title, report the latest update on the
session status, and forward subagent updates to the parent so GUI
clients can show them on subagent cards.
* feat: turn on NotifyUser progress updates by default
Flip the notify_user flag default to true; the flag stays as the opt-out
(`[experimental] notify_user = false` or KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=0).
Register the new subagent.update wire record in the vis renderers and
record-type union so the manifest coverage test passes.
* refactor: trim NotifyUser surfaces to conversation progress rows
* feat: make NotifyUser title optional and enforce single-line titles
* feat: make NotifyUser opt-in via the settings lab
* docs: describe NotifyUser as off by default with the settings lab opt-in
* fix(remote-control): exit the web server when the relay takes the device offline
* test(cli): satisfy the remote-control handle contract in tui web tests
* feat(tower): coordinate missions with deterministic orchestration
* fix(tower): disable goal mode
* feat(tower): degrade gracefully on tower state loss
When .tower/comms is deleted while .tower/ survives (e.g. git clean),
tower tools no longer hard-fail with not-initialized:
- TowerStatus returns a state-lost report; never-initialized repos
(no .tower/ at all) keep the existing not-initialized behavior
- TowerSend/TowerComplete recover a minimal state (directory skeleton,
empty missions/roster, recoveredAt marker, base taken from the
current checkout branch) and keep delivering; a caller whose roster
entry was lost is re-registered as a placeholder named after its
agent id
- TowerComplete degrades explicitly after a loss: the report is
delivered to the tower as a completion-report-state-lost message and
no mission is marked completed
- TowerMerge/TowerRebase and every other locked mutation fail with an
explicit state-lost error instead of silently running against an
empty history
- the recoveredAt marker is shown on the TowerStatus dashboard and in
the status summary, reminding the tower to report the history loss
to the user
* feat(tower): veto destructive bash commands in the tower main checkout
* fix(tower): allow git clean dot pathspecs inside tower worktrees
* fix(tower): restore attachment guidance for mission context
* fix(tower): engage bash guard only while the workspace has an owner
* fix(tower): veto git clean only with force and -x/-X; drop markInboxRead
* chore(tower): drop per-fix changesets to keep a single concise one
* fix(tower): check tower owner before preparing the user base
* feat(tower): show a notice when goal activation ends tower mode
* fix(tower): unify the mode-mutex exit reason across goal, plan, and swarm
* refactor(tower): drop token stamping from protocol artifacts
* fix(agent-core-v2): deny tower workers subagent delegation
Remove the Agent tool and the subagents allowlist from the tower-worker
profile so workers and reviewers cannot nest subagents inside a mission;
nested subagents bypassed the tower concurrency budget and the tower's
steering. Drop the now-unreachable reviewer-only Agent-resume veto and
its tests; the branch-lease and foreground-resume vetoes stay.
* test(tower): stub ILogService in every AgentTowerService test scope
The fork-restore tests build second DI scopes that register
AgentTowerService without stubbing ILogService, leaving this.log
undefined. When reconcileForeignTower fires a fire-and-forget exit and
the ownership release rejects, the rejection handler's this.log.warn
throws a TypeError that surfaces as an unhandled rejection in the full
suite. Add ix2.stub(ILogService, stubLog()) to all nine scopes that
lacked it, matching the existing sibling pattern.
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>