* 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>
* fix(agent-core-v2): stop discouraging mid-turn progress notes and calm NotifyUser nudges
- system prompt: drop the inaccurate "text between tool calls may not be
shown" premise; ask for one- or two-sentence progress notes instead
- remove the mid-turn text nudge that asked the model to repost visible
text as a NotifyUser update
- count the silence nudge in tool-call rounds (one per step) instead of
individual tool calls, so parallel calls no longer trip it early
* feat(agent-core-v2): track NotifyUser nudges and updates
- notify_user_nudge_shown: rounds since the last update and the nudge's
position within the current silent stretch
- notify_user_sent: rounds before the update, whether a nudge preceded
it, message length (no content), and whether it was displayed
* chore: add changeset for mid-turn progress notes and NotifyUser nudges
* feat(cli): launch desktop with kimi app
* fix(cli): report desktop opener failures and complete command tests
* fix(cli): resolve system openers from filesystem roots
* feat(kap-server): add allow_ignored_globs to fs:list for un-ignoring named paths
The list filter keeps gitignored entries hidden unless they match the
new allow_ignored_globs request field, letting callers surface
specific ignored paths (e.g. .tmp) while the rest of gitignore still
applies.
* docs: document allow_ignored_globs in the fs:list API reference
* fix: traverse ignored ancestor directories of allow_ignored_globs matches
A nested allow glob such as ignored-dir/keep.txt or ignored-dir/** used
to be unreachable in recursive listings because the ignored parent was
skipped before any child could match. Keep an ignored entry when it is a
literal-prefix ancestor of an allowed glob (directories only), and
document that dot-path allowances additionally require show_hidden.
* fix: match ignored ancestor directories across wildcard glob segments
The literal-prefix check still dropped ancestors when a directory
segment of an allow glob contained a wildcard, e.g. ignored-*/keep.txt
or ignored-dir/**/keep.txt. Decide reachability by consuming the
candidate path segment-wise against a prefix of the glob instead.
* fix: correct and harden workspace fs glob matching
Compile **/ to (?:[^/]+/)* so recursive wildcards preserve path
segment boundaries; a/**/b no longer matches a/xxb. This affects
include_globs, exclude_globs, and allow_ignored_globs across
fs:list / fs:search / fs:suggest / fs:grep.
Match glob prefixes iteratively in globCanMatchBelow so deeply
segmented globs cannot exhaust the call stack.
* chore: add changesets for allow_ignored_globs and the glob boundary fix
* fix: satisfy noUncheckedIndexedAccess in the glob prefix grid
* fix: restrict globstar segment expansion to whole path segments
foo**/keep.txt regressed when **/ gained segment-boundary semantics:
the stars sat mid-segment but were expanded as a whole-segment
globstar. Only expand **/ when the stars open a path segment, treat
mid-segment stars as an ordinary within-segment wildcard, and keep a
trailing ** matching across segments as before.
* fix: match workspace fs globs with picomatch and probe speculative ancestors
Replace the hand-rolled glob compiler with picomatch, the standard
engine already used elsewhere in the package, which settles segment
boundary, mid-segment globstar, and deep-pattern complexity edge
cases for good. An ignored directory kept only as a possible ancestor
of an allow glob is now probed (depth and entry capped, fail-open) and
withheld when no descendant actually matches.
* fix: cast readonly glob list for picomatch isMatch
* fix: parse globs whole in the ancestor check and share the probe budget
Splitting allow globs on '/' corrupts brace and extglob constructs
that contain slashes; take path segments from picomatch.scan instead
and fall back to a conservative answer for opaque single-token globs,
letting the descendant probe decide visibility with whole-pattern
matching.
Probe budgets are now shared across one fs:list request instead of
resetting per candidate directory, so a tree full of speculative
ancestors cannot multiply filesystem operations.
* fix: tolerate empty globs and slash-bearing alternatives
picomatch throws on empty patterns where the old compiler simply
never matched, turning previously valid requests into server errors;
filter them out up front. Brace and extglob alternatives that embed
a slash inside one scanned part now fall back to the conservative
ancestor answer as well, leaving visibility to the whole-pattern
descendant probe.
* fix: disable picomatch negation in workspace fs globs
A leading exclamation mark was a literal character in the old
matcher, so exclude_globs like !foo only matched a path literally
named !foo; as a picomatch negation the same request would exclude
everything except foo. Pin the prior semantics with nonegate.
* fix: scan allowance globs with nonegate and skip empty segments
The scan call still used default negation, stripping a literal
leading bang from allowance prefixes like !foo/keep.txt. Consecutive
separators produced empty scanned segments that made picomatch throw
during prefix compilation; empty segments now simply match nothing.
---------
Co-authored-by: qer <wbxl2000@outlook.com>
Fork copies the wire journal, so cron tasks persisted as cron.add
records were inherited by the fork and fired from both sessions.
Fold the forked marker record into the cron actor: the task table is
cleared at the fork point and a one-time cron_fork_cleared reminder is
delivered (tracked by a durable forkNotice flag confirmed via
ContextAppendMessage), mirroring the goal feature's fork handling.
The forked event class moves from features/goal to
session/agentLifecycle so both features share it.
* fix(agent-core-v2): fold UserPromptSubmit hook output into prompt content parts
UserPromptSubmit hook messages now merge into the user prompt message as
meta-marked text content parts (contentType text/xml, source 'user prompt
submit hook') at content position 0 instead of a standalone hook_result
user message, so one prompt always yields exactly one user message and
turn pairing can no longer shift. Structured hook JSON without a message
field (e.g. {"continue":true}) no longer injects anything; plain-text
stdout still falls through. turn.prompt records, TurnStarted prompts,
fold turn openings, and steer content keys all strip hook parts; existing
wires with standalone hook_result messages keep their current behavior.
* feat(agent-core-v2): keep UserPromptSubmit hook parts in turn records and handle them across client surfaces
* feat(agent-core-v2): meta-mark bundled skill parts and replace positional slicing
Bundled skill blocks are now constructed with meta { source: 'skill
activation' }, matching the hook-part convention. Every former
content.slice(skillActivations.length) site — turn prompt text, steer
merge/echo, session and fork titles, transcript fold, TUI replay — now
annotates then filters by meta, so prompt content no longer depends on
block positions and the loop needs no hook-part knowledge. A shared
annotate step keeps legacy wires working: leading unmarked blocks (one
per bundled activation) are marked on read before filtering.
* feat(agent-core-v2): strip hook parts from the public TurnStarted prompt at construction
* feat(kimi-code): show what trusting activates in the workspace trust prompt
* fix(kimi-code): keep trust prompt non-blocking and disclose only external additional dirs
* fix(kimi-code): mark trust disclosure incomplete when discovery times out or fails
* fix(kimi-code): exclude disabled MCP servers from trust disclosure and cap key lists
* fix(kimi-code): disclose disabled user MCP servers shadowed by project config
* test(kimi-code): merge redundant trust prompt disclosure cases
* test(node-sdk): merge mirrored user/project MCP override cases
* Update trust-panel-disclosure.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(kimi-code): disclose symlinked additional dirs by real target in the trust prompt
* fix(kimi-code): bound trust disclosure I/O and harden path containment
* fix(kimi-code): tighten trust prompt disclosure copy
* refactor(kimi-code): compute the trust disclosure inside the engine
The disclosure composition (project-origin MCP servers, additional dirs
with realpath containment, instruction sources) moves from the SDK into
agent-core-v2's workspace scope as WorkspaceTrustDisclosureService,
constructed alongside the other generation services. The SDK's
getWorkspaceTrustInfo becomes a thin forward, so the config-merge
semantics live next to the workspaceTrust gate that owns them instead
of being re-derived downstream.
Also drops the shadowed-user-server section and the per-section
deadlines with their disclosureComplete flag: the engine reads the
same loaded state session creation awaits, so the prompt needs no
timeout plumbing of its own, and the empty state stays a single honest
copy.
* refactor(kimi-code): compress trust disclosure tests and disclose only effective profiles
Merge mirrored test cases across the three suites and share fixtures;
small source trims (shared wrap helper, context.workspaceId).
The disclosed agent profiles now pass the same resolution the session
catalog applies: a workspace profile clashing with a builtin without
override: true, or losing to a higher-priority candidate, is no longer
reported as activated.
* test(kimi-code): drop the engine disclosure suite in favor of e2e coverage
The SDK e2e suite exercises the disclosure through the real engine, so
the stub-based engine unit file duplicated it; a builtin-clash agent
case moves there to keep the suppressed-profile behavior pinned.
Also folds the sanitize vectors into the MCP render case and trims the
trust type docs.
* fix(kimi-code): cap untrusted MCP target fields and correct profile wording
A crafted .mcp.json can supply arbitrarily long command/args/url
strings; each field now truncates at 80 chars before wrapping. The
profiles line no longer claims they feed the agent — a non-default
workspace profile is only made available as a subagent, the default
agent still binds the default profile.
* fix(kimi-code): honor profile overrides and reveal instruction symlinks in trust disclosure
A workspace profile with override: true replaces a same-name builtin
or higher-priority candidate in the session catalog, so the disclosure
now applies the same replace semantics instead of dropping it.
Instruction paths get the same realpath treatment as additional dirs:
a project AGENTS.md symlinked to a file outside the project renders
its real target instead of hiding behind the lexical path.
* fix(kimi-code): disclose skill roots escaping the project in the trust prompt
Read/Write/Edit/Grep/Glob all add getSkillRoots() to their allowed
roots, so a .kimi-code/skills symlink to an external directory is an
outside-project file access grant that trusting activates. The
disclosure now merges those roots (resolved via the same projectRoots()
the skill source uses) into the outside-directories list. Also caps
each displayed MCP key name, not just the key count.
* fix(kimi-code): cap the bearer-token env var label in the trust prompt
* refactor(kimi-code): disclose only real locations in the trust prompt
The path → realPath arrow is dropped; gated directories and AGENTS.md
entries are disclosed as their resolved real locations, and the pair
type collapses back to plain strings. Entries resolving to the same
real directory collapse into one.
* fix(kimi-code): tighten the trust prompt list caps to fit one screen
* fix(kimi-code): lower the trust prompt list caps further
* fix(agent-core-v2): keep explicit agent-file precedence in the trust disclosure
* Revert "fix(agent-core-v2): keep explicit agent-file precedence in the trust disclosure"
This reverts commit 6e5c45d1b69061a041cc991a0882b1c651fc6003.
* fix(agent-core-v2): preserve profile priority in trust disclosure
* fix(kimi-code): keep workspace trust choices visible
* fix(kimi-code): align trust prompt paging keys
* docs: remove trust prompt keyboard additions
* feat(kimi-code): summarize workspace trust with configuration sources
* chore(agent-core-v2): sync workspace instruction state manifest
* fix(kimi-code): recognize dot-prefixed project directories in trust summary
* refactor(kimi-code): reuse existing trust configuration sources
* fix(kimi-code): keep trust disclosure independent of skill watchers
* refactor(kimi-code): condense additional directory disclosure
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat: trust workspace via KIMI_CODE_TRUST_WORKSPACE env var
* fix: honor trust env override in MCP registry and untrust route
* test: add getEnv to mcpManagement bootstrap mock
* test: fold trust-env route case into existing workspaces suite
* fix(cli): mention KIMI_CODE_TRUST_WORKSPACE in the print-mode MCP trust warning
* docs: describe KIMI_CODE_TRUST_WORKSPACE by its semantics, not its MCP effect
The nudge reminders were gated on the notify_user flag and on the tool
being registered, but not on the host. A session created in the TUI keeps
the tool after it is reopened elsewhere, so a host that cannot display
updates kept telling the agent to send them while every call reported
that nothing was displayed.
Gate the reminders on the same availability check the tool uses when it
runs: the flag is on and the host declared the update_panel capability.
- tower mode: a message sent while the agent is running aborts the in-progress inbox-wake turn and runs as its own turn; notification-seeded turn waiters now settle launched/completion
- TowerTeardown: keeps worktrees of live agents (not overridable with force), supports exclude and dry_run
- tower roster: died/revived store writes are guarded to the active tower session and attributed with session+pid; agents stopped by the user are marked dead by intent with no revival nag, and unintended deaths get diagnose-first guidance
- workers must TowerInbox before marking a mission completed: the store refuses completion with unread inbox messages, and the briefing carries the inbox-before-completion, mission re-read, and long-operation chunking instructions
- tower store: state.json read-modify-write is serialized with a cross-process lockfile (token-checked release) and written atomically via per-writer tmp + rename
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
* chore: add regression and user-impact review to the PR workflow
The review-pr skill now runs a separate regression pass with impact levels
and a repository-specific list of user populations and contract files, the
PR template asks authors for a behavior-change table, Code Review Rules in
AGENTS.md encode the same checks for automated review, and changesets must
name the behavior users lose when a default flips.
* chore(agents): stop instructing agents to invoke the review-pr skill
The skill is user-triggered only (disable-model-invocation), so AGENTS.md
no longer tells agents to run it; the Code Review Rules keep pointing at
surfaces.md for the population and contract list.
* chore(review): define impact levels by consequence and close inventory gaps
Impact levels are now decided in three steps from the change itself
(consequence, escape hatch, silent-input upgrade) with changeset and docs
kept in the remediation check; escape-hatch wording allows explicit
maintainer sign-off; prompt-text rule names the receiving population;
one-sided diff evidence is accepted; release and packaging CI, the klient
facade, the transcript contract, kosong providers, and all env-var access
forms are added to the surfaces inventory.
* revert: roll back workspace trust-boundary hardening
Roll back the hardening added in #3964 that guarded against malicious
content inside an already-trusted repository:
- background git invocations no longer override repo-local git config
(fsmonitor, hooksPath, filters, drivers, gpg, editor)
- agent file tools no longer re-resolve paths through symlinks
- project-local local.toml applies without workspace-trust gating,
additional_dir entries are no longer rejected for resolving to the
home directory or filesystem root, and writing it is auto-approved
again inside a git worktree
Kept, since they only affect the pre-trust window or the trust prompt
itself: the fail-closed trust prompt and lazy footer git detection.
* Update revert-trust-boundary-hardening.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* docs(changelog): sync 2.1.0 from apps/kimi-code/CHANGELOG.md
* docs(changelog): shorten the fullscreen TUI entry to how to enable it
* docs(changelog): reword the fullscreen TUI entry
* chore: sync web dist from code-app
code-app: 955d6b744b76630828c3d80406e16d73a7a7454f
* chore: collapse web dist sync changesets into one summary entry
* fix(remote-control): pick credential slot and relay from login region
Global login stores tokens in kimi-code-env-*.json and should use
code-rc.kimi.ai; Remote Control always read kimi-code.json and
code-rc.kimi.com. Resolve both from the persisted oauth ref.
* fix(telemetry): skip the dead global collector and keep shutdown alive
The global collector host has no DNS. Drop it from the region profile and
skip the send instead of falling back to the mainland endpoint. Keep the
shutdown timeout timer referenced so a failed flush cannot exit before the
TUI hands the process to the foreground server.
* fix(remote-control): require explicit relay origin and drop hardcoded .com defaults
* feat(agent-core-v2): add repeat_breaker config switch
Add a top-level repeat_breaker boolean (default true) to config.toml,
overridable with the KIMI_CODE_REPEAT_BREAKER env var. When disabled,
the agent skips repeat reminders and forced stops on consecutively
repeated identical tool calls; same-step dedupe is unaffected.
* Update repeat-breaker-config-switch.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(agent-core-v2): add tool_dedupe config switch
* refactor(agent-core-v2): drop the tool_dedupe config switch
Same-step dedupe stays always on. repeat_breaker remains the only public switch.
* refactor(agent-core-v2): read the repeat breaker from the environment only
KIMI_CODE_REPEAT_BREAKER is the only switch. Unset keeps reminders and forced stops on.
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* chore: rewrite PR template in Chinese with structured sections
* chore: switch PR template to English, add write-pr and review-pr skills
* chore: add mermaid to write-pr, disable model invocation for review-pr, require write-pr skill in AGENTS.md
* fix: restore swarm members from persisted lifecycle events
* test(agent-core-v2): register subagent lifecycle records in the wire vocabulary
* chore(agent-core-v2): regenerate the wire manifest for subagent lifecycle records
The manifest inventory drives the vis wire-renderer coverage check, so vis gains a renderer for each new record type and treats them as timeline-neutral in the context projector.
* fix(transcript): associate a folded subagent with the task registered after its spawn
Some emitters register the task after publishing the spawn, so the durable
spawn record carries no task id and the cold fold keyed the member by its
agent id. A later task.started then created a second task that no subagent
terminal event ever reached, leaving a duplicate stuck in the running state.
Folding task.started now records the agent-to-task association the live
projector keeps, and terminal subagent records update both representations
when both are retained.
* fix(transcript): restore subagent token usage when folding a completed member
The durable subagent.completed record carries the member's token usage and the
live projector copies it onto the task, but the cold fold dropped it, so a
reopened conversation showed a completed member without usage metadata. The
fold now validates the payload's four counters and restores them.
* fix(kap-server): base the lost-member decision on the turn that spawned it
The cold snapshot only terminalised an unfinished foreground member when the
parent agent was idle, so a member left behind by a restart stayed marked as
running forever once the user started a new parent turn before backfill.
The restored agent refs already name the tool call that spawned each member,
so the decision now uses the turn owning that call: a member is lost when its
own loop is not running and its spawning turn is not the turn the parent is
running now.
* fix(transcript): adopt the spawn placeholder into the task registered afterwards
A spawn published before its task is registered — the Tower worker path —
carries no task id, so the fold created a placeholder keyed by the agent id
and the later task.started added a second task under the registered id. Only
the registered task was ever terminalised, and the detached placeholder was
skipped by the lost-member pass, so a restored transcript kept a duplicate
worker shown as running forever. task.started now folds the placeholder into
the registered task instead of leaving it behind.
* fix(kap-server): drain the live wire before reading a cold snapshot
The append log buffers records and writes them in batches, which is why its
own reader flushes the log first. The cold transcript reads the wire file
directly and skipped that drain, so a snapshot could stop on a stale prefix.
When the transcript binding is created after a turn has already ended, the
whole store is built from that snapshot and nothing repairs it afterwards,
leaving the turn's last tool call without its result and its member marked
lost. Reading now drains the live agent's wire first, as session export
already does before it reads the same files.
* feat(tower): enforce protocol invariants and deny AgentSwarm in tower mode
- split missions by functional boundary and workload; simplify spawn briefings
- flip completed missions back to active on non-clean reviews and brief respawns with review history
- refuse completed with open tasks or zero-diff build branches; cap review rounds at 5 with a task_drop escape hatch
- name the next step in review/merge/status tool results; flag unspawned missions in TowerStatus
- veto AgentSwarm execution while tower mode is active
- deflake kap-server catalog test: waitForServerState timeout 3s to 10s
* fix(spawnTool): remove striction
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
* feat(cli): support audience targeting for startup banners
* feat(cli): rename the banner audience field to kfc_audience
* chore: revert unintended oxlint autofixes introduced by the merge commit
The lint-staged oxlint --fix pass applied warning-level unicorn autofixes to
main's files during the merge (prefer-at, prefer-string-replace-all,
toSorted, catch-error-name). The catch-error-name rename also renamed object
shorthand properties (reason -> error), which no longer matches the declared
types and fails tsc. Restore main's versions of these four files; committing
with --no-verify so the same autofix does not re-apply.
* refactor(cli): type the banner wire shape concretely
* refactor(cli): narrow the banner audience fields to literal unions
* fix(cli): classify 403 as anonymous and map REGION_OVERSEA in banner audience
* refactor(cli): require banner_id for banner entries
* fix(cli): drop the banner subtext indent when it cannot fit
* fix(cli): avoid cache filename collisions across config endpoints
* fix(cli): keep banner loading from rejecting when the token lookup fails
* fix(kap-server): reject malformed and null Origins
isOriginAllowed conflated an unparseable Origin header with an absent
one and allowed both, so a sandboxed document (Origin: null) passed the
WebSocket upgrade check and had its Origin echoed by the CORS hook.
An absent Origin is still allowed for non-browser clients, but a
present Origin that fails to parse, is null, or uses a non-http(s)
scheme is now rejected: the WS upgrade responds 403 and the CORS hook
withholds Access-Control-Allow-Origin.
* fix(kimi-code): sync web bundle for kimi_origin query gating
* fix(agent-core-v2): gate workspace local.toml behind trust and tighten additional_dir
* fix(agent-core-v2): block symlink escapes in agent file tools
* fix: harden background git invocations against repo-local config
* fix(agent-core-v2): close trust-gate gaps in add-dir and additional_dir validation
* fix: neutralize repo-configured clean and process filters in hardened git calls
* fix(agent-core-v2): fail closed when a path is too deep to verify
* fix: probe worktree-scoped filter config in hardened git calls
* fix: fail closed when git filter discovery fails and cache probes for repo subdirectories
* fix(agent-core-v2): block symlink aliases of the project-local config in write tools
* fix: neutralize smudge, merge drivers, and repo-controlled commit signing in hardened git calls
* fix: expand included git configs when discovering repo-defined drivers
* Revert "fix(kimi-code): sync web bundle for kimi_origin query gating"
This reverts commit cd6af49cf1dfebe505f0e890453c3207a74d3e80.
* fix: keep git hardening fail-closed on includes and probe failures in scanners
* fix: fail closed on uncodable driver names and disable repo-controlled signature verification
* fix: neutralize repo-controlled editors and match include sections case-insensitively
* fix: probe config names only and reject dangling symlinks in file tools
* fix: fail closed when core.worktree escapes the repository
* fix(agent-core-v2): match project config paths across separators and case
* fix: arm footer git only after workspace trust persists
* fix(agent-core-v2): check resolved sensitive targets for absolute outside paths
* fix: only skip git config caching for actual include sections
* refactor: share git config hardening primitives between agent-core-v2 and the CLI
* fix(agent-core-v2): clear trusted additional dirs synchronously on trust revocation
* fix(agent-core-v2): compare resolved write targets with Windows path semantics
* fix: fail closed when the git directory disappears during hardening probes
* refactor: move shared git hardening primitives into @moonshot-ai/git-hardening
* fix: close symlinked-gitdir and submodule-recurse gaps in git hardening
* fix(agent-core-v2): reject home-directory ancestors as additional_dir roots
* fix: compare core.worktree with Windows path semantics
* fix: skip git config probes on renders after a negative repo detection
* fix: resolve gitfile pointers against the .git file directory and match includeIf sections
* test: drop duplicated symlink and plain-file cases covered elsewhere
* fix: only treat project-config writes as aliased when the lexical path misses the config pattern
* test: move the realpath guard matrix into a module test
* test: cover git-hardening primitives in the shared package
* refactor: inline git hardening helpers and drop the shared package
Keep the constants and parsers next to the async engine spawn path and
the sync CLI spawn path instead of a third workspace package.
* refactor: drop footer git trust gate and config probing
Workspace trust is a process-level decision, so the TUI never mounts
the footer until the user has trusted. Footer git status now uses
static hardening flags only.
* refactor: route git execution through IGitService.runGit
- Add runGit(cwd, args, options) to IGitService as the public door for
running git with repo-local config hardening applied.
- GitService implements runGit using the existing hardenedGitConfigArgs
helper; status/diff now delegate to it.
- collectGitContext drops its own spawn/probe logic and calls
IGitService.runGit instead, receiving the service through the
promptPrefix context.
- Tower keeps its own git() function (accepted gap).
* refactor: move git hardening helpers to app/git/hardening
* Revert "fix(kap-server): reject malformed and null Origins"
This reverts commit b799a21d0390ff0e4e66437f0d7b90c0b8171efe.
* perf: cut CLI startup time and memory in the native binary and config reads
Ship the single-file binary with a V8 code cache and a 16 MB semi-space
(execArgv), lower dynamic import() to require() in the CommonJS bundle and
fail the native build if any survives, and stop the engine from redoing
work on every config read: env-bound sections are re-validated only when
their env values change, the tool policy service caches the global tools
section and refreshes it on section-change events, Ajv instances are built
lazily, the macOS product version is read from SystemVersion.plist instead
of spawning sw_vers, and the host time zone and date formatter are cached.
Measured on the 100-round mock benchmark: CU 4.1-4.4 -> 3.0-3.2,
CPU 2.6-2.8 -> 2.0-2.2 core-seconds, peak RSS 550-610 MB -> 370-465 MB,
`kimi --version` 0.48 s / 401 MB -> 0.33 s / 311 MB.
* fix(native): keep dynamic import() in the code-cached binary and cover plugin entries in smoke
Lowering import() to require() broke `__plugin_run_node` (the bundler dropped
the node:url binding, and require() cannot load file:// URLs or top-level-await
modules), so every plugin MCP server that runs on Node failed under the native
binary. The CommonJS SEA main script keeps a working import() with the V8 code
cache on Node 24, so the lowering and the lexer guard go away and the native
smoke now runs a plugin entry. Also assert that tool policy checks do not read
the tools section, and drop the host time zone cache.
* fix(agent-core-v2): key the env-bound config memo by section identity
A domain re-registered by a different section definition could hit the memo
entry left by the previous definition when the validated base and env values
were unchanged, returning a value parsed by the old schema.
* feat(plugins): update kimi-webbridge plugin to the 2.0.11 skill
Bring the official plugin in line with the Kimi Browser Extension 2.x
daemon: document the ok/data response envelope, self-serve daemon
recovery via start, the movable listen address (10086 is only the
default), file-body retries on macOS/Linux, and the rewritten
operations reference (CLI subcommands, disk layout, new status
fields). Sync the manifest and marketplace versions to 2.0.11 and
point the help links at the new product pages.
* fix(plugins): point the Browser Extension help links at the canonical product path
The site canonicalizes /products/kimi-webbridge to
/products/kimi-browser-extension; ship the final URL in the skill,
manifest, and docs instead of relying on the redirect.
* feat(agent-core-v2): report active plugins and plugin toggles in telemetry
* fix(agent-core-v2): refresh active plugin context before recording plugin_toggle
* refactor(agent-core-v2): publish plugin telemetry from committed state
* refactor(agent-core-v2): split plugin value import from type imports
* refactor(agent-core-v2): report enabled plugins on turn and toggle events
* feat(transcript): include step timing and usage in cold-folded steps
* refactor(transcript): carry only TTFT and stream duration in cold-folded steps
* refactor(transcript): rename step timing field to llmTiming
* docs(skill): list all entries for versions with 10 or fewer upstream entries in sync-changelog
* docs(changelog): sync 2.0.2 from apps/kimi-code/CHANGELOG.md
* chore: sync web dist from code-app
code-app: 44d7281c7a63ee3c7a907f9406e490efdbec7411
* chore: collapse web dist sync changesets into one summary entry
* fix(agent-core-v2): floor the human turn clock at the wire-wide max on engine journal reset
* fix(kap-server): fold the cold transcript snapshot over the restorable branch chain
* fix(agent-core-v2): advance the human turn clock when a turn starts
* fix(kap-server): split reused wire turn ids in the live transcript projector
* fix(kap-server): adopt the completed cold tip for mid-turn attach events
Continuation deltas after a lazy transcript attach belong on the last
cold turn, not a newly split export id.
This reverts 64505e36e3.
The v1 WS + legacy REST + transcript surfaces are alive on main and remain
the single protocol surface; kimi-inspect returns to the transcript-based
data model (keeping #3747's removal of the prompt input, which adapted to
the agent-core-v2 prompt-queue fold).
* fix(agent-core-v2): reuse the queued prompt id for a single steer
A follow-up sent during a turn minted a second context id, so cancel
while the model was still requesting could show the same text twice
and make the host prompt un-undoable. Reuse the queued prompt id when
one prompt is steered, mark that user as in-turn so undo targets the
host turn, and project it as a non-anchor origin for the client.
* fix(agent-core-v2): type in-turn steer origin on the human prompt
The human PromptOrigin type rejected the inTurn field, which failed
typecheck/build and left the generated state manifest stale.
Reserve the context message id when a prompt is steered, emit turn.steer only after that message is appended, and join live/cold transcript plus fork by the id instead of content.
* fix(agent-core-v2): avoid root watch inventory and add a kill switch
Watch concrete candidate paths so a flooded project root cannot fill the V8 heap. Allow disabling every watcher with [watch] enabled or KIMI_CODE_WATCH.
* fix(agent-core-v2): stop watching the whole .kimi-code directory
Watch missing config files on the file path itself so session writes do not reload.
* fix(agent-core-v2): keep session deletion from hanging
Deleting a live session closes it first, and three waits on that path
had no upper bound:
- AgentLifecycleService.remove() checked its 3s quiesce deadline only
after awaiting loop.settled() and the compaction promise, so a loop
that never settled after cancel() blocked agent removal, session
close and delete forever. The settle wait now races the deadline and
falls through to the existing forced-teardown path when it expires.
- HostProcess.kill() on Windows waited for taskkill to exit with no
bound. It now stops waiting after 5s.
- setSessionArchived() awaited a pending resume inside its lifecycle
critical section. A resume queued behind the archive then waited for
the archive while the archive waited for it, wedging the session's
lifecycle chain so that every later operation on the session,
delete included, never started. The resume is now settled before
the critical section is entered.
* fix(agent-core-v2): terminate a taskkill that outlives its timeout
When the 5s wait on taskkill timed out, kill() returned but the spawned
taskkill child stayed alive and referenced, so every hang leaked a
taskkill.exe process and its handle could keep the CLI or server
process from exiting. Keep the handle and, when the timeout wins, unref
it and terminate it.
* fix(agent-core-v2): never apply a compaction summary that arrives after abort
Cancelling a full compaction, or removing its agent, aborts the
compaction's signal, and the abort listener records the cancellation
and detaches the compaction right away. compactionRound() never checked
the signal again after the summary request returned, so a provider
request that finished despite the abort still went on to
applyCompaction(). That rewrote the context of a cancelled compaction,
and since agent removal now stops waiting for the compaction at its
quiesce deadline, it could also write records after the agent was torn
down and recreate files under a deleted session directory. Check the
signal after the last await and before applying the summary.
The print background policy only watched goals, future cron fires, and
background tasks, so a turn steered by a cron fire was invisible to it:
- a fired one-shot task is deleted from the schedule at once, so the
policy saw an empty schedule and returned; teardown then cancelled the
steered turn mid-flight
- a recurring task whose fire comes due during a long turn keeps
reporting the same past fire time (the tick waits for the loop to go
idle), so the anti-spin guard misread a held fire as a wedged tick and
gave up on cron
The policy now checks turnActive (the same loop-state predicate the cron
tick uses) at the top of every round and waits the in-flight turn out
before drawing any quiescence or wedge conclusion.
* feat(agent-core-v2): support api_key_env for provider credentials
Providers can declare api_key_env = "VAR" in config.toml to read their
Bearer token from an environment variable instead of storing it inline.
The value is resolved per request (rotation-friendly); an unset or empty
variable fails with config.invalid naming the provider and variable.
apiKey+apiKeyEnv and apiKeyEnv+oauth are rejected as conflicts.
The /models refresh path resolves the same chain (inline apiKey ->
apiKeyEnv -> env sub-table); a provider whose variable is unset lands in
failed[] without affecting others, and open-platform providers keep the
env declaration instead of persisting the resolved secret.
Custom registries: applyCustomRegistryProvider consumes the api.json env
field — imported providers get apiKeyEnv, the legacy inline apiKey is
dropped, and a hand-edited apiKeyEnv survives refreshes (tracked via
source.envKey). kimi provider add --api-key and the TUI registry token
field are now optional so public registries import without a key.
* docs: document api_key_env provider credentials
* fix(oauth): preserve hand-edited apiKeyEnv on registry refresh and address review nits
* fix(oauth): validate apiKeyEnv conflicts during refresh and address review nits
* fix(oauth): mirror apiKeyEnv in the klient provider contract and harden refresh validation
* fix(oauth): stop auto-binding registry env names to provider credentials
* fix(oauth): preserve hand-edited apiKeyEnv across registry re-imports
* fix(oauth): scope apiKeyEnv preservation to the owning registry
* fix(agent-core-v2): preserve apiKeyEnv across models.dev catalog re-imports
* fix(kap-server): drop apiKeyEnv when the provider update route receives a replacement key
* fix(agent-core-v2): replace provider credential fields on TOML writeback
* fix(kap-server): normalize whitespace-only api_key in the provider update route
* refactor(oauth): unify provider credential rules into shared primitives
A new provider-credential module in the oauth package owns the credential
precedence chain, the mutual-exclusion conflicts, the user-facing messages,
and the write-side tri-state reconciliation; the refresh orchestrator,
agent-core-v2 (model-auth, catalog, auth readiness), and the kap-server
provider routes compose it instead of re-deriving the rules per layer.
applyOpenPlatformConfig now takes credential: {apiKey}|{apiKeyEnv} and
merges onto the existing record, so open-platform refreshes no longer drop
hand-written fields (customHeaders, the env sub-table) on the inline-key
path either, and the refresh's write-then-undo block is gone.
Registry imports converge on removeCustomRegistryEntries +
applyCustomRegistryEntries (prior-snapshot provenance): the exported
capture/restore helpers are gone, the CLI persists a re-import with a
single setConfig instead of per-id removeProvider RPCs, and
default_model/default_provider survive a re-import whenever they still
resolve. kap-server's importCustomRegistry keeps the purge-then-rewrite
two-phase replace the config service's raw-merging writeback requires.
kap-server create/replace routes accept api_key_env (tri-state, mutually
exclusive with api_key), and GET /config plus the provider catalog item
expose the binding. A declared-but-unset api_key_env now fails readiness
checks with config.invalid (was auth.token_missing), matching request-time
behavior. Registry 401/403 without a key prints an authentication hint.
* fix(cli): persist registry re-import removals on both harness generations
The v2 harness's setConfig deep-merges per domain (retaining omitted keys
and skipping undefined defaults), so a shrunk re-import left removed
providers and aliases in config.toml and kept a cleared default dangling.
The CLI and TUI registry flows now share persistRegistryImport: on v2 the
removal phase and the rebuilt records go out as two atomic
replaceConfigSections writes (the writeback merges alias fields from raw
TOML, so the purge must be persisted first); on v1 the removeProvider RPC
cascades the removals before one merge write. defaultModel/defaultProvider
snapshots are captured before any removal so the restore works on both
paths.
* fix(oauth): reject oauth before the legacy env fallback and persist cleared thinking on re-import
refreshProviderModels' env sub-table fallback now throws the same
apiKey+oauth conflict the chat path rejects, instead of authenticating
with the fallback key and rewriting the provider to drop oauth.
persistRegistryImport writes the cleared thinking section through the
replacement payload, and drops the v1 fallback branch — registry imports
always persist through replaceConfigSections, so the removal phase no
longer needs per-id removeProvider RPCs.
* chore: consolidate provider credential changesets into one entry
* chore: trim changeset to the api_key_env entry
* fix: harden provider credential status and registry imports
* fix(oauth): expose browser-safe provider credential rules
* fix(node-sdk): replace serialized provider credentials
* fix(oauth): reject credential conflicts during refresh
* fix: harden custom registry re-imports
Reject reserved provider collisions and credential endpoint drift before mutation. Replace registry-owned sections atomically with compare-and-set guards, and carry the replacement contract through klient and node-sdk.
* refactor(registry): centralize custom registry import ownership
Route CLI and TUI imports through the engine via the typed SDK and Klient operation. Keep registry replacement policy inside the engine and preserve credential edits made while fetching the registry.
Cover the shared entry point with real configuration storage and memory/IPC transport tests.
* test: trim redundant api_key_env and registry-import coverage
* fix(cli): reuse the stored registry key on keyless re-import
Both first-party clients encoded "no key given" as an empty string, but
the engine's fallback chain (options.apiKey ?? registryKeyFromExisting)
only reuses the key parked in the provider's source blob when the option
is undefined. A keyless re-import of a private registry therefore went
out without an Authorization header and failed with 401 from the CLI
and the TUI dialog, while the kap-server REST route (which passes
undefined) reused the stored key fine.
Pass undefined from the CLI handler and the TUI import dialog instead
of normalizing to ''. The dialog's result type now marks apiKey
optional, matching the engine contract.
* test: pin CAS rejection and credential-conflict behavior
The coverage trim went past redundancy in three places, leaving the
PR's headline guards untested. Restore the compare-and-set rejection
path (guarded value changed on disk -> retry error, no write) and the
absent-section guard normalization in config.test.ts, the
apiKey+apiKeyEnv / apiKeyEnv+oauth conflict rejections at runtime
resolution (modelAuth) and at the refresh orchestrator (discovery,
with per-provider failure isolation), and the open-platform refresh
through api_key_env proving the resolved secret never lands in the
provider record. Also add the first direct unit tests for the shared
declaredProviderCredential / reconcileProviderCredentialUpdate
primitives.
* refactor(oauth): share reserved provider id rules between import and refresh
* Delete .changeset/registry-import-concurrent-credentials.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* test: trim cross-layer duplicate registry and credential coverage
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(agent-core-v2): mark MCP servers needs-auth when a tool call returns 401
* fix(agent-core-v2): cover retried MCP calls with unauthorized handling and recheck entry after close
* fix(agent-core-v2): bind MCP unauthorized reports to the originating client
* fix(agent-core-v2): invalidate rejected MCP OAuth tokens when entering needs-auth
* fix(agent-core-v2): preserve concurrent MCP OAuth grants and recheck entry after credential awaits
* fix(agent-core-v2): flip MCP servers to needs-auth before invalidating rejected tokens
* fix(agent-core-v2): compare-and-clear rejected MCP OAuth tokens and tolerate cleanup failures
* refactor(agent-core-v2): move rejected-grant handling into the MCP OAuth service
* fix(agent-core-v2): compare MCP OAuth grant age with the scheduler clock
* fix(agent-core-v2): reject future timestamps from the concurrent MCP grant window
* fix(agent-core-v2): distinguish concurrent MCP grants from freshly rejected ones
* fix(agent-core-v2): ignore application-level MCP errors in the unauthorized sniff
* test(agent-core-v2): trim MCP unauthorized tests to the core contract
* Delete .changeset/mcp-oauth-clear-if-current.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(cli): add /desktop slash command and kimi install-app subcommand
Both entry points print the Kimi Code desktop app page
(https://www.kimi.com/code) and open it in the default browser:
the kimi install-app subcommand from the shell, and /desktop
(alias /install-desktop) from the TUI.
* fix(cli): derive the desktop app page from the active region
Replace the hardcoded .com URL with the existing region-aware
kimiCodeOfficialInstallUrl() resolved at invocation time, so
global-region users land on the .ai site and region changes are
respected.
* docs(cli): describe the desktop app page URL as region-dependent
* feat(agent-core-v2): add skill scopes and mark custom-theme as tui-only
Builtin skills can now declare a scopes whitelist (tui | web); skills
without scopes stay visible everywhere. The value is carried through
SkillSummary, the klient RPC contract, and the kap-server REST wire,
and the TUI slash commands filter by it. HTTP clients (code-app) drop
skills whose scopes exclude their own UI mode, so TUI-only skills like
/custom-theme no longer leak into app/web.
* fix: filter scoped skills from the acp and vscode palettes
ACP slash commands and the VS Code GetSlashCommands handler also
project SkillSummary lists; both now drop skills that carry a scopes
restriction so tui-scoped skills stay out of non-tui clients.
* fix(agent-core-v2): watch Windows 8.3 short paths through their long form
Watching a directory given in 8.3 short form (for example
C:\Users\RUNNER~1\...) makes libuv's Windows file watcher abort the process
with "Assertion failed: !_wcsnicmp(filename, dir, dirlen)" in fs-event.c,
because change notifications arrive under the long path.
On Windows, resolve a watch root containing a short-name segment to its long
form (the nearest existing ancestor when the root does not exist yet) and map
reported changes and ignore checks back to the requested path, so callers that
compare change paths with their own keep working.
* test(agent-core-v2): keep watch tests portable to Windows runners
* test(agent-core-v2): show received events when the Windows short path watch test fails
* test(agent-core-v2): derive the Windows short path without cmd quoting
* chore: add changeset for the Windows short path watch fix
* fix(agent-core-v2): keep watch children whose names start with two dots inside the root
relative() returns "..cache/x" for a child named "..cache", which the
startsWith('..') check treated as a path outside the watched root. Mapped
change paths then kept the resolved long form, and native signal events for
such children skipped the ignore predicate. Only a bare ".." segment, ".."
followed by a separator, or an absolute result is outside the root.
Store the optional metadata object on prompt submission and skill activation as opaque client metadata on the user or skill-activation origin, carry it through prompt events, the transcript projector, snapshots and history rebuilds, and echo it on prompt items without adding it to model content. Use display_text entries for session titles, undo labels and fork titles when every entry provides one. Project user turn payloads, live and rebuilt, through a path-free projection.
Accept skill_activation origins with a user-slash trigger in onTurnSteered so a slash command sent while a turn is running gets a transcript frame, and keep those frames from draining a queued prompt's id. Project origin file attachments onto steered frames on both the immediate and deferred paths, matching onTurnStarted and the history rebuild.
Widen activateSkillAttachmentSchema so the attachments array accepts text parts alongside image, video and file, letting a caller interleave text and media in the order the model should read them. Schema only; the route already resolves attachments as WireContent.
Media name preservation already ships: the REST schemas expose a name on
image and video content, and kap-server reads imageUrl.name and videoUrl.name
when projecting messages and prompt content. The kosong part types never
declared the field, so it survived only because object literals keep excess
properties through an assignment. Consumers that read these types directly
cannot see the name at all.
Declare it where the other optional part fields live. No runtime change; the
existing media name projection tests continue to cover the behavior.
* feat(agent-core-v2): upload images as file references for Kimi models
* fix(agent-core-v2): scope media upload cache by endpoint and recognize SDK auth error status
* fix(agent-core-v2): restore the stripped /v1 for kimi files uploads on the anthropic route
* feat(agent-core-v2): omit oldest media when accumulated inline media exceed the request budget
* fix(agent-core-v2): key the media upload cache by the stable JWT subject
* fix(agent-core-v2): harden media request recovery
* fix(agent-core-v2): correct image memo accounting
* feat(agent-core-v2): include saved paths in dropped media placeholders
* docs: drop the saved-path re-read guarantee from the media budget note
The guarantee does not hold for inline data-URL media (no persisted
path exists), and the runtime warning already conveys path availability
when it applies. Addresses the codex P2 review suggestion by removal.
* fix(tui): stop /usage and /status from prompting login before the first message
* docs(changeset): simplify the /usage fix entry
* fix(tui): mute the session-usage error line and cover the session-less path in tests
* docs(changeset): restore the confirmed wording
* perf(tui): batch and cache agent swarm progress rendering
* perf(agent-core-v2): evict completed subagent scopes behind an LRU cache
* fix(agent-core-v2): stop the agent actor in the session machine when its scope is removed
* fix(agent-core-v2): emit a terminal event for the final rate-limited run and defer eviction while background tasks run
* fix(agent-core-v2): include in-flight retries when abandoning suspended agents
* fix(agent-core-v2): roll back the closing flag when scope removal fails and wait per the configured eviction timeout
* fix(agent-core-v2): terminalize subagents spawned after batch cancellation and report task timeouts as failures
* chore: restore upstream/main state of files mangled by lint-staged autofix in the merge commit
oxlint --fix rewrites (prefer-set-has, catch-error-name, prefer toSorted/.at(-1)/toReversed)
touched seven files the branch never modified; one of them (oauth managed-kimi-code.ts)
no longer typechecks. Restore all seven to their exact upstream/main state. Committed
with --no-verify so the same autofix does not re-mangle them.
* fix(agent-core-v2): retry deferred scope evictions without exhausting the attempt budget, dedupe terminal events for starting retries, and patch-level the changeset
* fix(agent-core-v2): finish scope teardown instead of reactivating a partially torn-down scope
* fix(agent-core-v2): terminalize subagent-start hook cancellations and re-acquire resume targets after metadata reads
* fix(tui): record cancelled background agents in the transcript
* fix(agent-core-v2): route mirror terminal events through the batch exactly-once guard
* fix(tui): guard terminal swarm members from re-cancellation and deliver terminal status to cancelled foreground subagent cards
* fix(agent-core-v2): classify eviction as removed when teardown already closed the scope
* Merge remote-tracking branch 'upstream/main' into perf/subagent-scope-eviction
Skip lint-staged for this merge: its oxlint --fix pass rewrites unrelated files
(prefer-set-has etc. mangled seven files in the previous merge commit), while the
merge content is upstream's lint-clean code plus conflict resolutions already
verified by oxlint.
* chore: restore upstream/main changes dropped by a stash roundtrip during the merge
A git stash/pop cycle used to isolate an unrelated flaky test reverted 82 files of the
merge result to the pre-merge branch state, so commit 375ff2a4a missed them. The
working tree already held the correct merged content; this commit restores it.
* fix(agent-core-v2): propagate the cancellation reason from turn abort to tool signals
Cancelling a turn aborted the turn's llm scope and every tool scope
without a reason, so tools and subagents only ever saw the default
AbortError and could not tell a user cancellation from a system abort.
With the swarm terminal-event classification this surfaced as
`subagent.failed` ("This operation was aborted") on Ctrl+C instead of
`subagent.cancelled`.
Carry the reason end to end: `MachineEngine.abort(reason)` sends it on
`input.abort`, the agent machine remembers it for the turn and forwards
it to `turn.abort` and to every tool scope it aborts, the turn machine
passes it to the llm scope, and the loop service hands its cancellation
(a `UserCancellationError` by default) to the engine.
* fix(agent-core-v2): stop rebuilding an evicted subagent after its caller aborts
`createAgentAwaitingClose` polled for the previous scope to finish
closing without ever looking at the caller's abort signal, so cancelling
an `Agent` or `AgentSwarm` resume during that wait still recreated the
scope afterwards. In the `Agent` path the run then rejected before any
terminal event was mirrored, leaving an idle scope that the scope cache
no longer tracked.
Thread the caller's signal into the helper and check it before every
attempt; the Agent tool passes its call controller and the swarm service
passes the attempt signal. Also trim the eviction changeset to the one
sentence the changeset rules ask for.
* fix(agent-core-v2): report task-manager aborts of a subagent as cancelled
When the task manager times out or stops a background subagent it aborts
the run with a plain reason ('Timed out', the TaskStop reason), and the
run rejects with that raw reason. The task manager therefore classifies
the task as timed_out or killed, but the mirror only recognised
`UserCancellationError` as a cancellation and emitted `subagent.failed`,
so the transcript recorded a failure before the authoritative
`background.task.terminated` event could land.
Classify a terminated run by the shape of its abort reason: a real
Error that is not an AbortError (the swarm's own per-item timeout) stays
a failure, everything else that aborted the run is a cancellation. The
swarm's start-failure path uses the same rule.
* fix(agent-core-v2): flush the wire before evicting a subagent scope and re-pick LRU candidates
Two gaps in the scope-cache eviction pass:
- `AgentLifecycleService.remove()` swallows a failed wire flush and still
tears the scope down, so an eviction that hit a disk-full or transient
I/O error would drop the in-memory scope and let a later resume rebuild
from a stale wire. Flush the agent's wire explicitly before removing;
if that fails, keep the scope resident and retry on a later pass. The
idle and task checks are repeated after the flush so a resume that
started meanwhile is not cancelled by the removal.
- The pass snapshotted its candidates up front and re-validated them by
attempt count only, so a scope that was resumed and completed again
while an earlier eviction was awaited kept matching with attempts 0 and
was evicted despite now being the most recently used. Pick the oldest
eligible entry from the live map on every step instead, skipping only
the entries this pass already tried.
---------
Co-authored-by: Kaiyi <me@kaiyi.cool>
* fix(plugins): rename WebBridge display name to Kimi Browser Extension
The product has renamed WebBridge to Kimi Browser Extension in work and
Chrome. Update the user-facing display name across the built-in
capability entry, TUI plugins selector, managed plugin manifest,
marketplace catalog, skill docs, and user docs to match. Internal ids,
binary names, CDN paths, and daemon references are unchanged.
* fix(plugins): bump webbridge to 1.11.4 and keep legacy docs anchor
Address review: point the changeset at the scoped CLI package, bump the
renamed plugin's manifest/catalog/docs version so installed users are
offered the renamed package as an update, and preserve the published
#kimi-webbridge documentation anchor beside the renamed heading.
* docs(plugins): match only the known extension-update error messages
Broadening the version-mismatch trigger to the generic prefix 'Please
update the' would misclassify unrelated tool errors as an outdated
browser extension; match the two known full messages instead.
---------
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
The platform /usages payload now serves per-window used ratios
(goods_version, a usages map of limit5h/limit7d/monthTotal/monthCode
entries with used_ratio/reset_time, and boosterWallet) instead of
absolute used/limit rows.
- oauth: parse and fetch the quota model; keep the platform helpers
- toolkit / agent-core-v2 / node-sdk: getManagedUsage returns the quota
- kap-server: /oauth/usage passes the camelCase quota shape through
(the legacy snake_case wire is gone)
- TUI /usage: plan rows are presence-driven from the quota payload;
the monthly row carries a kimi/code breakdown line
- docs: server-api reference updated for the new response shape
* docs(web): add common slash commands table and rename startup options section
* docs(interaction): fold the goal mode guide into the interaction page and remove the orphaned page
* feat(agent-core-v2): fail fast on unsupported [secondary_model].default_effort
* fix(agent-core-v2): reject default_effort off for always-thinking pool models
* fix(agent-core-v2): surface adaptive_thinking on the assembled catalog model
* perf(tui): batch and cache agent swarm progress rendering
* perf(tui): honor the render-cache escape hatch for cell memoization
* fix(tui): stop swarm frame ticks after tool call end and cap retained label code units
* fix(tui): house swarm label caps in TUI constants and stop splitting surrogate pairs
* fix(tui): close OSC 8 hyperlinks sliced by the swarm label storage cap
* fix(tui): reset SGR styling sliced by the swarm label storage cap
* fix(tui): back the swarm label storage cap off to grapheme cluster boundaries
* test(tui): verify swarm terminal-state retention through the render seam
* perf(agent-core-v2): shard the session event bus by agent id
* fix(agent-core-v2): preserve full-stream-before-typed ordering in agent channels and render perAgent counts in the inspector
* fix(agent-core-v2): deliver agent full streams before session typed handlers and dispose sharded channels with the bus
* fix(agent-core-v2): prevent rebuilding agent channels after the event bus is disposed
* fix(agent-core-v2): isolate agent channels on generation replace and keep typed handlers on the shared per-type channel
* fix(agent-core-v2): require the active lifecycle context before firing an agent full-stream channel
* fix(agent-core-v2): reject stale lifecycle contexts when subscribing to an agent full-stream channel
* fix(agent-core-v2): honor the agent context in typed subscribeAgent subscriptions
* fix(agent-core-v2): release disposed agent scopes from the DI dependency graph
* fix(agent-core-v2): drop retired service instances from the tracked edge nodes and the dependency graph
* fix(agent-core-v2): resolve tower roster identities to the latest registration
Agent ids restart from agent-0 in each session, so a freshly spawned
worker collided with the previous session's roster entry and every
find-first lookup (caller identity, write guard, death bookkeeping)
resolved to the stale entry: workers read the wrong inbox, sent under
the wrong name, and were denied their own writes, mission updates, and
reviews.
- resolveAgent picks the latest roster entry for an agent id; all
callers (resolveCallerName, write guard, resume veto, died/revived)
go through it
- registerAgent retires same-agent-id entries before appending
- enter() (/tower on) now adopts the workspace roster itself instead of
relying on a manual TowerInit
* fix(agent-core-v2): adopt the tower roster when a replayed tower mode finds a dead foreign owner
A forked or restored session replays the durable TowerModeEnter into an
active tower mode without ever calling enter(), so the roster adoption
added there never ran on the restore path: with the previous owner
session gone, the fork kept the mode (and the stale roster) until a
manual TowerInit. The dead-owner branch of the restore-time
reconciliation now adopts the workspace, mirroring enter()'s takeover
of an idle owner; a live foreign owner still causes an exit.
* fix(agent-core-v2): await the outgoing tower owner's release before adopting its roster
Taking tower mode over from a live but idle owner called the owner's
synchronous exit(), whose ownership release ran fire-and-forget, and
immediately adopted the roster — two concurrent load-modify-save cycles
on .tower/comms/state.json that could lose the new owner or resurrect
the stale roster. exit() now awaits the ownership release and the
takeover awaits the owner's exit(); the remaining callers either await
(RPC/HTTP tower-mode-off) or explicitly ignore the promise (mode mutex,
restore-time self-exit).
* fix(agent-core-v2): fail tower activation when the roster adoption cannot complete
Adoption failures (unreadable or unpersistable state.json) were
swallowed into a log warning, so enter() and the stale-owner restore
both activated tower mode with the foreign owner and stale roster still
in place — the exact identity mismatch the adoption is meant to
prevent, now with a false success report. adoptTowerRoster() now throws
a TowerProtocolError with the underlying cause: enter() propagates it
before any tool is activated, and the restore path drops the replayed
tower mode instead (it must not throw inside the restore hook chain).
* fix(agent-core-v2): propagate unreadable tower state during roster adoption
adopt() treated any state.json read failure as an uninitialized
workspace via isInitialized()'s catch-all, so an existing but
unreadable state file (EACCES, EISDIR, transient I/O) reported a
successful no-op adoption and enter() activated tower mode against the
foreign state — bypassing the activation guard added in the previous
commit. Only ENOENT now means uninitialized; every other read error
propagates into the fail-activation / deactivate-on-restore paths.
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
* docs(skills): clarify flat-form skill naming and its limits
The flat-form description "Skill name is taken from the filename (minus
.md)" read as if the on-disk file should drop the extension, which
repeatedly confused readers. Rewrite the Creating-a-Skill section with
a concrete directory layout, explicit name-derivation rules for both
forms, the directory-over-flat precedence, and the flat form's top-level
scanning and KIMI_SKILL_DIR limitations.
* docs(skills): attribute directory-form names to the required frontmatter
parseSkillText requires name in directory-form SKILL.md and always
registers under it, so the subdirectory name is only a convention, not
the source of the Skill name. Reword the derivation bullets accordingly
(flat form keeps its filename fallback).
---------
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
* feat(oauth): add the dynamically_loaded_tools capability from the official models endpoint
* feat(agent-core-v2): support per-server deferred disclosure of MCP tools
* feat(klient): mirror per-server deferred in the MCP config contract
* fix(agent-core-v2): honor per-server deferred for the needs-auth MCP tool
* fix(agent-core-v2): register select_tools regardless of the profile tool allowlist
* feat(agent-core-v2): default MCP servers to inline tool disclosure unless deferred is enabled
* refactor(agent-core-v2): fold the prompt queue into the agent loop service
Delete AgentPromptService/IAgentPromptService: prompt submit/queue/steer/
abort now ride the agent machine queue through IAgentLoopService's extended
surface (submitPrompt/submitSteerPrompt/enqueuePrompt/steerPrompts/
abortPrompt/drainPrompts/injectPrompt/retryPrompt/promptQueue plus
hooks.onBeforeSubmitPrompt and the promptAdmission reservation slot).
Durable prompt.* event classes move to agent/prompt/promptEvents.ts and keep
their registrations; promptAdmissionKey keeps its fold. Dead assets go away:
promptResolutionKey, PROMPT_ALREADY_COMPLETED (kap-server 40903 path),
promptLaunchingKey and clear(). Compaction gating (queue while compacting,
resume on finish) moves into the loop reservation drain.
The 'agentPromptService' wire name keeps its shape: a thin AgentPromptChannel
facade (same decorator id) forwards submit/submitSteer to the loop, so
klient memory/ipc/debug transports and the kap-server dispatcher are
unchanged.
* test(agent-core-v2): re-pin prompt queue semantics on the loop service
Rewrite promptService.test.ts (25 cases, count unchanged) against the real
AgentLoopService with the scripted-LLM harness, fold the launch-window
reservation out of promptQueue().pending to match the old snapshot contract
exactly, and update the debug-channels pin to the facade's submit/submitSteer
surface.
* refactor(agent-core-v2): lint-align the loop prompt queue surface
* fix(agent-core-v2): settle prompt completions on the turn-result chain
Match the deleted service's microtask position: prompt.completed/aborted and
the completion deferreds fire from a result.then chain attached at bind time
instead of synchronously inside endTurn, so wire/emit ordering with
post-turn producers (tool unregister, next-prompt launch) stays identical to
the baseline snapshots. Steered children move onto the reservation so the
chain can settle them after the active turn is gone.
* refactor(agent-core-v2): move the prompt-submit gate into the agent machine
* refactor(agent-core-v2): keep image compression captions inline in prompt messages
* refactor(agent-core-v2): drop the durable prompt admission facility
* refactor(kap-server): resolve prompt auth at turn runtime instead of submit time
* refactor(agent-core-v2): teach the agent machine queue metadata, multi-id steer, gate rewrite, and pause
* refactor(agent-core-v2): converge prompt submission on machine queue user entries
* refactor(agent-core-v2): replace loop prompt records with a waiter registry fed by machine events
* feat(agent-core-v2): make the compaction attempt limit configurable
Add loop_control.compaction_max_attempts (default 5) to control the
maximum total attempts for a failing compaction request, replacing the
hardcoded MAX_COMPACTION_RETRY_ATTEMPTS constant. The value flows
through ProfileModelContext into the compaction retry loop, covering
both retryable generation errors and empty/truncated response shrinks.
* fix(agent-core-v2): bound compaction shrink attempts and compute retry delays lazily
Address codex review on #3750: the empty/truncated shrink path allowed
maxAttempts + 1 requests (throwing only when the shrink counter exceeded
the limit), and the backoff delay array was allocated eagerly with
maxAttempts - 1 entries before the first request. The shrink path now
stops at the configured limit, and each delay is computed lazily from
the retry index.
* fix(agent-core-v2): count every compaction request against the shared attempt limit
Address the second codex review round on #3750: the retryable-error and
empty/truncated recovery paths kept separate counters, and the retry
budget reset after each shrink, so alternating failure modes could issue
more requests than compaction_max_attempts. A single requestAttempts
counter now gates all recovery paths (the overflow shrink path keeps its
own tighter cap of 3), making the configured value a true maximum on
total requests per compaction round.
* feat(agent-core-v2): persist the human runtime mirror as human.* records in the wire journal
Introduce mode-2 coexistence: the machine eventStore journal becomes a
tree-projection-filtered adapter over the same wire file (single writer via
the WireService append queue), the engine switches from memoryJournal to the
file journal with a seed fallback while no human records exist, and cold
readers skip the new record family constructionally. Adds downgrade
round-trip e2e coverage, whitelists agent.switched/context.undone from
unknown-record telemetry, asserts the undo switch triple after flush, and
resets the machine engine after an undo rerun so machine and wire turn
clocks stay aligned.
Squashed from:
refact(agent-core-v2): project wire journal as an in-memory branch tree behind IAgentJournal
refact(agent-core-v2): undo as branch switch with dual-emitted legacy record and restore replay
refact(agent-core-v2): queue late attachments during restore rerun and flatten fork sources to the active chain
feat(agent-core-v2): persist the human runtime mirror as human.* records in the wire journal
* feat(agent-core-v2): add full compaction to the human agent domain
* refactor(agent-core-v2): keep pending input state events out of the wire journal
* refactor(agent-core-v2): flip agent DI scope ownership to session-spawned agent actors
* refactor(agent-core-v2): build the agent wire and blob views in the session layer
* refactor(agent-core-v2): persist human domain records under the agent.* wire namespace
- replace the human. record prefix with an explicit whitelist (AGENT_WIRE_RECORD_TYPES) guarded against the Event2 vocabulary; legacy human.agent.* journals stay readable through a transition prefix
- unknown-type records skipped during restore now go to the log file instead of the console (malformed records still report to the console)
* refactor(agent-core-v2): compose the llm recovery chain outside the turn machine
The turn machine hardcoded credentialsRecovery as the head of its
recovery chain and interpreted a refreshCredentials flag on proposals,
coupling the turn to the credentials domain. Strategies now return
self-describing records (strategy/action, optional messages, optional
opaque prepare effect), the turn only runs prepare and/or swaps
messages, and the engine composes the chain via chainRecoveries.
* refactor(agent-core-v2): inline the two-strategy recovery composition
With self-describing proposals the chain is a one-liner at each call
site; the named chainRecoveries combinator had only two call sites and
no third strategy to justify it.
The Remote Control HTTP tunnel validated each base64 message with a
backtracking regular expression. Requests larger than ~3.5MB (base64
~5MB) exhausted the V8 Irregexp backtracking stack and threw
RangeError, which the catch-all mapped to a 400 with an empty body,
surfacing as a generic connection failure on the web client.
Replace the regex with an O(n) character check, reject oversize
requests from the base64 length before decoding, and map non-SyntaxError
failures to 502 instead of 400.
* fix(agent-core-v2): rebuild session and search indexes on unrecoverable storage failures
* fix(agent-core-v2): propagate search sync truncation and track session count in index freshness
* fix(minidb): wipe derived stores under their own locks during rebuild
* fix(agent-core-v2): isolate wiped stores, invalidate stale projections, and account sync failures
* feat(agent-core-v2): replace session index freshness scans with a dirty journal and targeted reconciliation
* fix(agent-core-v2): split transient failures by op kind, surface failed startup reconciliation, and cool down search session skips
* feat(agent-core-v2): allow rm -rf targeting /tmp and /temp paths
The dangerous command guard flagged every recursive force rm regardless
of target. rm -rf now falls through without an ask when all operands are
literal paths under /tmp or /temp (segment-level prefix, no .. escape);
mixed, non-literal, or out-of-prefix targets stay dangerous.
* chore: add changeset for rm -rf temp path allowance
* feat(remote-control): compress textual tunnel responses with gzip
* fix(remote-control): honor explicit gzip exclusion over accept-encoding wildcard
* fix(remote-control): skip gzip for 206 responses and vary by accept-encoding
* fix(remote-control): compress tunnel responses off the event loop
* fix(remote-control): vary identity responses and version compressed etags
* fix(remote-control): drop etags from gzipped tunnel responses
* fix(agent-core-v2): honor request-level toolMessageConversion in openai formats
The requester already forwarded LlmRequestConfig.toolMessageConversion
into formatRequest, but the openai chat and openai-responses formats
only read the trait hook, so the explicit request config was silently
ignored. Resolve the mode as request config, then trait default, then
the protocol default, and pass the resolved value into lowering.
* refactor(agent-core-v2): split the protocol trait into per-protocol dialects and provider connection
The ProtocolTrait interface bundled endpoint/headers connection config,
model capability, message/params conversion hooks, and error
classification into one bag, and every format received the whole bag
whether or not it consumed each hook — hooks a protocol ignores were
accepted by the type system and silently dead at runtime.
Split it by consumer:
- ProviderConnection (protocol/connection.ts): endpoint env
declaration and default headers, still resolved per request inside
generate.
- Per-protocol typed dialects (OpenAIDialect, OpenAIResponsesDialect,
AnthropicDialect, GoogleGenAIDialect): only the customization points
each protocol actually consumes; data-shaped hooks (reasoningKey,
toolCallIdPolicy, toolMessageConversion, strictThinkingValidation)
are data fields, and message/history hooks use the protocol wire
types instead of Record<string, unknown>.
- convertError moves to a requester option, capability to a provider
variant field.
Dialects are bound when the format is created (createOpenAIFormat and
siblings), so FormatRequestInput and the stream parser carry request
data only; ProtocolTrait is deleted. The thinking hook returns the
kwargs and the preserveThinking flag together as ThinkingApplication,
and the kimi dialect emits the final thinking params directly instead
of routing them through an extra_body flatten in buildParams. The
llm-adapter layer is migrated to the same {connection, dialect,
convertError} assembly.
* refactor(agent-core-v2): compose format stages and dialect hooks in the requester pipeline
* refactor(agent-core-v2): rename protocol dialects to traits
* fix(agent-core-v2): include the system message in openai mergeHistory input
* refactor(agent-core-v2): enforce the format/trait contract boundary
- move CONTEXT_MANAGEMENT_BETA into anthropic/contract and have the kimi
trait import wire types from each protocol's contract.ts instead of
format/lower; export the four contract modules from human/index.ts and
guard the boundary in check-import-boundaries
- rename protocol/trait.ts to protocol/thinking.ts and move TraitContext
to protocol/base.ts; clean up the remaining dialect-era test variable
- type extractUsage as OpenAIRawUsage / OpenAIResponsesRawUsage instead of
Record<string, unknown> and drop the requester-side casts
* refactor(agent-core-v2): thread the request config by spread and drop dead format types
- the four requesters spread LlmRequestConfig into the plan input instead
of hand-enumerating fields, so a new cross-protocol config field cannot
be silently dropped per protocol
- ProtocolFormat loses the phantom TRequest/TResponse type parameters;
the dead OpenAIRawResponse/AnthropicRawResponse/GoogleRawChunk contract
types go with them
- the thinking test now names the extra_body flatten behavior explicitly
* refactor(agent-core-v2): keep protocol format modules internal to the requester pipeline
- drop the four format barrels (and openai/reasoning-key) from
human/index.ts; each base's public seam is now exactly
contract / trait / requester
- stop re-exporting contract wire types from format/lower modules so the
neutral vocabulary has a single home; move the *LoweredMessage staging
types out of contract into the owning format module
- guard the seam in check-import-boundaries: only llm/requester/bases
code and tests may import format/lower/patterns/reasoning-key
* refactor(agent-core-v2): plug llm credentials in through request config
Replace the withAuth/withAuthUpload requester decorators with a
credential contribution point on LlmRequestConfig: the llm machine's
request actor (machine path) and ModelRequesterImpl (direct path)
resolve credentials per attempt, and a recoverable 401 is recovered by
invalidating and re-resolving — emitted as llm.recovering on the turn
machine's credentials branch.
* fix(agent-core-v2): surface request-actor failures as llm.failed.remote
A rejection inside the llm machine's detached request actor (credential
resolution, message resolvers, or a throwing requester) could not reach
the machine, leaving the turn stuck in thinking. Convert it to an
llm.failed.remote event so the turn lands in its failed state.
* docs(agent-core-v2): attribute retry/recovery to the turn machine in llm.md
* refactor(agent-core-v2): unify credentials in human/credentials, turn-owned 401 recovery
* fix(agent-core-v2): credential recovery for direct paths, abort guard, turn-snapshotted credentials
* feat(agent-core-v2): IModelCatalog.generate with stream credential recovery
* refactor(agent-core-v2): inline credential recovery at call sites
Remove the attemptWithCredentialRecovery/streamWithCredentialRecovery
helpers from human/credentials; the single-retry recovery is now written
out at each direct call site (catalog ping/generate, full compaction,
media upload) so the control flow reads linearly without a wrapper hop.
The credentials module keeps only the provider factories and the
credential application helpers; recoverability and invalidation stay on
the LlmCredentialProvider itself.
* fix(agent-core-v2): settle the queued turn when a machine turn settles before gating
When the llm machine fails before requester.generate() runs — for
example when OAuth credential resolution rejects — the loop cleared the
pending machine turn marker on turnSettled without ever binding or
settling the queued reservation, leaving the submitter's ready/result
promises and settled() hanging. The loop now settles the unbound
reservation explicitly: cancelled on abort, failed otherwise, mirroring
the evaluateSettle error propagation.
* fix(agent-core-v2): bind and end pre-gate failures through the normal turn lifecycle
The direct settlement resolved turn.result without endTurn(), so no
TurnPrompt/TurnStarted/TurnEnded/AgentErrorEvent was published and the
next queued reservation was never launched. The unbound reservation is
now bound via beginActiveTurn and ended via endTurn (failed, or
cancelled on abort), chained onto afterChain when the previous turn is
still settling, so pre-gate failures get the same lifecycle events and
queue progression as any other failed turn.
* fix(agent-core-v2): settle seeded turns on pre-gate failure, preserve coded credential errors, close steps on recovering
- Notification-seeded machine turns that fail before gating now bind a
seeded reservation and end through endTurn like queued ones, instead
of leaving the nudge pending and settled() hanging
- The llm machine's request actor attaches the raw error to
llm.failed.remote and the turn machine prefers it for its terminal
failure, so coded errors (e.g. auth.login_required) survive to
endTurn's error payload
- projectMachineEvent now closes the current machine step on recovering
exactly as it does on retrying, keeping step.begin/step.end balanced
when credential recovery re-enters the request
* fix(kap-server): add generate to IModelCatalog test fakes
* fix(klient): migrate examples to the credentials API
* test(agent-core-v2): drop credential recovery coverage
* refactor(agent-core-v2): fold kimi-oauth credential adapter into credentials/
* refactor(agent-core-v2): pair credential refresh with invalidate, optional Model.credentials, abort-safe request actor
- oauthCredentials.invalidate() now starts the forced refresh eagerly and
hands the pending token to the next resolve(), so the force signal can no
longer leak into or be consumed by unrelated resolves
- Model.credentials becomes optional, dropping the {} as never /
staticCredentials(undefined) filler in test fakes
- the llm machine request actor swallows abort outcomes instead of
reporting llm.failed.remote, so an aborted turn settles as aborted
rather than failed
* refactor(agent-core-v2): credentials recovery strategy chain, shared credential-recovery executors, single-proposal failure triage
* fix(agent-core-v2): settle message-less notifications on pre-gate failure, recover credentials in media resolver uploads
* test(kap-server): add the IModelCatalog.generate stub to the history suite
* fix(agent-core-v2): honor request-level toolMessageConversion in openai formats
The requester already forwarded LlmRequestConfig.toolMessageConversion
into formatRequest, but the openai chat and openai-responses formats
only read the trait hook, so the explicit request config was silently
ignored. Resolve the mode as request config, then trait default, then
the protocol default, and pass the resolved value into lowering.
* refactor(agent-core-v2): split the protocol trait into per-protocol dialects and provider connection
The ProtocolTrait interface bundled endpoint/headers connection config,
model capability, message/params conversion hooks, and error
classification into one bag, and every format received the whole bag
whether or not it consumed each hook — hooks a protocol ignores were
accepted by the type system and silently dead at runtime.
Split it by consumer:
- ProviderConnection (protocol/connection.ts): endpoint env
declaration and default headers, still resolved per request inside
generate.
- Per-protocol typed dialects (OpenAIDialect, OpenAIResponsesDialect,
AnthropicDialect, GoogleGenAIDialect): only the customization points
each protocol actually consumes; data-shaped hooks (reasoningKey,
toolCallIdPolicy, toolMessageConversion, strictThinkingValidation)
are data fields, and message/history hooks use the protocol wire
types instead of Record<string, unknown>.
- convertError moves to a requester option, capability to a provider
variant field.
Dialects are bound when the format is created (createOpenAIFormat and
siblings), so FormatRequestInput and the stream parser carry request
data only; ProtocolTrait is deleted. The thinking hook returns the
kwargs and the preserveThinking flag together as ThinkingApplication,
and the kimi dialect emits the final thinking params directly instead
of routing them through an extra_body flatten in buildParams. The
llm-adapter layer is migrated to the same {connection, dialect,
convertError} assembly.
* refactor(agent-core-v2): compose format stages and dialect hooks in the requester pipeline
* refactor(agent-core-v2): rename protocol dialects to traits
* fix(agent-core-v2): include the system message in openai mergeHistory input
* refactor(agent-core-v2): enforce the format/trait contract boundary
- move CONTEXT_MANAGEMENT_BETA into anthropic/contract and have the kimi
trait import wire types from each protocol's contract.ts instead of
format/lower; export the four contract modules from human/index.ts and
guard the boundary in check-import-boundaries
- rename protocol/trait.ts to protocol/thinking.ts and move TraitContext
to protocol/base.ts; clean up the remaining dialect-era test variable
- type extractUsage as OpenAIRawUsage / OpenAIResponsesRawUsage instead of
Record<string, unknown> and drop the requester-side casts
* refactor(agent-core-v2): thread the request config by spread and drop dead format types
- the four requesters spread LlmRequestConfig into the plan input instead
of hand-enumerating fields, so a new cross-protocol config field cannot
be silently dropped per protocol
- ProtocolFormat loses the phantom TRequest/TResponse type parameters;
the dead OpenAIRawResponse/AnthropicRawResponse/GoogleRawChunk contract
types go with them
- the thinking test now names the extra_body flatten behavior explicitly
* refactor(agent-core-v2): keep protocol format modules internal to the requester pipeline
- drop the four format barrels (and openai/reasoning-key) from
human/index.ts; each base's public seam is now exactly
contract / trait / requester
- stop re-exporting contract wire types from format/lower modules so the
neutral vocabulary has a single home; move the *LoweredMessage staging
types out of contract into the owning format module
- guard the seam in check-import-boundaries: only llm/requester/bases
code and tests may import format/lower/patterns/reasoning-key
* feat(kap-server): add flat entity message protocol (v3 WS + history API)
* feat(kap-server): add task notification user messages, skill origins, and history has_more
* fix(kap-server): page history by turns so oversized turns stay reachable
* fix(kap-server): synthesize foreground subagent tasks in the cold fold when no task records exist
* fix(kap-server): dedupe queued prompts for turn records without a prompt id
* fix(kap-server): adapt v3 projection to agent-core-v2 API changes and filter empty thinking
* feat(kap-server): align v3 protocol with design revision 1094 and add agent.state
---------
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
* fix(remote-control): carry server token in rc local UI link
- add required localServerToken to RemoteControlOutputOptions
- render the Local UI line with buildOpenableUrl and dim the #token= fragment like the ready banner
- pass the resolved token at both rc call sites (kimi web --rc, TUI /rc)
* chore(changeset): add changeset for rc local UI token fix
* fix(remote-control): align rc output test with token-bearing local UI link
- assert the Local UI line carries the #token= fragment
- scope the token-free ban to the relay and session URLs, which must never leak the local server token
* fix(agent-core-v2): tower resume, wake, teardown, and mission-resolution reliability
- Require run_in_background=true in every roster-agent resume guidance and veto foreground roster resumes, abstaining when the profile has no background task tools; resume ids are trimmed before the roster lookup.
- Wake the tower when workers send inbox messages to it or broadcast, coalesced into a single notification per batch.
- Make teardown idempotent: worktrees git no longer knows are reported as already removed instead of failing.
- Resolve branch-to-mission unambiguously (closed missions skipped, latest open wins, merges of closed-only branches refused), reject plan-time branch collisions against existing missions and unowned git refs, and reserve the protocol names tower/all.
* fix(agent-core-v2): validate reserved tower names before spawn side effects and stamp reviews with their mission
- TowerSpawn rejects the reserved protocol names tower/all up front; the roster backstop no longer fires after the worktree, subagent, and detached task already exist.
- Reviews now record the mission they were written for; TowerMerge refuses when the latest clean review was stamped for a different mission, or carries no stamp while the branch is shared with other mission records.
* fix(agent-core-v2): pin tower reviews to the mission assigned at reviewer spawn
The reviewer's roster entry now records the mission resolved when it was spawned, and submitReview stamps that assignment instead of recomputing at submit time, so a review stays pinned to the mission the reviewer was briefed on even if the branch's resolution moved on; roster-less submissions fall back to the current resolution.
* chore: drop the enable hint from the tower changeset
* refactor(agent-core-v2): omit undefined fields in tower frontmatter rendering
renderFrontmatter now accepts and skips undefined values, so callers pass optional properties (review mission, inbox scope/action/consent_ref) directly instead of using conditional spreads, per the repository rule for optional object properties.
* fix(agent-core-v2): leave reviews by unpinned tower reviewers unstamped
A roster reviewer without a recorded mission assignment (entries persisted before reviewMissionId existed) no longer gets a stamp inferred from the submission-time branch resolution, which could name a different mission than the one it was briefed on; unstamped reviews on shared branches are refused by the sibling-mission gate until a pinned re-review, while unshared branches merge as before. Tower-submitted reviews keep the resolution fallback.
* fix(agent-core-v2): probe tower worktree registration per path instead of parsing porcelain output
git worktree list --porcelain emits paths containing newlines literally, so the line-based parser truncated them and teardown misreported those worktrees as already removed; the -z flag suggested for this is unavailable on the git versions the project supports (2.34). teardown now checks each mission worktree directly: git -C <path> rev-parse --git-dir resolving under <repo>/.git/worktrees means registered, which round-trips any filesystem path on any git version.
* fix(agent-core-v2): resolve the common git dir when probing tower worktree registration
In a linked checkout repoRoot/.git is a file and worktree admin directories live under the repository's common git directory, so every mission worktree failed the containment check and teardown left them on disk; the probe now resolves git rev-parse --git-common-dir first.
* docs(agent-core-v2): permit foreground roster resumes when background task tools are inactive
* fix(agent-core-v2): reject blank or whitespace-padded tower agent names
Recipients are trimmed at send time, so a name carrying surrounding whitespace would register an unreachable agent or alias a reserved protocol name; spawn and roster registration now reject such names up front.
* fix(agent-core-v2): drop queued tower inbox wakes on tower exit
The wake notify handle is now retained and dropped by exit(), so a queued wake can no longer inject a stale TowerInbox prompt after the user leaves tower mode.
* fix(agent-core-v2): drop queued tower inbox wakes when availability changes
reconcileTowerProjection now drops a pending wake and resets the wake state whenever the effective projection turns inactive, covering live flag-disable and feature-disassembly paths that never call exit().
* fix(agent-core-v2): truncate the subject preview in tower inbox wakes
The full subject stays in the inbox file, but the synthetic wake message now caps it at 120 characters so a body-sized subject cannot flood the tower's context.
* fix(agent-core-v2): recheck branch ownership at tower spawn time
A branch created between TowerPlan and TowerSpawn is no longer silently checked out as the mission's base: addWorktree now refuses it unless the mission already has an owner (a genuine continuation) or the worktree directory exists (a retry of an earlier spawn attempt).
* fix(agent-core-v2): abort tower spawns on worktree protocol refusals
TowerSpawn used to downgrade every addWorktree failure to a continuing warning, so a branch-ownership refusal still registered the worker and recorded an owner, which then satisfied the ownership check for later spawns; protocol errors now abort the spawn while git-level reuse cases (existing worktree directory) continue as warnings.
* fix(tower): select the review stamped for the mission being merged
Review rounds are per-reviewer, so a higher-round review written for a
closed sibling mission shadowed a fresh clean review stamped for the
mission being merged, and the gate's re-review advice could not succeed
until the new reviewer exceeded the old round count. The merge gate now
prefers reviews stamped for the resolved mission and falls back to
unstamped legacy reviews only when no stamped review exists; a branch
whose reviews all belong to other missions reports 'no review'.
* fix(tower): create unowned mission branches atomically
The spawn-time branch ownership check and the subsequent worktree
creation were a check-then-act sequence: a branch created by another
process in between was silently checked out instead of created from the
recorded base, letting unrelated history into the worker's worktree.
worktreeAddNewBranch now attempts the creation directly so git itself
refuses an existing ref, and the store maps that refusal to a
TowerProtocolError so the spawn aborts instead of degrading to a
warning.
* fix(tower): weigh later legacy verdicts on unshared branches
Review rounds are per-reviewer, so ordering reviews by round said
nothing about recency across reviewers: a mission-stamped clean review
shadowed a later unstamped P1 from a legacy (unpinned) reviewer, and
the gate merged on the stale clean verdict. Reviews are now ordered by
file mtime, and on a branch with a single mission the candidate set
keeps unstamped reviews alongside stamped ones — an unstamped verdict
there is unambiguous. Shared branches keep the stamped-only preference
so the legacy ambiguity guard still applies.
* fix(tower): accept only the registered worktree as proof of a prior spawn
The spawn-time ownership refusal was bypassed by any directory entry at
the planned worktree path: an unowned branch plus a plain mkdir let the
spawn continue past the refusal (the failed git worktree add degraded
to a warning), and the unrelated branch could later reach the merge
gate. The exception now requires the path to be a registered worktree
checked out on the mission branch — exactly what a crashed first spawn
attempt leaves behind — while a plain or foreign directory is refused.
* fix(tower): warn when a tower message has no running task to deliver it
A TowerSend to an idle roster agent only writes a file — nothing wakes
the recipient, so the fleet stalls until someone happens to resume it
(seen in the field: a tower messaged a worker back for another review
round without resuming it, and the mission sat idle for hours until the
human nudged the session). When the tower messages a roster agent that
has no active task in this session, the tool result now notes that the
message sits in its inbox until it is delivered with
Agent(resume=..., run_in_background=true). Broadcasts and sends from
workers are unchanged: workers cannot resume peers — the tower relays
wake-ups.
* fix(tower): record review submission order in the review itself
Review order was derived from file mtime — filesystem metadata that
coarse-tick filesystems quantize and that copies or archive restores
can rewrite, so a newer blocking verdict could sort behind an older
clean one and let the merge gate proceed. submitReview now stamps a
store-wide monotonically increasing seq into the review frontmatter
and reviewsFor orders by it; mtime remains only as the tie-breaker for
legacy reviews that predate the field and for same-seq races between
concurrent submissions.
* chore: shorten the tower changeset and cover the merge gate
* chore: condense the tower changeset to the three fixed areas
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
* fix(mcp): preserve original attachments omitted from model output
* test: register session media storage in agent lifecycle fixture
* fix(mcp): retain media originals and bound attachment details
* fix(mcp): resolve session attachments and honor cancellation
* test: check concrete media references in provider requests
* fix(media): read session attachments from their owning storage
* fix(media): preserve attachment access across runtime and history changes
* fix(mime): recognize structured application text attachments
* feat(agent-core-v2): add event-sourced store core and rewire human agent/session
human/eventStore core (schema-first defineEvent factories, serial
fold-first dispatch with snapshotting, late-join slices, journal
reset), agent events/slices/historySchema, machine rewrite around a
store actor with mirror consumption, session stores with
turnIndex-boundary undo, todo slice, v2 migration to the new event
types, and the loop engine bridge.
* refactor(agent-core-v2): make the agent machine self-consistent and the store actor static
Phantom-turn root cause: local patches plus the async store.changed
mirror were two consistency models for the same data. The machine now
owns its state (every transition is a local assign plus a store.append
dual write) and consumes only store.ready/reset/error.
- store actor becomes static storeActor logic with the engine passed
via invoke input; the session machine registers agentActor and
spawns it by name
- SessionStores.open/fork return the engine directly
- store.reset aborts the old abort scope (cascades to in-flight
turn/tools/background)
- no-op drain events are no longer journaled
- engine bridge seeds the initial turn id into the journal before
folding, and resetHistory is awaited before the retry notify
- tests pass the store via machine input and wait for the log to
catch up before store assertions; drop debug residue
* refactor(agent-core-v2): drop journal snapshots for plain full-replay event sourcing
Snapshots serialized the whole slice state every 500 events and, on
an asynchronous backend, their fire-and-forget append could still be
queued when the next dispatch read journal.nextSeq(), making the
recorded FoldContext.ref.seq one less than the persisted event's
sequence and shifting turnIndex undo boundaries. Removing the
mechanism eliminates the race and the full-state rewrite; refold now
always seeds from initial state and replays every event record
(legacy snapshot entries are ignored).
* feat(agent-core-v2): warn on [models] entries missing the model field
* refactor(agent-core-v2): contribute model config diagnostics from the models section
Move collectMalformedModelEntries next to ModelRecordSchema and derive
the known object-field set from the schema shape instead of hardcoding
it, so future object fields cannot drift out of sync. Sections now
contribute load-time diagnostics through a collectDiagnostics hook,
mirroring the deprecations contribution.
The human/llm provider-catalog state machine becomes the single source of
truth for the runtime model/provider directory. IModelCatalog's data plane
reads through a new llm-adapter catalog-runtime adapter that mirrors config
section diffs into catalog upserts, and CatalogEntry cache invalidation is
driven precisely by the machine's changed events instead of a blunt clear.
The persistence layer is untouched: config.toml stays the full source of
truth and refresh results keep being written to it.
Also remove the model resolution trace/inspect stack end to end:
ResolutionTraceCollector, the llm-adapter model/contract inspection modules,
IModelCatalog.inspect, the capability explainability indirection, and the
kimi-inspect model inspector panel.
- the human agent state machine now assigns turn ids and echoes the drained queue item in turn.started; the facade adopts machine ids instead of reserving its own
- the turn machine produces the step ordinal and surfaces it via a first-class step.started fact; the llm requester layer stays step-agnostic
- turn.prompt wire records carry an explicit turnId; turnKey and transcript foldFacts read it directly with a counting fallback for legacy records
- queued prompts are addressed by promptId: cancel(turnId) is active-turn only, cancelQueued targets queue entries, Turn.id resolves once the turn starts
- drop PromptPayload.disabledTools and its klient/node-sdk pass-through (no client consumer)
* docs(changelog): sync 0.42.0 from apps/kimi-code/CHANGELOG.md
* docs(changelog): use absolute URL for the Remote Control guide link
* docs(changelog): use absolute URLs for the remaining doc links
* fix(agent-core-v2): avoid repeated truncation of file reads
* fix(agent-core-v2): resume reads within long lines
Add column offsets and forward recovery for bounded long-line reads. Reject malformed UTF-16 in Read instead of replacing invalid input.
* fix(agent-core-v2): warn on lossy UTF-16 reads
Return readable text after strict UTF-16 decoding fails, with a warning retained in budgeted read continuations.
* fix(agent-core-v2): avoid redundant tail scans and detect file changes
* feat(agent-core-v2): accept HEIC, HEIF, and BMP images for Kimi models
Key the accepted image MIME set by the provider the agent is bound to
instead of a single global allowlist: Kimi accepts PNG/JPEG/GIF/WebP plus
BMP/HEIC/HEIF, every other provider keeps the baseline set. The provider
image policy lives in the human layer (`llm/media/image-formats`) so both
the agent-side gates and the Anthropic protocol lowering share it.
- thread the provider type through every ingestion gate: ReadMediaFile,
prompt entry, MCP tool results, session media references, kap-server
prompt/skill routes; expose it via IAgentProfileService.getModelProviderType
- ReadMediaFile sends accepted formats it cannot re-encode locally inline
up to the provider's per-image budget (5MB for Kimi, 3.75MB otherwise)
and points at a conversion command above that; full_resolution uses the
same provider budget
- acp-server no longer gates formats itself and leaves the decision to the
engine prompt pipeline, which knows the bound provider
- the Anthropic lowering allowlist follows the bound provider so Kimi over
the Anthropic-compatible protocol no longer rejects HEIC client-side
* fix(agent-core-v2): resolve the image format policy on the production paths
Follow-ups from review of the provider-aware image formats:
- the Anthropic lowering read the policy from `LlmModel.provider`, which
the adapter registry sets to the route id (`anthropic`) rather than the
model's provider type, so Kimi over the Anthropic-compatible protocol
still rejected HEIC client-side; the accepted set now comes from a
`ProtocolTrait.acceptedImageMimes` hook that the Kimi traits implement,
and the test goes through the real trait with the routed provider id
- kap-server prompt ingestion gated media against the currently bound
model even when the same request selected another model; the profile
service now resolves the provider type for a requested alias
- the session media memo served a cached inline image to a provider that
does not accept its format; memo hits are re-judged against the current
provider before being returned
- the media tools registrar keyed ReadMediaFile only by alias and
capabilities, so a reload that changed the alias's provider type or
protocol kept the stale tool; both now take part in the key
* test(kap-server): keep the optional chain the lint autofix dropped
* refactor(interaction): replace per-agent InteractionService with human-native session facade
- add human/interaction: global interaction machine + tag-query facade (enqueue/respond/findAll/findOne/wait)
- add session-scope SessionInteractionService owning one interaction actor per session
- rewire approval/question/userTool/sessionActivity to the session facade and delete features/interaction
* refactor(interaction): rewire kap-server, klient and node-sdk to the session interaction facade
- kap-server routes, ws broadcaster and transcript binding read ISessionInteractionService (tags replace origin)
- klient memory dispatcher delegates to the session service; zod contract mirrors origin -> tags
- node-sdk session wiring subscribes the session service directly
- register interaction wire events in their new home and regenerate the wire manifest
* refactor(agent-core-v2): replace interaction DI services with a global human singleton
resolve rebase conflicts
- scrub all ambient KIMI_CODE_* env vars in the agent-core-v2 and
kap-server test setups so developer shell env cannot leak in
- drop the load-sensitive stepRetry fake-timer cases and the three
FSEvents-based skill catalog watch cases
- give node-sdk, klient and the vscode extension project an explicit
15s testTimeout matching their integration-test nature
- retry temp-dir removal in taskManager teardown to absorb async
terminal-effect writes
* feat(agent-core-v2): allow read-only tools in /btw side questions
The btw child agent previously vetoed every tool call. Allow the
read-only tools Read, Grep, and Glob so side questions about the
codebase can be answered from current file contents; write and
execute tools stay disabled.
* docs: address review — single-sentence changeset, inline-code tool names
* chore: drop the benefit clause from the btw changeset
* fix(agent-core-v2): clarify subagent model disclosure in Agent and AgentSwarm prompts
The Available models section annotated "primary" with its alias only
when the caller's model was already listed, and stayed anonymous exactly
when the annotation was needed. The same model was reachable three ways
with an undisclosed thinking-level difference, the [main model] marker
duplicated the primary handle under a second name, and AgentSwarm
repeated the whole list verbatim.
- primary now always names its binding: primary (= <alias>)
- drop the [main model] marker; keep [default]
- state the pool entries' thinking level in a closing note
- AgentSwarm renders a compact one-line summary instead of the full block
- keep the selection guidance only in the model parameter description
* Delete .changeset/clear-subagent-model-list.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(agent-core-v2): drop the hard-task guidance from the model parameter descriptions
* fix(agent-core-v2): render a disabled-thinking variant of the pool thinking note
* test(agent-core-v2): drop the added prompt-text variant tests, keep updated assertions
* fix(agent-core-v2): state the pool thinking note as one unconditional binding rule
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(telemetry): stop silently dropping key event attributes
- turn_ended now carries error_type (the engine error code) when a turn
fails, matching the v2 event catalog definition that no emitter set
- TelemetryClient reports properties dropped by sanitizeProperties via a
new onUnexpectedError hook, wired to the CLI, web, and v2-print logs
- session_started client attribution keys emit empty strings instead of
null so they survive payload flattening
* fix(telemetry): sanitize at sink accept time and guard the drop handler
Addresses review on #3638:
- Properties are sanitized when the sink accepts the event (at track time
on the direct path, at drain time for the pre-init queue), so drops on
events queued before initialization are reported once
onUnexpectedError is installed
- The onUnexpectedError hook is invoked behind a guard, mirroring the
agent-core-v2 unexpectedError path, so a faulty host handler cannot
abort track()
* fix(telemetry): snapshot queued properties at track time
Addresses review on #3638: the pre-init queue now stores the sanitized
snapshot taken at track time (only primitives survive, so the snapshot
is exact), and the dropped keys ride along as pending diagnostics that
are replayed once the handler is installed. Callers mutating a reused
properties object between track() and initialization can no longer
alter the emitted event or its drop reports
* feat(kimi-code): two-line collapsed tool cards with width-aware headers
Collapsed cards now show the call on line one and a single dim outcome
row on line two: a Bash command's last output line (the live tail while
running) with a line-count chip, a Grep/Glob path sample, or a generic
tool's first output line. Read groups stay header-only and honor the
global expand state; failed calls keep their preview. Headers split into
head / flexible / tail segments so a long command fills the terminal
width before the chip.
* feat(kimi-code): NotifyUser tool and mid-turn update panel
Add an experimental NotifyUser tool (flag notify_user, off by default)
that agent-core-v2 offers only when the host declares the update_panel
UI capability at bootstrap; the TUI declares it through the SDK harness
options. The system prompt's guidance to send updates is injected only
when the tool is active for the agent. The TUI renders the turn's
updates in a panel above the editor (stacked, 8-row window, Ctrl+N pages
back) that the next turn clears, and keeps a one-line card in the
transcript.
* feat: add subagent updates and bidirectional paging
* fix: preserve session tools and prompts when toggling updates
* fix: correct notification delivery and CI regressions
* feat(secondary-model): drop the experimental flag from the subagent model pool
* feat(secondary-model): sync the web bundle without the experimental gate
* fix(agent-core-v2): stop tool actor teardown from aborting sibling tools
* refactor(agent-core-v2): hoist abort signal ownership to spawn sites
* refactor(agent-core-v2): derive cancellation from a Go-like abort scope tree
* fix(agent-core-v2): bind the abort scope's child abort
* feat(agent-core-v2): anchor compaction resumption on the latest user message
* fix(agent-core-v2): count the continuation anchor in tokensAfter
* fix(agent-core-v2): simplify the continuation anchor to a single statement
* fix(agent-core-v2): avoid absolute claims in the compaction continuation texts
* fix(agent-core-v2): flag the continuation anchor in compaction records so old records replay unchanged
* fix(agent-core-v2): replay the continuation anchor unconditionally, drop hasContinuation
The anchor is an injection (policy, not history): like the system prompt
and other reminders, it is re-rendered by current code at replay time, so
pre-anchor compaction records gain it on resume. The divergence is
self-limiting (only records written before this change, absorbed on first
resume) and needs no per-record format flag. Fold-length accounting counts
the anchor unconditionally to stay at parity with the rebuilt context.
* fix(vis): align the context projector with the compaction continuation anchor
Locate the compaction summary by origin instead of assuming it is the last
shape message (the anchor now follows it), and pin the anchor in the
projection expectations.
* refactor(agent-core-v2): drop anthropic_beta and google-vertex protocol names
* refactor(agent-core-v2): resolve human protocol variants at generate time
* docs(agent-core-v2): add the llm module design guide
* docs(agent-core-v2): align the llm guide with the shared syntax errors, failure ends, and anthropic_beta naming
* feat(tui): show each background agent's model in the /tasks list
* fix(agent-core-v2): carry model and thinking effort in tower spawn task metadata
* fix(tower): honor subagent timeout config for tower spawn tasks
TowerSpawnTool registered its detached subagent task with the hardcoded
DEFAULT_SUBAGENT_TIMEOUT_MS, bypassing the [subagent] timeout_ms setting
and KIMI_SUBAGENT_TIMEOUT_MS. Resolve the timeout through
resolveSubagentTimeoutMs(config) like AgentTool does, keeping the 2h
default when nothing is configured.
* test(agent-core-v2): deflake the sessionIndex perf baseline under load
* feat(agent-core-v2): carry user-verbatim mission context through tower briefings
TowerPlan missions accept an optional context field holding the user's
own key sentences verbatim, rendered into the mission file and every
briefing that embeds it. Reviewer briefings now include the full mission
text and the author's review-request, with an intent-first checklist;
worker briefings escalate substantive ambiguity to the tower before
acting and reconcile completion reports task by task.
* chore: merge tower changesets into one
* refactor(tui): narrow tasks-browser host appState with Pick
* docs(agent-core-v2): correct the subagent timeout description in the Agent tool
* docs(agent-core-v2): drop the timeout bullet from the Agent tool description
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
* feat(kimi-code): two-line collapsed tool cards with width-aware headers
Collapsed cards now show the call on line one and a single dim outcome
row on line two: a Bash command's last output line (the live tail while
running) with a line-count chip, a Grep/Glob path sample, or a generic
tool's first output line. Read groups stay header-only and honor the
global expand state; failed calls keep their preview. Headers split into
head / flexible / tail segments so a long command fills the terminal
width before the chip.
* feat(kimi-code): show short tool output whole and point at hidden output
Collapsed cards now show up to three output lines before falling back to
a single outcome row, the Grep chip counts files or matches according to
output_mode, and the tools' pagination and empty-result notices no longer
count as results. While the recent turns hold tool output that ctrl+o
would reveal or hide, the footer shows ctrl+o expand or ctrl+o collapse.
* feat(kimi-code): mark hidden tool output with counts and direction
A collapsed Bash card's chip now counts the hidden lines (`N more
lines`), its last-line outcome row carries a leading ellipsis, a generic
tool's first-line row a trailing one, and the Grep glance keeps its
"+N more" count in the fixed tail so width cuts drop samples, never the
count. Background Bash results identify the task by their first
metadata line instead of trailing internal hints, and header truncation
now treats ANSI escapes as atomic zero-width units and stays bounded by
the terminal width for huge arguments.
* fix(kimi-code): keep tool notices and context rows out of result counts
Glob's timeout, truncation, and warning lines no longer inflate the
file count or pose as glance samples, and unnumbered Grep content with
context flags falls back to an exact file count instead of claiming
matches it cannot distinguish. The footer's ctrl+o hint now also
appears when an outcome row is cut by the terminal width — one to three
very long lines hide the remainder that ctrl+o reveals wrapped.
* fix(kimi-code): align the ctrl+o hint with what expansion reveals
The Edit check now uses the same clustered diff render as the preview
(context rows and inter-hunk separators count toward the cap), and an
ExitPlanMode outcome card no longer reports hidden content — the plan
is fully rendered by the call preview and its result body is
expansion-independent. OUTCOME_MAX_LINES moves to the TUI constant
directory with the other shared rendering limits.
* fix(kimi-code): tighten the collapsed-card hidden-content signals
Outcome rows drop terminal control sequences before they are cut, a
failed Bash card leaves the hidden-line count to its preview trailer,
an unnumbered Grep glance lists each file once, a solo subagent card
never reports hidden content, and the glance sample cap joins the other
collapsed-card limits in the rendering constants.
* fix(kimi-code): count paginated Grep totals and wrapped error previews as hidden
The Grep chip and glance use the tool's count-mode summary and pagination
total instead of the current page, Windows drive letters stay inside
unnumbered content paths, Glob's ripgrep stderr continuation lines no
longer count as files, a background question follows the line-count rule,
and an error preview whose long line wraps past its row cap keeps the
footer's ctrl+o hint on.
* fix(kimi-code): treat spilled tool output as an envelope, not as results
An oversized result reaches the TUI as agent-core's truncation envelope;
cards now render its first line as the outcome row and carry no chip
instead of counting its metadata as files or lines. The Grep chip keeps
the count-mode totals on an empty page, and the Bash chip counts rows
the way the outcome rows do, so whitespace-only rows never claim hidden
lines.
* fix(kimi-code): widen the hidden-content signal to failed previews and ! cards
A paginated content search reports the tool's match total, a capped Edit
or Write preview counts as hidden even when the call failed, and a
user-run ! command card tells the footer about its running tail or
capped result.
* fix(kimi-code): keep result chips honest on incomplete, narrow, and cut-off cards
Grep and Glob counts read as lower bounds (`12+ files`) when the tool
reports a timeout or output cap, a header too narrow for its fixed parts
drops the middle and cuts the head before the chip, and a call whose
arguments were cut off by max_tokens no longer claims hidden content.
* fix(kimi-code): stop the expand hint lying on goal cards, ! cards, and paginated context
A pagination total only stands in for the match count when no context
flag is set, a ! command card that finished while expanded still counts
its rows past the preview cap, parsed goal snapshots and bodiless goal
updates report nothing to expand, header fitting measures graphemes
instead of trusting code-unit length, and the escape pattern and tail
window join the rendering constants.
* fix(kimi-code): read zero context flags as none and mirror the Edit cap exactly
A Grep call with -A/-B/-C set to zero produces no context rows, so its
matches stay countable; the Edit hidden-content check now renders the
preview capped and uncapped and compares them, so a body that fills the
cap exactly no longer counts as cut.
* fix(kimi-code): keep the collapse hint for expanded cards outside the window
Toggling ctrl+o off collapses every expanded card, including one that
slid before the three-turn cutoff since it was expanded, so the footer
now keeps offering collapse while any expanded card hides content. Grep
context detection follows the backend's -C precedence, where a defined
-C makes -A and -B moot.
* fix(kimi-code): render Edit and Write results the same way in both states
Their one-line success acknowledgements repeat what the header, chip and
preview already show, so they render in neither state; any other
successful output shows as an outcome row in both. The footer's expand
hint therefore depends only on the capped preview for these cards.
* fix(kimi-code): keep a Read group's failure count on narrow rows
The finished header is three segments — label, line count, `· N failed`
tail — so a narrow row drops the line count before the failure count,
which is the only sign of partial failure while the per-file body is
collapsed.
* fix(kimi-code): show cut-short empty searches and keep the hint with a status command
A Grep or Glob the tool cut short before any row renders its notice
instead of an exact-looking empty result and carries no chip, a
ReadMediaFile result that is not a media envelope follows the line-count
rule, and the footer's ctrl+o hint moves to line 2 when a
status_line.command owns line 1.
* fix(kimi-code): count unreadable directories as an incomplete Glob and stop hinting on notice-only searches
A Glob that skipped unreadable directories reports its file count as a
lower bound, and a search cut short before any row — which renders only
the tool's notice, the same way in both states — no longer makes the
footer offer ctrl+o.
* fix(kimi-code): let the ctrl+o hint displace an inline tip and count parsed WaitFor results as hidden
An inline tips slot gives way when the fixed hint would not fit beside
it, and a parsed WaitFor result — whose raw fields only render when
expanded — reports hidden content instead of falling to the line-count
rule.
* fix(kimi-code): keep spill pointers and sensitive-only notices out of counts and honor streaming previews
The per-line truncation pointer agent-core appends to an oversized
result stays out of line and file counts, byte sizes and outcome rows; a
search whose only matches were filtered as sensitive files shows the
tool's notice instead of an exact-looking empty result; and while
arguments still stream, only a Bash card (whose partial command appears
once expanded) reports hidden content.
* feat(agent-core-v2): watch user-level skill roots so the catalog stays fresh
UserFileSkillSource previously only re-scanned the user skill roots
(KIMI_CODE_HOME/skills and ~/.agents/skills) on catalog load, so a skill
created while the daemon was running never appeared until a restart.
Mirror the workspace-root source: watch both user-level base directories
with a subtree filter limited to the skill-root candidates, debounce
(200ms) change events, and fire onDidChange so the catalog reloads the
'user' source automatically. Watch handles are registered on the
service's DisposableStore and disposed with the App scope.
Supersedes the skills:reload endpoint approach (#3597).
* fix(agent-core-v2): merge same-base watch candidates and drop test comments
Codex review on #3608:
- When KIMI_CODE_HOME equals the OS home dir, deduplicating watch bases
skipped the .agents/skills candidate entirely. Group candidates by
base instead so one watch covers both roots.
- Remove // phase labels from the catalog test: agent-core-v2 is a
comment-free zone enforced by scripts/check-no-comments.mjs (lint was
red on CI).
The decode-window split measured part arrival at the moment the event
loop resumed the pending await, so any loop-busy work (rendering,
timers, GC) between parts was reported as server time. Measure loop
utilization across the decode window with perf_hooks and split it into
loop-idle wait (server), per-part processing (client), and a new
clientBlockedMs bucket (loop busy with other work), carried through to
the step events, wire record, transcript contract, and the TUI debug
timing line.
Resuming a session dominated by cron-fired turns mounted an unbounded
transcript: limitAgentReplayByTurns only counted human-typed prompts as
turn boundaries, so the replay limit never engaged (10,478 records for
11 'turns' on an affected session). The mounted transcript made every
render frame O(total lines), starving the event loop during streaming
and collapsing client-side drain throughput.
Treat cron_job and cron_missed replay records as turn boundaries,
matching the TUI transcript window's own boundary rule.
* feat(agent-core-v2): remove the context budget reminders
PR #3423 shipped two behaviors together: the context budget reminders
(context_budget and compaction_ahead) and the compaction recovery
pointer. Remove the reminders so the recovery pointer's effect can be
measured on its own in an ablation run.
Removed: the features/contextBudget feature, the budget-only
CompactionStrategy.budget()/CompactionTriggerBudget/
IAgentFullCompactionService.budget() surface, the summarizer-input
reminder filter, and the context_budget_reminder /
compaction_ahead_reminder telemetry plus the ahead_* fields on
compaction_finished. The recovery pointer (Context Recovery footer,
wire journal line ranges, whole-line wire.jsonl reads, empty-history
compaction failure) is unchanged.
* chore: add the changeset for removing the context budget reminders
A malformed config entry (e.g. an unquoted dotted TOML key like
[models.kimi-k2.7-code]) parses into a nested object that lacks the
top-level model field. The v2 config schema marks model optional, so the
entry reaches profile matching with undefined, crashing getModels with
TypeError: Cannot read properties of undefined (reading 'toLowerCase').
matchKnownAnthropicModelProfile, matchUnknownClaudeProfile, and
parseAnthropicModelVersion now return undefined/null for undefined input
instead of throwing.
Fixes#2981
Co-authored-by: zhaopeiwen.dev <zhaopeiwen.dev@bytedance.com>
* feat(agent-core-v2): add env bindings for print-mode and bash task timeout config keys
Wire bashTaskTimeoutS, printBackgroundMode, printWaitCeilingS, and
printMaxTurns into taskEnvBindings as KIMI_CODE_BACKGROUND_* variables
so headless/print runs (e.g. container harnesses) can bound or disable
the print background policy without mounting a config file. bashTaskTimeoutS
accepts 0 (no timeout); invalid values are ignored. Env-set values count
as set for applyPrintModeConfigDefaults and win over the print fill.
Both integer parsers use Number.isSafeInteger: zod's int() rejects
unsafe integers, and since applySectionEnvBindings validates the combined
overlay, one unsafe value would otherwise discard the whole section's
env bindings, including valid siblings.
* Delete .changeset/print-task-env-bindings.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(remote-control): release the lock before reporting off and re-read the server token per forward
* fix(remote-control): pass the token provider through the manager
- remote-control is always available: kimi rc, kimi web --remote-control and /remote-control no longer require KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL or the master flag
- add the [database] config section with base (session-index read model) and search (global search worker) kill switches, both defaulting to true
- rename the kill-switch env vars to KIMI_CODE_PERSISTENCE_MINIDB_READMODEL and KIMI_CODE_SEARCH_WORKER (precedence: env > config > default)
* refactor: remove the legacy agent-core v1 package
Delete packages/agent-core (v1 engine) and migrate node-sdk fully onto
agent-core-v2:
- node-sdk owns its public contract (error protocol, KimiConfig schema
and file IO, process-global logging facade, telemetry client
interface); event and interaction types re-export from
@moonshot-ai/protocol or agent-core-v2
- createKimiHarness is now the single harness factory, backed by the
v2 client; the v1 SDKRpcClient is deleted
- CLI and vscode run agent-core-v2 unconditionally;
KIMI_CODE_LEGACY_FLAG and useAgentCoreV1 are removed
- the wire.jsonl replay fold moves into agent-core-v2's replayBuilder
(foldWireRecords), and the v2 journal now persists tool-call display
so replay restores diff/todo displays
- TUI engineV2 branches collapse to the v2 path
* test(vscode): retry temp-dir teardown in replay-resume integration test
* fix(agent-core-v2): deduplicate repeated tool call ids inside the turn machine
* fix(agent-core-v2): keep the raw tool call id whenever a normalizer rewrites it
* refactor(agent-core-v2): absorb the human layer into src/human and rename vendored kosong to llm-adapter
* feat(agent-core-v2): preserve media attachment names in human message contract
* refactor(agent-core-v2): remove llm-adapter code duplicated in human/llm
* refactor(agent-core-v2): replace the agent loop with a facade over the human turn state machine
* refactor(agent-core-v2): rename the loop adapter layer to loop/machine
* test(acp-server): implement gateway resolve in the scripted provider stub
* refactor(agent-core-v2): restore v2 batch semantics in the loop machine tools adapter
- delegate each step's tool calls to the v2 tool executor as one whole
batch: beginBatch arms the expected call set from the llm.done entry,
per-actor execute calls collect into a pending barrier, and the full
batch is submitted in tool-call order once complete
- drop the serial batchChain/batchStopped/skipped approximation so the
executor's prepare-stage stopBatchAfterThis skip, veto short-circuit,
and ToolScheduler access-based concurrency apply natively again
* fix(build): update flake pnpmDeps hash for the post-absorb lockfile
* fix(agent-core-v2): align loop facade reservations with the machine queue by id
* fix(agent-core-v2): drain persisted wire records before turn end
* fix(agent-core-v2): route executor-level tool errors to the loop error path and backfill aborted tool results on forced abort
* fix(agent-core-v2): keep turn step retrying in the facade step numbering space
`kimi web --host 0.0.0.0` (or `--host ::`) auto-opened the browser on the
wildcard bind address, which is not navigable. Open localhost on the same
port instead, matching the ready banner's Local: line. Also make the ready
banner's port parsing tolerate the `::` wildcard origin, which crashed
`new URL()` at startup.
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
Co-authored-by: qer <wbxl2000@outlook.com>
Preserve optional image and video filenames through media resolution, shared protocol schemas, turn events, and transcript reconstruction. Cover stored media reuse, unsupported image conversion, and steering with regression tests.
* fix(kimi-code): flush wire journals before print-mode exit
A print-mode turn's tail records (step.end / turn.ended / prompt.completed)
are dispatched fire-and-forget and reach the journal only through the wire
service's async persist queue. The print cleanup path never awaited that
queue: with telemetry disabled (KIMI_DISABLE_TELEMETRY=1) cleanup returns
in microseconds, and process.exit on the error path cut off the pending
append-log flush, dropping the failed turn's closing records. Flush every
session agent's dispatcher (which awaits the wire persist queue and the
append-log store) before disposing the app, bounded by the shutdown
timeout and best-effort so a persist failure never masks the turn outcome.
* fix(kimi-code): settle each agent's wire flush independently
Promise.all rejects as soon as one agent's flush fails; the best-effort
caller then proceeds to app.dispose() and process.exit while the remaining
agents' flushes are still in flight, re-exposing their journals to the
truncation this cleanup is meant to prevent. Await every flush with
Promise.allSettled instead (matches the drain helper's convention), and
cover it with a two-agent regression test.
* fix(kimi-code): run print-mode shutdown phases concurrently
The wire flush, v2 telemetry shutdown, and v1 telemetry shutdown each hold
a 3s allowance; run sequentially they can take 9s, past the 8s outer
cleanup bound — the caller's process.exit would then cut off the tail
(app.dispose included). The phases are independent, so await them
concurrently (worst case one allowance) while keeping the v2 shutdown
failure propagation and the best-effort semantics of the other two.
* fix(kimi-code): quiesce active turns before the print-mode wire flush
A termination signal (SIGINT/SIGTERM/SIGHUP) can arrive mid-turn: the
cleanup ran the wire flush immediately, while the still-running turn only
produced its cancellation and closing records from dispose()'s
fire-and-forget teardown — after the flush, and after the signal handler's
process.exit. Cancel every session agent's queued and active turns and
await loop idleness before flushing (best-effort, bounded by the shutdown
timeout); idle loops make this a no-op on the normal exit paths. Torn-down
agent scopes are skipped defensively, mirroring the flush's per-agent
settlement.
* fix(kimi-code): drain prompts and await prompt completion in print quiesce
Two follow-ups from review:
- The loop settles (releaseActiveTurn) before the prompt-settle chain
dispatches prompt.completed, so awaiting loop idleness alone let the
wire flush race the final record. Quiesce now also drains each agent's
prompt queue and awaits the tracked prompt completions (registered at
enqueue time) before the flush runs.
- Restore drainBackgroundTasks to Promise.all: a previous edit
unintentionally switched it to allSettled alongside the per-agent wire
flush, silently discarding persistence failures from
suppressTerminalNotification()/wait(). Only the independent wire
flushes are meant to be best-effort.
* fix(kimi-code): cover every agent's prompt queue in print quiesce
Tracking only the main run's prompt completion left subagent and
background prompts racing the wire flush the same way: the loop reports
idle before that agent's prompt-settle chain dispatches prompt.completed.
Replace the per-handle completion tracking with a uniform wait — after
draining and cancelling, poll every agent's prompt queue snapshot until
none reports an active or pending prompt. settle() clears the active
prompt and dispatches the record in one synchronous block, so an empty
snapshot proves the record is already queued for the flush.
* fix(agent-core-v2,kimi-code): cover the prompt launch window in print quiesce
A termination signal can also arrive while startNext() is mid-launch: the
prompt has left pending and is not active yet, so drain() cannot cancel it
and an active/pending snapshot reads empty. Quiesce would then flush and
exit while the launch still dispatches records afterward.
Expose the service's launching phase on PromptQueueSnapshot and make the
print cleanup's quiesce a repeat-until-idle loop: every pass drains,
cancels, and awaits the loops, then re-checks launching/active/pending, so
a prompt surfacing from the launch window is cancelled on the next pass
instead of escaping the flush.
* fix(kimi-code): freeze loop producers across the print-mode flush
An empty prompt queue is only a point-in-time observation: background
task completions and cron fires enqueue straight into the loop, bypassing
the prompt queue, so a late producer could still start a turn — and new
records — after the journals were flushed. Quiesce now leaves a
quiescence guard held on every loop once queues read empty; the caller
holds the release across the wire flush and app disposal, so late
submissions queue behind the guard and are rejected by disposal instead
of racing process.exit. Loops that refuse the guard (still busy) send the
quiesce into another drain/cancel/settle pass.
* fix(agent-core-v2): flush the wire journal when removing an agent
AgentLifecycleService.remove() quiesced the agent but never flushed its
wire journal: the records reached the append log only through the
fire-and-forget retirement flush, so a process exiting right after a
subagent's removal (e.g. print mode on a termination signal) could
truncate the closing records. Flush the agent's event dispatcher after
the quiesce and before disposal; a persist failure is reported without
blocking the removal.
* fix(agent-core-v2): wait for the prompt queue to go idle before flushing a removed agent
The remove() flush could still snapshot the persistence queue before the
cancelled prompt's final record was appended: loop.settled() resolves in
releaseActiveTurn() before the prompt-settle chain dispatches
prompt.aborted/prompt.completed, and prompt.drain() does not await that
settlement. Wait (bounded) for the prompt queue to report no launching,
active, or pending prompts before flushing; an unreadable snapshot counts
as idle so a wedged service cannot stall the removal.
* chore: widen the print wire-flush changeset to termination signals
* fix(agent-core-v2): re-cancel prompts that finish launching during removal
A prompt mid-launch (startNext awaiting daemon materialization or a
pre-submit hook) is invisible to drain() and has no turn to cancel yet;
the passive idle wait would then let it run to the deadline and flush
before its closing records existed. Repeat drain/cancel/settle on every
pass until the queue reads idle, and once idle hold a quiescence guard
across the flush and disposal so a late producer cannot start new work
in between.
* fix(kimi-code): stop task producers before the print-mode wire flush
Task termination bypasses both the prompt queue and the loop quiescence
guard: AgentTaskService dispatches TaskTerminated straight to the wire,
and disposal would force-stop still-running tasks after the journals were
already flushed. Stop every session agent's tasks up front in the quiesce
phase (mirroring AgentLifecycleService.remove()), so each task's
termination record is dispatched before the flush instead of racing
process.exit. keepAliveOnExit tasks stay exempt, matching remove().
* fix(agent-core-v2): do not let suppression failure short-circuit stopAllOnExit
A detached task's terminal-notification suppression failing (e.g. the
persist write rejects) used to reject the whole stopAllOnExit before
stopAll() ran, so every task stayed active — and callers settling the
rejection (print-mode cleanup, agent removal) proceeded as if the tasks
were stopped, losing their termination records at exit. Settle each
suppression independently, log the failure, and always stop the tasks.
keepAliveOnExit tasks remain exempt by design.
* chore(kimi-code,agent-core-v2): halve print wire-flush tests and trim comments
- rename the mode commands to /yolo and /auto (formerly
/ask-when-needed and /never-ask) and drop their on/off arguments
- running either command now opens the permission mode list with the
corresponding mode preselected; Enter confirms the switch
- decouple the choice picker's initial cursor from the current-value
marker via a new initialValue option
- after a mode switch, show the mode-specific description as the yellow
status line instead of the generic unconfirmed-changes warning
* feat(agent-core-v2): drop the experimental gate from turn-level file history
The file_history flag and its KIMI_CODE_EXPERIMENTAL_FILE_HISTORY env
var are removed; first-touch baselines, turn checkpoints, and the
changes/turnRecorded/contentAt reads now run unconditionally, so the
per-turn file diffs served by kap-server no longer depend on opt-in
configuration.
The changes REST response drops its enabled field — recorded alone
tells an authoritative empty result apart from a turn whose records
were never made, lost to a crash, or displaced by retention.
* fix(agent-core-v2): probe blob existence with a stat, not a directory listing
turnRecorded always answered false for turns whose snapshots lived under
the file-history/ prefix directory: BlobStoreService.has listed the
scope directory (a shallow readdir) and exact-matched the nested key
against top-level entries, so keyed blobs were never found and the
changes endpoint reported recorded=false for exactly the turns that had
edits. Stat the key through storage.size instead, and pin the
turn-recorded assertions against a real node-fs home so the harness
covers the production layout.
* fix(agent-core-v2): probe every keyed blob before calling a turn recorded
Merging the start and end entries let an end entry with key null (a
deleted or oversize-at-end file) shadow the start baseline, and probing
only one surviving key missed partial loss in the other phase: a
session displaced from the thirty-session window could still claim
authority for turns whose snapshots were gone. turnRecorded now stats
every distinct keyed blob across both checkpoints and reports the turn
unrecorded when any is missing.
* feat(agent-core-v2): allow unanalyzable bash commands in auto permission mode
* feat(agent-core-v2): drop the dangerous command guard in auto permission mode
* fix(agent-core-v2): keep background questions open past turn end and inline their answers
Background AskUserQuestion reused the generic task pipeline end to end, which
broke it in two ways: the pending interaction was still bound to the asking
turn, so it was cancelled the moment the agent finished its turn, and the
turn-end cancel response was then misread as an answer. On top of that the
completion notification only carried a pointer to the task output file,
forcing an extra Read round trip for a few bytes of JSON.
- Detach background question interactions from the asking turn so they stay
pending until answered, stopped, or the agent closes.
- Treat cancelled interaction responses as dismissals.
- Inline the answer JSON in the question task notification and word the
notification as answered or dismissed; fall back to the output file only
when the answer exceeds the inline budget.
- Trim the background launch result to task id, status, and one next step.
- Fold transcript notification summaries before inline answer blocks.
* fix(agent-core-v2): fail background questions on tool errors and translate interaction cancellations in the question service
Follow-up hardening from review:
- The interaction kernel's cancellation response now has a named shape,
InteractionCancellation, and SessionQuestionService.request translates it
into a dismissal (null) before handing the result to callers. The
AskUserQuestion tool no longer inspects answer maps for a cancelled key,
so a bare answer map can never be mistaken for a cancellation.
- QuestionBackgroundTask settles as failed with the tool's message as the
stop reason when the question tool reports an error, instead of writing
the error text as completed output. The generic task notification then
carries the reason, and the answered/dismissed wording is not used.
- The question notification only says answered or dismissed when the task
output parses as an answers payload; any other output keeps the generic
completed wording.
* feat(agent-core-v2): remind the model of its context budget and point compaction notes at the wire journal
Add the contextBudget feature: a context_budget reminder that restates used/max/trigger tokens as usage crosses half, three quarters and ninety percent of the compaction trigger, and a compaction_ahead reminder delivered once per window when the trigger is within ten percent of the context window, so the model can persist and verify state while it can still call tools. Both read IAgentFullCompactionService.budget(), which derives from the same CompactionTriggerBudget that drives auto compaction, and both are stripped from the summarizer input.
Behind compaction_recovery_pointer, compaction records the wire journal line range it covered (wireLines on context.apply_compaction, folded into the replayable fullCompaction.wireRanges key) and appends a Context Recovery footer to the model-facing contextSummary with the on-disk wire.jsonl path, every earlier window's line range, and a primer on reading the journal; the UI-facing summary stays the note plus TODO. Read returns wire.jsonl lines under the sessions directory untruncated and spill-exempt so a single record can be read back after Grep locates it, and the compaction instruction tells the summarizer a recovery pointer follows the note.
Both flags default on; KIMI_CODE_EXPERIMENTAL_CONTEXT_BUDGET_REMINDERS=0 and KIMI_CODE_EXPERIMENTAL_COMPACTION_RECOVERY_POINTER=0 disable them. Telemetry gains context_budget_reminder, compaction_ahead_reminder, and ahead_* fields on compaction_finished.
* feat(agent-core-v2): ship context budget reminders and the recovery pointer without flags
- Remove the two experimental flags; both behaviors now ship unconditionally and the compaction instruction carries the recovery note in its template.
- Lower-bound recovery windows at the latest context.clear journal record so a window never points into history the user discarded.
- Add the appended recovery footer's estimated tokens to summaryOutputTokens so tokens_after and the post-compaction token floor stay honest.
* fix(agent-core-v2): cap event log reads and refuse empty-history compaction
- Cap a single wire.jsonl record read at 150k chars, below the window-minus-trigger margin, so one read can never push the context past the model window; the note points at sed | jq for longer records.
- Keep at least the last record when tail-reading the event log instead of returning silently empty output with a contradictory note.
- Fail the compaction when an overflow shrink would drop every message, instead of compacting an empty history and replacing the context with a groundless note.
* fix(agent-core-v2): drop the redundant ninety bucket and soften ahead-reminder wording
- Remove the 90% context-budget bucket: the compaction-ahead threshold is always at or below it, so it only echoed the stronger last-chance reminder moments later.
- Stop suggesting a commit as a way to persist state before compaction; files and the todo list cover it without prompting unwanted commits.
- Make the event-log note's primer reference conditional on a compaction having run.
* chore: merge the compaction changesets into one
* refactor(agent-core-v2): remove the staleGuard feature
Drop the read-before-edit runtime guard: Edit/Write executions are no
longer vetoed when the target file was never read or its mtime changed
since the last read, and successful Read/Edit/Write no longer refresh a
recorded mtime. Removes the staleGuard replayable state key and the
staleGuard.recorded / staleGuard.cleared durable wire events; old wires
keep replaying through the unknown-type skip path. apps/vis keeps
projecting and rendering those historical records via locally declared
legacy record types.
* fix(agent-core-v2): skip retired wire record types silently during restore
Restore reports every journal record whose type has no registered event
class through onUnexpectedError. Sessions written before the staleGuard
removal can hold a staleGuard.recorded entry per successful
Read/Edit/Write, so loading one floods the log with WireError stacks.
Keep a retired-type list of record types that were once durable
vocabulary; restore skips them without reporting, while genuinely
unknown records stay on the error path.
* fix(telemetry): deduplicate session_started and model switch events
* fix(telemetry): keep engine session_started for direct v2 SDK clients and avoid model_switch race
* fix(telemetry): preserve model_switch when activation rebinds the same alias
* fix(telemetry): route TUI reload through harness
* test: update /reload message-flow test for harness reload route
* docs(zh): restyle configuration and customization sections
Editorial pass across 11 pages: clear explanatory dashes, replace arrow
cross-references with inline links, compress oversized table cells while
keeping operational facts (value ranges, override precedence, activation
conditions), split >5-sentence paragraphs by theme, fold interface
contracts and low-frequency internals into details blocks, add map
sentences to multi-paragraph sections, add subcommand overview table to
kimi-command reference. Add /provider manager screenshot to media.
* docs(zh): restore dangerous_command_guard, fix trust prompt default and secondary-model default
- config-files: restore the dangerous_command_guard paragraph dropped
during the style pass (regression, content from upstream #3290)
- mcp: the trust prompt defaults to Trust this folder per
trust-prompt.test.ts; docs had the direction reversed (pre-existing)
- config-files: subagent model pool defaults on since #3334;
KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=0 disables (pre-existing staleness)
* docs(zh,en): sync en mirrors and fix anchor slugs
Add restyled en mirrors for all 11 configuration/customization pages,
mirroring the zh structure (section parity, map sentences, compressed
cells, details folds) while keeping en phrasing.
Fix anchor slugs in both locales (underscore kept, dots dropped per
@mdit-vue slugify): loop_control, openai_responses, kimi_model_,
systemmd variants; retarget renamed permission-mode section
(yolo/auto -> The three permission modes / 三种权限模式).
* fix(tree-sitter-bash): recognize heredocs in character-level balanced scanning
scanBalancedStatements treated heredoc bodies as ordinary characters, so a stray quote, paren, or backtick inside a heredoc body (for example an apostrophe in a PR body passed through a command substitution) broke the scan and produced ERROR nodes; the resulting hasError made dangerous-command-ask judge the whole command unanalyzable and prompt for approval even in yolo mode.
Parse << and <<- delimiters (excluding <<< herestrings) with the same unquoting rules as the token-level heredoc reader, queue pending bodies, and skip them line-wise at newlines. Skip arithmetic $(( ... )) regions via scanBalanced so a left-shift << is never mistaken for a heredoc operator.
* fix(agent-core-v2): raise the bash parse wall-clock budget to 500ms
A 20ms wall-clock budget could abort an otherwise fine parse under CPU contention, GC pauses, or cold-start JIT, flipping the permission verdict to unanalyzable (spurious approval prompts) or silently dropping AGENTS.md re-reminders. Normal commands parse in well under 1ms; maxNodes stays the deterministic cap, and 500ms remains a backstop against pathological parser loops.
* fix(tree-sitter-bash): skip comments and legacy arithmetic during heredoc-aware scanning
The heredoc-aware scan queued a heredoc for any << it encountered, including inside comments (echo $(printf x # <<EOF\n)) and legacy arithmetic expansions (echo $(echo $[x << 2]\n)); both are valid bash that parsed cleanly before, and the regression flipped them to hasError and an unanalyzable permission verdict.
Skip # comments to end of line when the preceding character starts a new word, and skip ${ ... } / $[ ... ] expansions as balanced units (mirroring skipDollar), so << is only recognized where a redirection operator can actually appear.
* fix(tree-sitter-bash): skip word-glued subscripts during heredoc-aware scanning
Indexed assignments such as echo $(a[x<<2]=3\n) put arithmetic inside a word-glued [ ... ] subscript; the heredoc-aware scan read the << shift operator there as a heredoc start, regressing valid bash that parsed cleanly before to hasError and an unanalyzable permission verdict.
Skip a [ ... ] region as a balanced unit when the bracket immediately follows a word character (subscripts and glued glob classes); a bracket at word start keeps the existing character scan, so real heredocs after words (cat foo[ab]<<EOF) and bare [ command arguments are unaffected. The subscript regression is covered by a unit case only: the reference parser splits subscript arithmetic into binary_expression while this parser keeps it an opaque word, a structural difference that predates this change and has no differential fixture yet.
* fix(tree-sitter-bash): skip conditional regions and look through continuations in heredoc-aware scanning
Two more character-scan contexts queued bogus heredocs after the heredoc-aware scan: a [[ ... ]] conditional region (echo $( [[ x == @(<<EOF) ]]\n) and regex right-hand sides such as [[ x =~ <<a ]]), and a # preceded by a backslash-newline continuation (echo $(printf foo\<newline>#bar)), where removing the continuation keeps the hash inside the preceding word instead of starting a comment. Both are valid bash that parsed cleanly before.
Skip word-start [[ ... ]] as a balanced region (a << inside a conditional is never a heredoc operator; a bracket in argument position keeps the character scan), and walk back over \+newline pairs before classifying a # as a comment. The extglob conditional case joins the differential fixtures; the continuation case is unit-only because the reference parser errors on it.
* fix(tree-sitter-bash): scan substitutions as part of heredoc delimiters
A heredoc delimiter containing a substitution (echo $(cat <<$(foo)\nbody\n$(foo)\n)) was truncated at the first paren, so the queued delimiter never matched the body closing line and the scan swallowed the rest of the range, regressing valid bash to hasError and an unanalyzable permission verdict.
scanHeredocDelimiter now scans $( ), ${ }, $[ ], and backtick regions wholesale as part of the delimiter word (recursing with the heredoc-aware statement scanner for $( )), mirroring how bash treats the whole word as the delimiter. The case stays unit-only because unquoted delimiters hit the already-registered heredoc-content-chunks structural difference with the reference parser.
* fix(kimi-code): honor KIMI_DISABLE_TELEMETRY and restore crash telemetry in v2 print mode
* fix(kimi-code): attribute v2 print crash telemetry to the resolved session model
* fix(agent-core-v2): track session index mirror give-up event once per failure episode
The give-up telemetry fired on every failed flush once the consecutive
failure count crossed the threshold, so a session with a persistently
failing mirror reported hundreds of thousands of events per day. Emit
it only when crossing the threshold; the counter resets on the next
successful flush, so a later episode reports again.
* fix(agent-core-v2): latch the mirror give-up event per failure episode
The equality guard missed episodes where unpublished-manifest flushes
(manifest === undefined also increments the counter) pushed the count
past the threshold before the first throwing flush. Latch the report
per episode instead; the latch resets on the next successful flush.
* fix(agent-core-v2): rebuild persisted subagents when resuming after a restart
Agent(resume=<id>) only consulted the in-memory agent roster, so once a
session was reopened in a new process every persisted subagent id failed
with "does not exist" even though its wire log and metadata were intact.
The resume path now reads the session metadata once, verifies the target
is a subagent owned by the caller, and re-creates its agent scope from
the persisted records when it is not live before running the turn.
* fix(agent-core-v2): sync rebuilt subagents to the caller mode and match rules by their persisted profile
A subagent rebuilt for resume replayed the permission mode it was
spawned with, so a child persisted in yolo kept running in yolo after
the parent had switched back to manual; the rebuild now copies the
caller's current mode the way a fresh spawn does.
Permission rules with a profile subject such as Agent(coder) were
matched against the fallback label while the target was offline,
because only live agents exposed their profile during execution
resolution. Every spawn site now records the profile name in the
subagent labels and the resume path reads it back for display and rule
matching when the agent is not live.
Adds a kap-server e2e case that spawns a subagent, restarts the server
on the same home directory, resumes it through the Agent tool, and
checks that its prior context reaches the next model request.
* fix(agent-core-v2): keep pinned permission modes when rebuilding a subagent for resume
Tower workers are pinned to auto mode at spawn and skipped by the
permission-mode broadcast, but the rebuild path synced every restored
subagent to the caller's mode, so a dead worker recovered through
Agent(resume=...) after a restart lost its pinned mode. The exception is
now a single predicate next to the worker profile, shared by the
broadcast and the rebuild.
* fix(agent-core-v2): record subagent profiles in the lifecycle instead of at each spawn site
The persisted profile name that offline resumes use for display and
permission-rule matching was written by three spawn sites, and the
session-init child was still missing it. The lifecycle now records it
centrally: create() takes the profile from the binding it is given and
fork() takes the override or the source agent's profile, so every
subagent spawn is covered and the spawn sites no longer pass it.
* chore: merge the subagent-resume changesets into one
* fix(agent-core-v2): write the bound model into the ambient telemetry context
* fix(agent-core-v2): restore the ambient telemetry model after profile replay
The process-level uncaughtException handler installed by startServer
called process.exit(1), so a single stray exception killed every
session the process served — and, embedded in the desktop main
process, killed the whole app past the shell's own crash guard. Log
the error at error level and keep serving instead.
createServerLogger gains an optional stream so hosts can route server
logs into their own log channel.
* fix(agent-core-v2): rework tower mode enter failures and config-flag assembly
- IAgentTowerService.enter() returns a typed TowerEnterResult: the 4
former silent failure points now carry their reason (not-main-agent /
experiment-off / feature-not-assembled / owned-by-live-session with
the owner session id + title); kap-server and node-sdk map each reason
to a distinct SESSION_TOWER_MODE_INVALID message via
towerEnterFailureMessage() instead of one misleading catch-all.
- Fix config-sourced [experimental] tower flag never assembling the
tower feature: ConfigService seeds its state synchronously at
construction with a best-effort readFileSync of the config document it
owns, so the flag is already visible when TowerFeature's constructor
runs during App-scope creation. No event machinery: a runtime config
flip updates flag reads but does not re-run feature constructors — a
restart is required, pinned by test. docs/flag.md + docs/features.md
document the contract.
- Consolidated changeset for @moonshot-ai/kimi-code.
* fix(tower): release handler when teardown
---------
Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
FeatureAssemblyService ran during the eager bootstrap batch, while
ConfigService was still loading config.toml asynchronously, so
flags.enabled() always read false for config-sourced flags and gated
features (e.g. tower via [experimental] tower = true) never assembled.
registerFeature now accepts a { flag } option; gated registrations are
assembled in a deferred pass once IConfigService.ready resolves, and
IFeatureAssemblyService.ready exposes that pass. WorkspaceInstanceManager
.materialize also awaits config.ready so session creation cannot race
the deferred assembly.
* refactor(agent-core-v2): rebuild telemetry context as a scope-bound layered registry
* fix(agent-core-v2): prefer ambient model in telemetry envelope and repair print-runner stub
* fix(agent-core-v2): delete only the fragment registered by this telemetry binding
* fix(agent-core-v2): bind telemetry context writes to the binding's own fragment
* fix(agent-core-v2): keep stale telemetry emissions on the binding-owned fragment
* refactor(agent-core-v2): mirror turn-request trace_id into ambient telemetry context
* refactor(agent-core-v2): telemetry key-resolution cleanups
Drop the dead defineAgentTelemetryEvent branch in declaredKeysFor: agent_id is already copied unconditionally as identity plumbing before the declared-key loop, and the loop itself skips agent_id, so the branch had no runtime effect. Name the wire session id property key (WIRE_SESSION_ID_PROPERTY) so the session_id-to-sessionId mapping is explicit.
* refactor(agent-core-v2): flow ambient telemetry context to every event unconditionally
Remove the registry-key filtering and the FILTERED_CONTEXT_KEYS hard
filter from composeTelemetryProperties: the merged ambient now reaches
every event's properties, with session_id still remapped to the
camelCase sessionId key and explicit defined values winning on
collision. Explicit undefined values no longer clobber ambient fields.
With filtering gone, correctness for time-varying fields rests on
lifecycle discipline, so the loop now clears the ambient trace_id when
a step starts (mirroring the activeRequestTrace reset) instead of
relying on an explicit undefined payload value to mask the stale
mirror.
* refactor(agent-core-v2): move thinking_effort into the turn telemetry context
* refactor(agent-core-v2): replace the telemetry fragment registry with parent references
* feat(kimi-code): show file-change warning when switching to ask-when-needed or never-ask mode
* Update permission-mode-file-warning.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(kimi-code): show file-change warning on goal and swarm permission switches
* fix(kimi-code): indent every line of multi-line notice details
* feat(kimi-code): render the permission-mode file-change warning in warning color
* refactor(kimi-code): render the file-change warning via showStatus
* fix(kimi-code): defer the goal permission-switch notice until the goal starts
* refactor(kimi-code): move the file-change warning copy into the TUI constant directory
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* feat(agent-core-v2): add a handoff step after forced stops and report subagent stop reasons
- When the tool-call repeat breaker stops a turn, the loop now runs one
text-only handoff step; tool calls issued in that step are refused and
the turn ends with the same stop reason.
- Tool results can carry stopTurnReason; LoopRunResult and turn.ended
expose the forced stop reason.
- Subagent results report stop_reason, a resume hint, and a next-step
line; a turn that ends without a final message fails with
agent.no_final_message, and the step-cap failure is rephrased for the
parent model.
- Remove the subagent summary length policy and its continuation prompt.
* chore(agent-core-v2): regenerate the state manifest after merging main
* fix(agent-core-v2): run the handoff step at the step cap and surface swarm stop reasons
- The per-turn step cap no longer skips a queued handoff step, so a
forced stop that lands on the last permitted step still produces the
text handoff instead of a max_steps failure.
- AgentSwarm results carry the worker's stop reason and render it as a
stop_reason attribute; a stuck worker now also triggers the resume hint.
* fix(agent-core-v2): keep the repeat-breaker classification and persist turn.ended stop reasons
- A handoff that ends without text keeps stop_reason repeat_breaker on
the parent-facing failure instead of collapsing to no_final_message.
- Only a user cancellation maps to stop_reason cancelled; other kills
report stopped and keep the resume hint.
- turn.ended persists stopReason in its durable record and the wire
manifest lists it.
2026-09-02 17:32:25 +08:00
2811 changed files with 167615 additions and 266596 deletions
description: Use when developing in packages/agent-core-v2 (the DI × Scope agent engine) — adding or modifying a domain Service, choosing a LifecycleScope, wiring DI dependencies, splitting a domain across scopes, owning or migrating a config section, gating behavior behind an experimental flag, raising coded errors, working on the permission system, writing DI/Scope tests, porting business logic from agent-core (v1) to v2, triaging a main-branch commit against v2, or exposing a v2 domain over server-v2 while keeping the /api/v1 wire contract compatible with released clients. Self-contained guide organized by development stage (orient → design → implement → test → verify) plus align workflows for v1→v2 migration, main-branch commit triage, and server-v2 wire exposure; each file carries the rules, examples, and red lines for its step.
---
# agent-core-dev
> Develop `packages/agent-core-v2` by lifecycle stage. This skill is **self-contained**: every rule, recipe, and red line lives in the stage files below — it does not delegate to `packages/agent-core-v2/docs/`.
`agent-core-v2` is the new agent engine built on the **DI × Scope** architecture (a port of `packages/agent-core`). Everything resolves through the container: a service declares an **identity**, its **dependencies**, and a **lifetime**; the container decides construction, singleton-per-scope, ordering, and disposal. The stage files restate the rules in imperative form so you can work without reading the source docs.
## Lifecycle at a glance
```text
Orient → Design → Implement → Test → Verify
│ │ │ │ │
│ │ │ │ └─ lint:imports · typecheck · test · dep graph · red lines
Stages are ordered but not strictly linear: a test failure (stage 4) that reveals a wrong scope sends you back to design (stage 2); a `CyclicDependencyError` sends you to `design.md` §dependency-direction and `implement.md` §cycles.
## Workflows
End-to-end procedures that span the stages. Reach for these before reading the stage files individually.
- [Align (port `agent-core` → `agent-core-v2`)](align.md): split a v1 class into semantic units, fix each unit's domain / scope / Service / dependencies, then migrate the logic and tests. Use when the task is "move feature X from v1 to v2" or "port `IXxxService` to v2".
- [Commit align (triage a `main` commit against v2)](commit-align.md): given one `main` commit hash + a short note, find the v1 logic it changed, check whether v2 already has the corresponding implementation, bucket it (aligned / partial / missing / not-applicable), and recommend a minimal fix. Use in the `kimi-code-v2`-catching-up-to-`main` phase, for one commit at a time; escalate to [align.md](align.md) if the gap is a whole domain.
- [Server align (expose `agent-core-v2` over `server-v2`)](server-align.md): wire a v2 domain into `packages/kap-server` over `/api/v2` (native) and `/api/v1` (v1-compatible mirror), keep the wire schema byte-compatible with the established v1 contract by sharing the `@moonshot-ai/protocol` schema, and isolate v1-only behavior in a `<domain>Legacy` edge adapter instead of distorting the native v2 Service. Use when the task is "expose the new v2 Service on the server", "add a route to the `/api/v1` surface", or "keep server-v2 wire-compatible with released v1 clients".
## Stages
- [Stage 1 — Orient](orient.md): the DI black box (identity / dependencies / lifetime), the four `LifecycleScope` tiers and visibility, and the no-comment convention. Read before touching business code.
- [Stage 2 — Design a service](design.md): pick a scope, split a domain across scopes, choose a calling style (direct call vs event vs hook), and direct dependencies. Decide *where things live and who knows whom* before coding.
- Topic: [Domain boundaries vs Scope](domain-boundaries.md) — keep `session` / `agent` / `turn` from becoming god objects; data-ownership test and their split conclusions.
- Topic: [Persistence layering](persistence.md) — the three-layer `Store → Storage → backend` model, naming Stores by access pattern, and which layer business code should depend on.
- Topic: [Edge exposure — `resource:action` + WS events](edge-exposure.md) — which Services are exposed over `/api/v2` (per-scope action map) and which events stream over WS; what to wrap in a facade.
- [Stage 3 — Implement](implement.md): the standard Service recipe and the DI building blocks — interface + identity, constructor injection, scoped registration, `Disposable`, eager vs delayed, `invokeFunction`, `createInstance`, child scopes, and the cycle-refactor playbook.
- Topic: [Telemetry](telemetry.md) — emitting events via `ITelemetryService`, context propagation, and appender destinations (`ConsoleAppender` / `CloudAppender`).
- [Stage 4 — Test](test.md): resolve the system under test by interface, pick `TestInstantiationService` vs `createScopedTestHost`, shared stubs, service groups, teardown.
- [Stage 5 — Verify & submit](verify.md): `lint:imports`, `typecheck`, `test`, and the pre-submit checklist.
## How to use this skill
Jump to the stage you are in and read that one file; each is self-contained and ends with its own red lines. Skim the global red lines below before submitting — they catch most mistakes across every stage. The repo's source of truth remains the code in `packages/agent-core-v2/src/`; this skill codifies the same rules so you do not have to re-derive them.
## Global red lines
Invariants that hold across every stage. Each is expanded in the stage file noted.
1. No `new` on a class whose constructor carries `@IService` deps — inject with `@IX` or `accessor.get(IX)`. (implement.md)
2. `@IX` decorates constructor parameters only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services). (service-authoring.md)
3. Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique. (implement.md)
4. Parent scope never depends on child scope — short-lived may inject long-lived, never the reverse. (orient.md)
5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); activation timing does not break dependency cycles. (design.md, implement.md)
6. `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. (implement.md)
7. Scope follows state identity — no `Map<sessionId, …>` at `App` to fake per-session state. (design.md)
8. Foundational layers never know upstream ones; business code never depends on the edge layer (`gateway`/`rpc`). (design.md)
9. Throw coded errors; register codes centrally; branch on `code` across the wire, never `instanceof`. (errors.md)
10. Gate unreleased behavior behind a flag contributed via `registerFlagDefinition` and resolved through `IFlagService.enabled(id)`; no ad-hoc env toggles. (flags.md)
11. Tests resolve the SUT by interface; shared stubs live under `test/`, never `src/`. (test.md)
12. Config is the preference registry: only preferences that are persistable, schema'd, and user/operator-facing go in `IConfigService`. Domain-specific config (including env-only operational toggles) goes through `registerConfigSection` + `envOverlay`. Facts → `IBootstrapService`, and host invocation arguments (CLI flags, host identity headers, prompt identity) → `BootstrapInput.args` / `IBootstrapService.args` — never new per-domain runtime-options services; domain runtime state (cron/flags/model) never goes onto `IBootstrapService`; session state → Session scope; constants → code. Business domains never call `IBootstrapService.getEnv()` directly. (config.md)
Port business logic from `packages/agent-core` (v1) into `packages/agent-core-v2` (v2) by **splitting semantics, then fixing the domain, scope, Service, and dependency relationships**, and finally migrating the logic and tests.
Use this when the task is "move feature X from v1 to v2", "port `IXxxService` to v2", or "align a v1 domain with the v2 architecture". It complements the stage files: orient / design / implement / test explain the *target* architecture; this file explains how to get there *from v1*.
## The one-paragraph mental model
v1 is a **VSCode-style singleton container**: services self-register with `registerSingleton`, resolve as singleton-per-container, and have no explicit lifetime tier — so a single `ISessionService` / `IToolService` tends to accumulate global, per-session, and per-agent state in one class. v2 is a **DI × Scope tree**: every service binds to one of `App` / `Session` / `Agent`, and a domain with state at several lifetimes is split into several Services. Porting is therefore **not** a file copy — it is "find each lifetime of state hiding in the v1 class, give each its own v2 Service at the right scope, then re-wire the dependencies".
→ Direct dependencies → Port logic → Port tests → Verify
```
Each step below states the goal and the concrete action, then points to the stage file that goes deeper. Do them in order; a later step often sends you back to an earlier one (a scope that does not fit means the semantic split was wrong).
### 1. Read v1
**Goal:** build an accurate inventory of what the v1 code actually owns. Read the v1 *source*, not v1 docs.
Actions:
- Locate the v1 entry: contract (`<domain>/<domain>.ts`) + impl (`<domain>/<domain>Service.ts`), plus any helpers under the same folder.
- Inventory three things from the impl:
- **State** — every field / `Map` / cache the class holds. For each, note its *identity* (global? keyed by `sessionId`? by `agentId`?).
- **Behavior** — every public method; group them by which state they touch.
- **Dependencies** — every `@IFoo` constructor injection and every cross-domain relative import (`from '../<other>/...'`).
- Note the v1 registration line (`registerSingleton(...)`) and any `services.set(IX, ...)` overrides at bootstrap (these reveal runtime static args or prebuilt instances the port must preserve).
Do not start splitting yet — an accurate inventory prevents the common mistake of porting the class shape instead of the semantics.
### 2. Semantic split
**Goal:** break one v1 class into independent semantic units, each owning state at exactly one lifetime. This is the heart of the port.
Method — for each piece of state from the inventory, ask:
1. **What is it keyed by?** nothing → a global unit; `sessionId` → a per-session unit; `agentId` → a per-agent unit.
2. **When should it die?** with the process / the session / the agent. State that must outlive its neighbors is a different unit.
3. **Which methods touch only this state?** they travel with the unit.
Worked example — v1 `ISessionService` (one class, ~600 lines) holds:
- a global index of all sessions → **global** unit → v2 `sessionStore` (`ISessionStore`, App);
- this session's metadata → **per-session** unit → v2 `sessionMetaStore` (`ISessionMetaStore`, Session);
- this session's activity / status → **per-session** unit → v2 `sessionActivity`;
- this session's context projection → **per-session** unit → v2 `sessionContext`;
- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **per-workspace** unit → v2 `sessionLifecycle` (Workspace, one per live workspace handler).
A v1 class that maps cleanly to one v1 decorator often becomes **three to five** v2 Services. That is expected and correct — do not try to keep the v1 class shape.
Red lines:
- If two pieces of state have different identities, they belong in different units — do not keep them together "because v1 did".
- Do not split by method count or file aesthetics; split by state identity (design.md §3).
- If a unit has no mutable state (pure behavior), defer its scope decision to step 4 (it is pulled down by its shortest-lived dependency).
### 3. Map to v2 domain
**Goal:** assign each semantic unit to a v2 domain — an existing one if it fits, a new one only if none does.
Actions:
- Search v2 `src/` for an existing domain that owns the same responsibility. Prefer joining an existing domain over creating a new one.
- If creating a domain, name it after the responsibility (camelCase folder, e.g. `sessionActivity`), not after the v1 file.
- Keep a domain's public surface to one contract file (`<domain>.ts`) plus its impl(s).
Reference mapping (a **starting point**, not gospel — verify against the current v2 `src/`, which is the source of truth):
When the table says "verify", or when v1 and v2 have diverged, **read the v2 `src/` tree and decide from the code** — do not invent a mapping.
### 4. Assign scope
For each semantic unit, fix its `LifecycleScope` from the identity you found in step 2. Follow design.md §2 verbatim:
- global → `App`; per `sessionId` → `Session`; per `agentId` → `Agent`.
- Stateless unit → default to `App`, pulled down only by a shorter-lived dependency.
- Self-check: "when this scope is disposed, should this state disappear with it?"
This is the decision v1 never had to make — get it right before writing any v2 code, because the scope is fixed at registration and changing it later ripples through every consumer.
### 5. Shape Services
Decide the Service shape per unit, following design.md §3:
- A unit that owns **one instance's** state → a single per-instance Service (`ISessionXxx` / `IAgentXxx`).
- A unit that owns a **global view plus per-instance** state → split into an `App` registry/factory (`XxxStore` / `XxxRegistry` / `XxxCatalog`) **and** a per-instance Service. The `App` half creates or locates the per-instance half.
- Do not pre-split a unit that has state at only one lifetime.
Most consumers inject the per-instance Service; inject the `App` factory only for genuine cross-instance management.
### 6. Direct dependencies
Re-wire the dependencies you inventoried in step 1, now across the new v2 Services. Follow design.md §4–§5:
- **Calling style** — need a result / I orchestrate → direct call (`@IX` injection); stating a fact → event; ordered participation that may veto → hook.
- **Scope direction** — a Service may inject only its own scope or an ancestor. If an `App` Service needs something from a `Session` Service, the dependency is backwards: re-scope or invert into an event.
- **Domain direction** — foundational layers must not know upstream ones. A cycle means a v1 relative import is now pointing the wrong way; extract a third Service or invert the notification into an event.
- **Durable facts** — state changes that must be recorded / replayed / projected across agents go on the wire (`wireRecord`), not a direct call alone.
Run `lint:imports` (verify.md) as soon as the dependencies compile — it catches v1 imports and kosong boundary violations early.
### 7. Port the business logic
Move the behavior into the shaped v2 Services, applying the mechanical conversions below. Follow implement.md for the recipe.
**Registration:**
```ts
// v1
import { InstantiationType, registerSingleton } from '../../di';
import { createDecorator, Disposable, IInstantiationService } from '../../di';
import { KimiError, ErrorCodes } from '../../errors';
// v2
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { Disposable } from '#/_base/di/lifecycle';
import { IInstantiationService } from '#/_base/di/instantiation';
import { KimiError, type ErrorCode } from '#/_base/errors';
```
**Constructor injection** — unchanged in shape (`@IX` on constructor params, service params after static params). Verify each dependency is resolvable from the new scope (step 6).
**Errors** — move any shared error into a co-located `XxxError extends KimiError` with a registered `code` (errors.md). Do not keep throwing v1's central error codes from a v2 domain.
**Flags** — replace any `FlagResolver` / env check with `IFlagService.enabled(id)`; contribute new flags from the owning domain's `flag.ts` via `registerFlagDefinition` (flags.md).
**Events** — v1's `Emitter` / `Event` from `base/common/event` maps to v2's `event` / `eventBus` domains. Read existing v2 usage in neighboring domains and match it; do not import v1's `Emitter`.
**Runtime static args / prebuilt instances** — if v1 bootstrap did `services.set(IX, new SyncDescriptor(C, [bag]))` or set a prebuilt instance, preserve that behavior at the v2 composition root (the scope that owns the Service). Do not silently drop it.
Red lines:
- Do not copy a v1 file and "fix imports". Re-split first (steps 2–6); a straight copy carries v1's implicit-singleton assumptions into v2 and creates the `Map<sessionId, …>`-at-`App` anti-pattern.
- Do not leave v1 relative imports (`from '../x/...'`) in v2 — use the `#/...` alias.
- Do not preserve a v1 behavior just because it exists; if the split reveals it was a workaround for the missing scope tree, drop it.
### 8. Port the tests
Convert v1 tests to the v2 harness, following test.md:
```ts
// v1
import { TestInstantiationService } from '@moonshot-ai/agent-core/di/test';
- Resolve the SUT by interface (`ix.get(IX)`), never `new` a `@IService`-carrying impl, and prefer `ix.get(IX)` over `ix.createInstance(Impl)`.
- Move shared stubs into `test/<domain>/stubs.ts`; import by relative path, never `#/...`.
- If the port introduced scope-layer behavior, add a `createScopedTestHost` test that asserts resolution from the correct scope (with `_clearScopedRegistryForTests()` + explicit re-registration in `beforeEach`).
- Keep v1's behavioral assertions where they still describe observable behavior; delete assertions that only checked v1's internal class shape.
## Migration checklist
Before submitting a port:
- [ ] Every piece of v1 state landed in a v2 Service whose scope matches its identity (no `Map<sessionId, …>` at `App`).
- [ ] Each v1 dependency now points in the right scope direction; `lint:imports` passes.
- [ ] Registrations use `registerScopedService` with an explicit scope and domain name; no `registerSingleton` remains.
- [ ] Imports use the `#/...` alias; no v1 relative (`../../di`, `../../errors`) imports remain.
- [ ] Errors are co-located coded errors; flags go through `IFlagService`.
- [ ] Tests resolve the SUT by interface; scope behavior is asserted via `createScopedTestHost`; teardown goes through one `DisposableStore`.
- [ ] v1 bootstrap overrides (`services.set(...)`) are preserved at the v2 composition root.
## Red lines (this subskill)
- Porting is semantic splitting, not file copying — never preserve a v1 class shape in v2.
- Decide scope from state identity before writing v2 code; the scope is fixed at registration.
- Verify the domain mapping against current v2 `src/`; the table here is a starting point, not authority.
- One Service owns state at exactly one lifetime; split global-view + per-instance into registry + per-instance.
- A dependency cycle introduced by the port means a v1 import is now backwards — refactor it; activation timing cannot break the cycle.
How to shut down a scoped service in `agent-core-v2`: when `dispose()` is enough, when to add an async `close()`, and where cancellation / abort belongs. Read this before putting business shutdown logic into a `Disposable`.
## The one-sentence rule
> **`close()` is async business shutdown; `dispose()` is synchronous resource cleanup.**
The container calls it during scope teardown. Disposal order is deterministic (orient.md): child scopes first, then reverse construction order within a scope. Nothing awaits a Promise returned from `dispose()`.
Business shutdown is usually async. It may need to:
- stop in-flight tasks and wait for settlement;
- decide policy (`kill` vs `keepAliveOnExit` vs `markLost`);
- flush write queues and persistence;
- emit final records / events / telemetry;
- close sockets, child processes, or external clients.
If that logic lives in `dispose()`, it becomes fire-and-forget: the scope keeps tearing down, dependencies may be disposed immediately afterward, and the async continuation can run against a half-dead object graph.
## What `close()` owns
Add `close(): Promise<void>` when a service owns async shutdown work:
```ts
export interface IXxxService {
readonly _serviceBrand: undefined;
close(reason?: string): Promise<void>;
}
```
A good `close()`:
- is idempotent — repeated calls return the same Promise or no-op;
- is called by lifecycle code **before**`scope.dispose()`;
- rejects new work after it starts;
- applies shutdown policy explicitly;
- awaits the work it starts;
- leaves `dispose()` with only synchronous cleanup.
Sketch:
```ts
class XxxService extends Disposable implements IXxxService {
- `_register(...)` event subscriptions and hook registrations;
- clear timers;
- remove signal listeners;
- dispose child `IDisposable`s;
- detach from synchronous handles.
`dispose()` must be idempotent and should avoid throwing. If `close()` was already called, `dispose()` should be a no-op for business work and only clean resources.
## Where abort / cancellation belongs
Cancellation is not the same thing as graceful shutdown.
For an operation-scoped object, a cancellation trigger can be disposed:
```ts
const tokenSource = new CancellationTokenSource();
This is fine when the contract is **fire-and-forget cancel**: the operation observes the token and settles asynchronously; disposal does not wait for completion.
For a manager/service that owns many tasks and their state, do not use `dispose()` as the graceful abort path. Expose `stop()` / `stopAll()` / `close()` and let lifecycle code await the one it needs.
Background-specific rule: a `background`-style service may use `AbortController` internally to propagate cancellation to process / agent / question tasks, but manager shutdown belongs in `close()` or explicit `stopAll()`. `dispose()` may best-effort abort controllers only as a safety net; it must not be the mechanism that decides terminal status, persistence, or notifications.
## Decision tree
```text
What does the service own?
│
├─ only event subscriptions / timers / disposable handles?
│ └─ add close(): Promise<void>; call it before scope.dispose().
│
├─ a single operation that callers may cancel?
│ └─ expose an AbortSignal / CancellationToken or a fire-and-forget cancel handle.
│
└─ both async shutdown and disposable resources?
└─ close() for business shutdown; dispose() for resource cleanup.
```
## VSCode parallel
VSCode uses the same split:
- `src/vs/base/common/lifecycle.ts` — `IDisposable.dispose(): void` for synchronous cleanup.
- `src/vs/base/parts/storage/common/storage.ts` — `close(): Promise<void>` flushes and closes the database.
- `src/vs/base/common/cancellation.ts` — `CancellationTokenSource.dispose(true)` / `cancelOnDispose()` cancels operation-scoped work without awaiting it.
The lesson is not "never cancel in dispose". It is: **disposal may trigger cancellation for a scoped operation, but service shutdown policy stays in an explicit async close path.**
## Red lines (this topic)
- Do not put business shutdown in `dispose()` — `dispose()` is synchronous and is not awaited.
- Do not `await` inside `dispose()`.
- Do not rely on `dispose()` to flush persistence, emit final events, wait for tasks, or send notifications.
- Add `close(): Promise<void>` for async shutdown and call it before `scope.dispose()`.
- Keep `close()` and `dispose()` idempotent; `dispose()` after `close()` must be safe.
- Use disposal as a cancellation trigger only for operation-scoped work, not as a manager/service shutdown policy.
# Subskill — Commit align (triage a `main` commit against v2)
Context: you are on the `kimi-code-v2` branch, in the phase of catching it up to **new commits that landed on `main`**. Those commits change `packages/agent-core` (v1); the job is to decide, for one commit at a time, whether v2 (`packages/agent-core-v2`) already has the corresponding logic — and if not, what the minimal fix is.
Use this when the user hands you **one commit hash plus a short description** ("look at `<commit>` — it fixed the steering race"). It is the small, per-commit sibling of [align.md](align.md): `align.md` ports a whole v1 domain into v2; this file triages a single `main` commit and says *port / adapt / skip*. If the triage reveals a whole missing domain, stop and switch to [align.md](align.md).
## The one-paragraph mental model
A `main` commit edits v1's singleton-container code. The same behavior in v2 lives behind a scoped Service, so a commit lands in one of four buckets: **already-aligned** (v2 has it, possibly by construction), **partial** (v2 has a nearby version whose semantics drift), **missing** (v2 has nothing), or **not-applicable** (the v2 architecture removed the very problem the commit fixes). Your output is a bucket assignment plus evidence, then a fix sized to that bucket — never a blind port of the diff.
## The workflow
```text
Read the commit + the user's note → Locate the v1 logic → Map to a v2 domain
→ Check v2 for a corresponding implementation → Bucket it → Recommend a fix → Verify
```
### 1. Read the commit and the note
**Goal:** know exactly what changed in v1 and *why*. The user's one-liner gives the intent; the diff gives the facts.
Actions:
- Inspect the change scoped to v1: `git show <commit> -- packages/agent-core` (and `--stat` first to see the blast radius).
- From the diff, list: touched files, changed functions/methods, and the observable behavior delta (before → after).
- Reconcile with the user's note: is this a bugfix, a semantic correction, new behavior, or a refactor? The *why* decides whether v2 even needs the change.
Do not skim the user's sentence and guess — the diff is the spec for what "aligned" means here.
### 2. Locate the v1 logic
Pin the change to a v1 place: the contract (`<domain>/<domain>.ts`) + impl (`<domain>/<domain>Service.ts`), or the helper/handler the commit touched. Note which state it reads/writes and which other v1 services it calls — this is the same inventory as [align.md](align.md) §1, scoped to the commit's footprint.
### 3. Map to a v2 domain
Use the v1 → v2 domain table in [align.md](align.md) §3 as a starting point, then **verify against the current `packages/agent-core-v2/src/` tree** — it is the source of truth. Identify the candidate v2 Service(s) that would own this behavior, and their `LifecycleScope`.
### 4. Check v2 and assign a bucket
Search the candidate domain in v2 (Grep the method name, the state field, the error code). For each piece of the commit's behavior delta, decide:
- **Already-aligned** — v2 produces the same observable result (sometimes for free, because the v2 design never had the bug). Cite the v2 file:line.
- **Partial** — v2 has a near miss: same method, different guard/ordering/error; or the state lives at a different scope. Name the exact drift.
- **Missing** — no v2 Service owns this behavior. Confirm it is a single-Service gap, not a whole-domain gap (latter → [align.md](align.md)).
- **Not-applicable** — the v2 architecture removed the condition the commit fixes (e.g. the scope tree already serializes what v1 patched with a lock). Explain why, so a reviewer trusts the skip.
Every claim needs a citation (`path:line`) on both sides; "I couldn't find it" is a finding only after you name where you looked.
### 5. Recommend a fix (sized to the bucket)
- **Already-aligned** — say so and stop; reference the v2 location. No code change.
- **Partial** — propose the smallest edit that closes the drift: which Service, which method, which guard. Stay inside v2 rules — scope/domain direction, no `Map<sessionId, …>` at `App` (see [align.md](align.md) §6–§7 red lines).
- **Missing** — sketch the port at commit granularity: target domain + scope, the Service/method to add or extend, the dependency direction, and which [align.md](align.md) §7 conversions apply (registration, `#/…` imports, co-located coded error, `IFlagService` for any gate). If it needs a new scope or a wire change, flag it.
- **Not-applicable** — recommend no v2 change, but call out any test worth adding so the gap stays closed.
Keep the recommendation to the commit's footprint. If it keeps growing, that is the signal to hand off to [align.md](align.md) for a full domain port.
### 6. Verify
Point at the checks that cover the fix, per [verify.md](verify.md): `lint:imports`, `typecheck`, and the relevant `test`. Note the expected outcome rather than asserting you ran it if you did not.
## Output shape
When triaging, answer in this order so the user can act on it directly:
1. **Commit + intent** — one line restating what the commit changed and why (from the note + diff).
2. **v1 location** — file(s) and the behavior delta.
3. **v2 status** — one of the four buckets, with `path:line` evidence on both sides.
4. **Recommendation** — the concrete fix (or the justified skip), scoped to the commit; name the target Service / scope / dependency direction.
5. **Verify** — which checks should pass, and whether to escalate to [align.md](align.md).
## Red lines (this subskill)
- Read the diff and the note before judging v2; never infer "aligned" from the description alone.
- Do not copy a v1 diff into v2. Decide the bucket first; a bugfix commit often maps to **not-applicable** because the v2 design already removed the defect.
- Cite `path:line` on both sides. A recommendation without evidence is a guess.
- Stay in the commit's footprint. Growing scope means "switch to [align.md](align.md)", not "keep porting here".
- Do not break v2 invariants to chase v1 parity — scope direction, domain direction, and no `Map<sessionId, …>` at `App` still hold ([align.md](align.md) red lines).
How the `config` domain works and how a domain owns its configuration section. Covers the section-registry model, the App vs Session split, the TOML on-disk format, and the recipe for adding or migrating a config section.
The `config` domain is a thin registry + loader: it does **not** know the shape of any individual section. Each domain owns the schema (and, where needed, the TOML transform) for the config it consumes, contributes the section (statically at module load via `registerConfigSection`, or at runtime as a `ConfigSectionContribution` collection record), and reads it through `IConfigService`. There is no whole-config object passed around.
## What belongs in Config
`IConfigService` is the **preference registry**: it holds values a user or
operator *chooses*, each with a schema and a default, that *can* be persisted to
`config.toml`. It is not a grab-bag for every value a domain needs. Before
registering a section, classify the value along three axes — **decision-maker**,
**preference vs fact**, **mutability / persistence**:
| Type | Decision-maker | Preference/Fact | Persisted? | Examples | Home |
|---|---|---|---|---|---|
| User preference | user | preference | ✅ config.toml | model, theme, log level | **Config** |
- `src/app/config/configService.ts` — `ConfigRegistry` + `ConfigService` impl; self-registers at App scope. The registry is also the fold of the `ConfigSectionContribution` collection: it drains the module-level contributions at construction, then refolds incrementally (`added` → `registerSection`, `removed` → `unregisterSection`).
- `src/app/config/configSectionContributions.ts` — the `ConfigSectionContribution` collection token (the runtime channel: a unit contributes with `this.provide(ConfigSectionContribution, …)`) plus the module-level `registerConfigSection` collector (the static channel, import = register).
- `src/app/config/configOverlayContributions.ts` — the module-level `registerConfigOverlay` collector for `ConfigEffectiveOverlay`s (drained at construction like the sections).
- `src/app/config/toml.ts` — generic snake_case ↔ camelCase machinery plus the registry-aware `transformTomlData` / `applySectionToToml` entry points. Per-domain normalization lives in the section owner's `configSection.ts` (registered as `fromToml` / `toToml`); this module stays free of any other domain's semantics.
- `src/kosong/model/thinking.ts` (owner domain, not `config`) — the `resolveThinkingEffort` helper and the authoritative `ThinkingConfig` type (the `thinking` section itself registers from `src/app/kosongConfig/configSection.ts`).
A domain that owns a section keeps the schema in its own `configSection.ts` (e.g. `src/app/flag/flag.ts` for `experimental`, `src/agent/loop/configSection.ts` for `loopControl`). Exception: kosong-owned sections (`providers`, `models`, `thinking`) — kosong is a pure, persistence-free abstraction layer that defines only the types (`src/kosong/{provider,model}`); the section constants, the zod schemas (re-derived from those types and compile-time pinned via `AssertExact<Equal<z.infer<typeof Schema>, Type>>`, see `_base/utils/typeEquality.ts`), the registrations, env bindings, and TOML transforms all live in the persistence wrapper `src/app/kosongConfig/configSection.ts`. (`modelCatalog` has no kosong-side type at all — its section is fully self-contained in `app/kosongConfig`, types derived from the schema.) A cross-section env overlay (e.g. the `KIMI_MODEL_*` synthesis in `src/app/kosongConfig/envOverlay.ts`) lives in the wrapper too and is registered via module-level `registerConfigOverlay`. The session subagent domain owns two sections in `src/session/subagent/configSection.ts`: `[subagent]` (`timeout_ms` on disk) and `[secondary_model]` (`default_model` plus the `[secondary_model.models]` pool, with a lone legacy v1 `model` key honored as a fallback default below `default_model`); neither carries a cross-section overlay. Cross-field pool validation (default present / in-pool / every key resolvable) runs at session creation in `subagentModelsValidationService.ts`, not in the schema. The two-way sync between config sections and kosong's in-memory registries is owned by `IKosongConfigService` (`src/app/kosongConfig/kosongConfigService.ts`).
## Scope
- `IConfigRegistry` / `IConfigService` — **App** scope, process-global. One registry of sections; one loader reading `~/.kimi-code/config.toml` (path from `IBootstrapService.configPath`).
All config reads go through `IConfigService` (global config). Per-session runtime state (active model, thinking level, etc.) lives in the owning Session-scoped service (e.g. `IProfileService`), not in `config`.
## The section-registry model
A config section is identified by a camelCase domain key (`'providers'`, `'thinking'`, `'loopControl'`). Each section has:
- `schema?: ConfigSchema<T>` — zod schema used to validate the value (absent ⇒ passthrough).
- `defaultValue?: T` — filled when the file has no value for the domain.
- `merge?: ConfigMerge<T>` — how `set(domain, patch)` combines base + patch (default `deepMerge`).
- `fromToml?: ConfigFromToml` — read-path transform (snake_case file value → in-memory shape). Defaults to a plain key-casing pass; owners register one when the on-disk shape needs custom normalization (record key preservation, nested object conversion, array entries, key renames, reshapes).
- `toToml?: ConfigToToml` — write-path transform (in-memory value → snake_case file value). Defaults to a plain camelCase→snake_case key mapping.
Two contribution channels:
- **Static (import = register)** — the owning domain calls `registerConfigSection(domain, schema, options)` at the top level of its `configSection.ts`; `ConfigRegistry` drains the collected contributions when it is constructed. Every in-repo section uses this channel.
- **Runtime (collection record)** — a unit contributes `this.provide(ConfigSectionContribution, { domain, schema, options })` (e.g. a feature assembled through `IFeatureManager`); the `ConfigRegistry` fold registers the section when the record lands and unregisters it when the record is withdrawn (provider disposed). User TOML values survive a withdrawal — they just stop being validated and effective.
Ownership rules:
- **One owner per section.**`registerSection` throws if a domain is registered twice — the static channel fails fast when `ConfigRegistry` drains it; a conflicting runtime record is reported through `onUnexpectedError` and the first registration wins (the fold is an event path and never throws).
- **The domain that consumes a config owns its schema.** This is what keeps `config` from depending on its consumers: `config` must not import `externalHooks` / `permissionRules` / `provider` / `kosong` / etc. for a section's schema. If a schema needs a domain's types, the schema lives in that domain.
- **Demand-driven.** Do not register sections for config that no domain reads yet; a section appears (with its schema in the owning domain) only when a consumer appears.
## Env bindings
A section can declare how its fields are read from environment variables, so the
value resolves through `config.get(...)` rather than ad-hoc `process.env` reads.
Declare the bindings with `envBindings(schema, { … })` — the field names are
type-checked against the schema (no magic strings), and nested schemas recurse:
`ConfigRegistry` drains module-level contributions when it is constructed, so the section exists before any consumer resolves `IConfigService` — no owning Service needs to be constructed first. Make sure `src/index.ts` imports the leaf so the top-level call runs.
3. (Runtime variant) a dynamically loaded unit (e.g. one assembled through `IFeatureManager`) contributes the section as a collection record instead:
The `ConfigRegistry` fold registers it incrementally and unregisters it when the unit is retracted (user TOML values survive) — see "Late registration".
└──────── IConfigService.set/replace ◀──── only on explicit writes
```
- **Read path** (startup, every service): `config.toml` is loaded into `IConfigService` once; services read via `get()`. This path **never writes the file**.
- **Write path** (rare): `config.toml` is rewritten only when something explicitly calls `IConfigService.set/replace`. The only production writers today are provider CRUD (`ProviderService.set/delete`, e.g. provisioning a provider after OAuth login).
**Runtime service state is not config.** Mutating a service at runtime does **not** rewrite `config.toml`:
- `ProfileService.configure(...)` / `update(...)` / `setModel(...)` / `setThinking(...)` only change **in-memory** fields and append to the session **wireRecord** (for replay). They never call `IConfigService.set`.
- Switching model or thinking level mid-session is session runtime state, not a config edit — the user's `config.toml` is left untouched.
So `configure(...)` never overwrites the local file. Treat `config.toml` as the user's static config; runtime overrides live in memory and the session record.
## Late registration
`ConfigService` loads in its constructor (first `get(IConfigService)`). Static sections are drained before that, but a runtime-contributed section (a `ConfigSectionContribution` record) can register at any later moment. To keep validation and defaults correct:
- `IConfigRegistry` emits `onDidRegisterSection` whenever a section is registered (and `onDidUnregisterSection` when a runtime record is withdrawn).
- `ConfigService` subscribes and, on registration, re-validates the already-loaded raw value for that domain, applies the default if the raw value is absent, re-runs the env overlay, and fires `onDidChange` if the effective value changed. On unregistration it devalidates the domain — `get(domain)` falls back to the raw value.
- Before a section is registered, `get(domain)` returns the raw (transformed, unvalidated) value; consumers that need validated values should read after the section lands, or react to `onDidChange`.
This means registration order is never a correctness concern — you do not need an eager bootstrap.
## TOML on-disk format
`config.toml` stores keys in **snake_case**; in-memory values are **camelCase**. `ConfigService` converts both ways by dispatching to each section's registered transform:
- **Read**: `transformTomlData(fileData, registry)` maps each top-level key to a domain and applies that domain's `fromToml` hook (or a plain key-casing pass when none is registered). Owner domains register their own normalization — e.g. provider `oauth`/`env`/`customHeaders`, permission `deny/allow/ask` → `rules`, `experimental` keys preserved verbatim. When a section registers after the initial load, `ConfigService` re-applies its `fromToml` against the preserved snake_case raw value (see "Late registration"), so registration order is never a correctness concern.
- **Write**: `applySectionToToml(rawSnake, domain, value, registry)` applies the domain's `toToml` hook (or a plain camelCase→snake_case mapping) into a raw clone of the file, preserving unknown top-level keys and unknown sub-fields (lossless round-trip).
`ConfigService` keeps four views:
- `rawSnake` — snake_case clone of the file; the write base, never carries the env overlay.
- `raw` — camelCase, env-free; the read/set/replace base.
- `validated` — validated `raw`, env-free; the base every live env re-application starts from, so a degraded or removed env value falls back to the file instead of a stale overlay.
- `effective` — `validated` plus the env overlay, recomputed on load/set; `get()`/`getAll()` re-apply the overlay on a fresh `validated` copy per read rather than caching it.
### Renaming config keys and env vars (deprecations)
Renames are declared once on the section, never hand-rolled in `fromToml`:
- A deprecated TOML key is **ignored** (its value no longer applies — the schema only knows the new key) and reports a warning `ConfigDiagnostic` while present; the file is never rewritten, so the warning is the migration guide. Diagnostics are recomputed on every load/reload and surface to clients via `IConfigService.diagnostics()` and `onDidChangeDiagnostics` (kap-server republishes them as the global `event.config.warning` WS event).
- A deprecated env var still **resolves** as a fallback (new var first), with the same warning treatment, and `stripEnvBoundFields` treats it as env-owned for writes.
- See `src/agent/loop/configSection.ts` for a worked example (`max_retries_per_step` → `max_attempts_per_step`).
### `KIMI_MODEL_*` env overlay
When `KIMI_MODEL_NAME` is set, the `kosongConfig` wrapper's `kimiModelEnvOverlay` (`src/app/kosongConfig/envOverlay.ts`) injects a reserved model alias (`__kimi_env_model__`) into `effective`, points `defaultModel` at it, and merges the request `modelOverrides`; the reserved provider (`__kimi_env__`) comes from the `providers` section env bindings. The overlay is registered via module-level `registerConfigOverlay` and applied **only to `effective`**, never to `rawSnake`, so it is never persisted. Its `strip` (plus the providers section `stripEnv`) is the final guard so a caller that read `effective` (with the overlay) cannot write the reserved entries or the shell API key back to disk. `config` itself only runs registered overlays — it does not know the `KIMI_MODEL_*` semantics.
## Owner-owned sections
`config` holds no monolithic config schema and no whole-config object. Every section is owned by the domain that consumes it: the schema (and any `fromToml` / `toToml` normalization and `stripEnv`) lives in that domain's `configSection.ts`, and the domain contributes it via module-level `registerConfigSection` (or a runtime `ConfigSectionContribution` record). Cross-section env behavior (e.g. `KIMI_MODEL_*`) lives in an owner-registered `ConfigEffectiveOverlay` (module-level `registerConfigOverlay`). To add a section, follow "Add a config section" above in the owning domain — never add schema or normalization to `config` itself.
## Ownership map (generated)
The authoritative, always-current list of registered sections — rendered in the on-disk `config.toml` shape, with owner file, scope, defaults, env bindings, and schema fields — is generated from the live registry:
- `packages/agent-core-v2/docs/config-manifest.toml` (checked in; do not edit by hand).
- Regenerate with `pnpm --filter @moonshot-ai/agent-core-v2 gen:config-manifest` (add `--check` for a freshness check; `test/app/config/configManifest.test.ts` enforces it in CI).
`config` must not import from any of these owner domains; that is the whole reason the schemas, TOML normalization, and env overlays live with their owners.
## Scope & dependencies
- `config` is a low-level capability: domains that own sections import `config` (for `IConfigRegistry` / `IConfigService`), never the reverse — section schemas live in the owning domain.
- Cross-domain type sharing for a config type: prefer importing the type from the owning domain over re-declaring it (e.g. `plugin` imports `McpServerConfig` from the MCP config schema).
- `IConfigRegistry` / `IConfigService` are **App**. Agent scope services may inject App services via ancestor lookup.
- `config` never imports a higher domain and holds no section schemas of its own; if a section needs a type from another domain, that schema lives in that domain.
## Red lines (this topic)
- One owner per section: a duplicate static registration throws when `ConfigRegistry` drains it; a conflicting runtime record is logged (`onUnexpectedError`) and the first registration wins.
- `config` never imports the domains that consume it — keep section schemas in the owning domain.
- Config is the **preference registry**: register only values that are preferences, persistable, schema'd, and user/operator-facing. Facts → `IBootstrapService`; session state → Session scope; constants → code.
- Business domains read `config.get(...)` or structured `IBootstrapService` facts; never call `IBootstrapService.getEnv()` directly — only `config` reads the raw env bag to build overlays.
- Keep `IBootstrapService` domain-agnostic: host invocation arguments (CLI flags, host identity headers, prompt identity) go into `BootstrapInput.args` / `IBootstrapService.args` — never into new per-domain runtime-options services; domain runtime state (cron, flags, model params, …) never goes onto `IBootstrapService` at all. Domain-specific config goes through `registerConfigSection` + `envBindings`, read via `config.get(...)`.
- Do not pass a whole config bag via options; read each section through `IConfigService`. There is no `KimiConfig` object — config is a registry of owner-owned sections.
- `config.toml` is snake_case on disk, camelCase in memory — never write camelCase keys to disk, and never write to `config.toml` except through `IConfigService.set/replace`.
- Reading config / calling `configure(...)` / switching model at runtime must not rewrite `config.toml`; runtime state lives in memory and the session wireRecord, not the file.
- Never persist env overlays (`__kimi_env__` / `__kimi_env_model__` / shell API key / experimental env); overlays live only in `effective` / `Memory`.
- Runtime contribution (a `ConfigSectionContribution` record from a unit at any scope) is fine — the late-registration mechanism keeps validation correct; the static channel needs no eager bootstrap (import = register, drained at `ConfigRegistry` construction).
Decide *where things live and who knows whom* before writing code. Every rule here derives from two questions:
1. **What is the identity of the state it owns?** → decides the **Scope**.
2. **Who owns the decision, and who needs the result?** → decides the **calling style** and **dependency direction**.
## 1. What a Service is
A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetime**.
- **Behavior** is almost free — the same logic runs anywhere, so it does not by itself decide a scope.
- **State** pins a Service to a scope. State has an **identity** (what it is keyed by) and a **lifetime** (when it is born, when it dies).
- **Dependencies / calling style** answer a different question: who controls whom, and who knows whom.
## 2. Choosing a scope
> Scope = the identity + lifetime of the owned state.
| Scope | State identity (keyed by) | Lifetime |
|---|---|---|
| `App` | none (single global instance) | the process |
| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) |
| `Session` | `sessionId` | one session |
| `Agent` | `agentId` | one agent |
### Decision tree
**Q1. Does it own mutable state?**
- No (pure behavior) → jump to Q3.
- Yes → Q2.
**Q2. What is the identity of that state?**
- one global instance → **`App`**
- one per workspace (shared by every session of that workspace) → **`Workspace`**
- one per session → **`Session`**
- one per agent → **`Agent`**
- a mix (a global registry *and* per-instance state) → **split it** (see §3).
**Q3 (stateless). What is the shortest-lived dependency it must inject?**
A stateless Service is pulled *down* by its shortest-lived dependency: if it injects an `Agent`-scoped Service, it cannot be `App`. Among the scopes that still satisfy every dependency, **default to the longest-lived one** (usually `App`) to maximize reuse. Push it down only when it must inject a shorter-lived Service, or when you want to limit its visibility.
### The core anti-pattern (a litmus test)
> **Do not store per-session state in a `Map<sessionId, …>` inside an `App` Service.**
This is the tell-tale sign of "should have been `Session`-scoped but was parked at `App`". Consequences: nobody cleans the entry up when the session ends (leak); every consumer threads `sessionId` around (loss of type safety); it cannot inject `Session`/`Agent`-scoped collaborators.
### One-sentence self-check
> "When this scope is disposed, should this state disappear with it?"
>
> - Yes → the scope is right.
> - It must outlive the scope → too short; move up one tier.
> - It should be one-per-unit but is shared → too long; move down one tier.
## Scope is not a domain
Scope answers **lifetime and visibility**. Domain answers **responsibility and data ownership**. A Service registered at `Session` or `Agent` scope is not automatically part of the `session` or `agent` domain, and an entity Service must not be named `I{Scope}EntityService` just because its data is scoped that way.
Use the data-ownership test and the `session` / `agent` / `turn` split conclusions in [domain-boundaries.md](domain-boundaries.md) before naming a Service or adding `I{Domain}EntityService`.
## 3. Multi-Scope splitting
> One Service owns state at exactly one identity / lifetime. If a domain owns state at several lifetimes, split it along those boundaries — one Service per lifetime.
The standard split is "global registry / factory" + "per-instance":
| Tier | Role | Naming tends to |
|---|---|---|
| `App` | global registry / catalog / factory — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` |
| `Workspace` / `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` |
- **`kosong`** — `IProtocolHandlerRegistry` (`App`) + `IProviderManager` (`Session`). Generation is driven by `ILLMRequester` (`Agent`) in the `llmRequester` domain.
Split when the domain genuinely has both a global view and per-instance state. Do **not** split when state lives at only one lifetime (e.g. purely `App` like `log`; purely `Agent` like `prompt`). Do not pre-split for symmetry.
After the split, the `App` Service usually plays the **factory**; most consumers inject the **per-instance** Service. Inject the `App` factory only when you genuinely need cross-instance management.
## 4. Choosing a calling style
Three mechanisms answer three different questions:
| **Direct call** | command: A tells B to do | A → B | yes | one (known) |
| **Event** | fact: A announces "X happened" | both depend only on the bus | no | zero / one / many (unknown) |
| **Hook** (`onWill` / `onDid`, `OrderedHookSlot`) | participation: observers step into an operation, in order | both depend only on the bus | can observe / veto | many, but ordered |
### Decision tree
**Q1. Does A need a return value from B?** → Yes: **direct call**. Events cannot return a value (request/reply over events is an anti-pattern).
**Q2. Is B's reaction part of A's responsibility, or B's own concern?**
- A's responsibility *includes* B's behavior (A orchestrates B) → **direct call**. E.g. `session` drives `agentLifecycle`; `loop` drives `llmRequester` / `toolExecutor`.
- B's reaction is B's own concern, A merely states a fact → **event**. E.g. `flag` reacts to `config.onDidChange`.
**Q3. How many consumers?**
- exactly one, known → **direct call**.
- zero / one / many, producer should not know → **event**.
**Q4. Would a direct A→B call create a cycle or violate scope direction?** → A *consequence check*, not a primary reason. Decide by Q1–Q3 first; do not turn a genuine direct call into an event just to break a cycle.
**Q5. Is this fact part of the durable record / replay / cross-agent projection?** → Yes: **emit it on the wire** (`wireRecord`). State changes that must be recorded, replayed, or synchronized across agents are projected onto the wire, not handled by a direct call alone (`permission.set_mode`, `goal.create/update/clear`, `plan_mode.enter/exit`). The wire is the *durable record*, not the live notification channel.
### One-sentence rule
> "I am telling you to do this, and I may need the result" → **direct call.**
> "I am announcing that something happened; react if you care" → **event.**
> "I am announcing something, and you may step in, in order, possibly to veto" → **hook.**
### As extension points (open-closed)
The three mechanisms above are also where a domain accepts new behavior without being edited. When adding a scenario would otherwise require changing this domain's `if/else`, expose the right extension point instead:
| Need | Extension point | Typical scope |
|---|---|---|
| Register a new implementation / definition | a **registry / catalog** the domain queries | `App` |
| React to a fact the domain announces | an **event** on the bus | the announcing scope |
| Step into an operation in order / veto | a **hook** (`onWill`/`onDid`, `OrderedHookSlot`) | the owning scope |
| Swap a backend (File ↔ DB ↔ S3) | a **Store / Storage token** at the byte layer (see persistence.md) | `App` (composition root) |
The standard shape of a "registry / catalog the domain queries" row is an L3 contribution point: the target domain owns a `collection<T>` token, contributors call `this.provide(token, record)` from a unit, and a fold service in the target domain injects the `CollectionView` (incremental `onDidChange`; provider death withdraws the record). The four in-repo seams are `ConfigSectionContribution` → `ConfigRegistry`, `AgentToolContribution` → `AgentToolActivationService`, `AgentProfileContribution` → `IAgentProfileRegistry`, and `WireModelContribution` → `WireService` (file-level pointers: `packages/agent-core-v2/AGENTS.md` §Units and contribution points).
Closed-for-modification means: the domain's own file is not where new scenarios branch. If a new scenario forces an edit here, an extension point is missing or misplaced.
## 5. Dependency direction
Two layers are involved:
- **Scope direction**: short-lived → long-lived, **enforced by the container** (see orient.md).
- **Domain direction**: which domain may depend on which — **a matter of judgment**, not enforced by the container.
> **A depends on B iff A needs B's data or behavior to do its own job.**
Add one anti-rot heuristic to keep the graph from collapsing into a clique:
> **Do not let a more foundational / more-reused Service come to know a more specific / more-upstream one.**
Once a foundational component knows about an upstream scenario, it can no longer be reused by other scenarios and will almost always create a cycle.
### The boundaries of this repo
`agent-core-v2` has no mechanical domain-layer numbering — dependency direction is the judgment rule above, applied per domain. What remains enforceable is a small set of specific boundaries (`lint:imports`, `scripts/check-import-boundaries.mjs`):
- v2 never imports v1 (`@moonshot-ai/agent-core`).
- The kosong subtree keeps its strict internal order (`contract ← protocol ← provider/model`, purity bans, the `provider/bases` registration boundary).
Two standing red lines on top of that:
- The **base substrate** (`_base`, errors, wire types) never depends on any business domain.
- Business logic never depends on the **edge** (`gateway`, `rpc`, the `*Legacy` v1 adapters) — business code should not know REST / WebSocket exist.
- A cycle means knowledge was placed backwards: extract a third, more foundational Service, or invert the "notification" half into an event.
> Capability → orchestrator (e.g. `prompt → turn`) is allowed and present in this repo; the real red line is *inverted reuse* — a foundational / lower Service depending on a specific / upper one.
> When a Service is meant to be reached over the wire (`/api/v2`, WS), see [edge-exposure.md](edge-exposure.md) for the per-scope `resource:action` map, which Services may be exposed directly vs wrapped in a facade, and how events stream.
## 6. New-Service checklist
1. **What does it remember, and what is the state's identity?** → pick the scope (§2).
2. **What is the shortest-lived dependency it must inject?** → the scope cannot be longer than that.
3. **Does it own state at both a global and a per-instance lifetime?** → if yes, split Multi-Scope (§3).
4. **For each collaborator: am I commanding it, notifying it, or letting it participate?** → pick the calling style (§4).
5. **Does each dependency arrow make a more foundational thing know a more specific thing?** → if yes, invert it (§5).
## 7. Render the placement tree
After the checklist, render the result as a plaintext tree — the deliverable reviewers read. Keep it in the design doc or PR description.
- List **only real interfaces**; write `—` for a scope with no exposed interface. Most domains are single-scope — do not invent symmetry.
- On `depends`, tag each arrow with its calling style: `direct`, `event`, or `hook`.
- On `serves`, tag each consumer with its **access mechanism**, grouped `inject` first then `accessor`:
- `inject` — a descendant or peer scope DI-injects me. Resolved by the container; lifetime-safe.
- `accessor` — an ancestor or edge scope borrows me through `IScopeHandle.accessor.get(...)`. Valid only while this scope lives; never cache the result; must run before the child scope is disposed. See the cross-scope borrow diagram below.
- An empty `(inject)` group with a non-empty `(accessor)` group is a signal: the interface is currently an edge / lifecycle command surface — check it is not leaking internals.
- A consumer is upstream of you. If you cannot name one business consumer, the domain may be dead or mis-scoped.
### Cross-scope borrow diagram
When a domain has `accessor` consumers, draw the reverse-direction borrow next to the tree so it is never mistaken for injection:
```text
App scope
<AncestorService> ──holds──► IScopeHandle(<id>)
│
│ accessor.get(<IMyService>)
│ └── resolve runs inside the child scope
▼
<Child> scope (<id>)
<MyService> ← the interface lives here
```
Read it as:
- `──holds──►` = the ancestor owns a handle to the child scope (it stores the key, not the service). DI allows this.
- `accessor.get(...)` = a **runtime borrow**, not a dependency edge. It must cross an `IScopeHandle`, run on demand, never be cached, and finish before the child scope is disposed.
sessionMetadata / agentLifecycle / … ← per-session services live here
```
How the three lenses shaped it:
- **Scope (§2)** → the live registry of one workspace's session scopes is per-handler, so it is Workspace-scoped; the process-wide handler registry lives in the App-scoped `workspaceLifecycle`; per-session data stays in Session-scoped services, reached through the handle's `accessor`.
- **Dependency direction (§5)** → `sessionLifecycle` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service.
- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `sessionLifecycle`.
For a multi-scope split, the `exposes` block fills more than one scope — see the `records` pattern in §3.
## Red lines (this stage)
- Scope is not a domain; ownership follows write authority and invariants, not read consumption.
- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`) that re-merge multiple domains.
- No `Map<sessionId, …>` at `App` to fake per-session state.
- Scope follows state identity; stateless Services are pulled down by their shortest-lived dependency, otherwise default to `App`.
- Do not pre-split a domain that has state at only one lifetime.
- Need a result / I orchestrate → direct call; stating a fact → event; ordered participation / may veto → hook.
- Foundational layers never know upstream ones; business code never depends on the edge layer.
- A cycle means knowledge is placed backwards — refactor, do not route around it.
- Render the placement tree with real interfaces only — never pad an empty scope for symmetry.
- Tag `serves` consumers with `inject` / `accessor`; an empty `inject` group is a signal to check the interface is not leaking internals.
- An `accessor` consumer is a runtime borrow across a scope boundary, not DI injection — never cache the result and finish before the child scope disposes.
- A `serves` list with no business consumer (or only edge consumers) signals a dead or leaking interface.
How to keep `agent-core-v2` from recreating a god object after splitting one. Read this before naming a Service, adding an `I{Domain}EntityService`, or deciding whether data belongs to `session`, `agent`, or `turn`.
## The one-sentence rule
> **Scope is a lifetime and visibility boundary; a domain is a responsibility and data-ownership boundary.**
A Service registered at `LifecycleScope.Session` or `LifecycleScope.Agent` is **not automatically in the `session` or `agent` domain**. Scope says when an instance is born, when it dies, and who can see it. Domain says which business responsibility it owns and which data it is allowed to mutate.
## Definitions
| Term | Meaning |
|---|---|
| **Scope** | Lifetime / visibility tier. Current code registers Services at `App`, `Session`, or `Agent`. |
| **Domain** | A cohesive business responsibility with its own model, invariants, and write authority. |
| **Entity** | Data with identity and lifecycle, usually suitable for `get/list/create/update/delete` semantics. |
| **Aggregate** | A consistency boundary: the owner that enforces invariants over a cluster of data. |
| **Read model / projection** | Derived data built for queries; it may be shaped like a domain, but it is not the write authority. |
| **Runtime state** | Ephemeral data that dies with its scope; it should not be forced into an entity store. |
## The data-ownership test
Do not ask "does Session / Agent / Turn use this data?". Most data is used by several of them. Ask these instead:
1. **What is the data's identity?**`sessionId`, `agentId`, `turnId`, `taskId`, `workspaceId`, `providerName`, or something else?
2. **Who is the only writer?** The writer is usually the owner. Readers and projectors are not owners.
3. **Who enforces the invariants?** The domain that decides valid transitions owns the model.
4. **What is the authoritative source?** Atomic document, append-log / event stream, blob, query projection, config, or runtime memory?
5. **Can it be named without `Session` / `Agent` / `Turn`?** If yes, it probably deserves its own domain.
Examples:
- `PermissionRules` are Agent-scoped, but `permission` owns rule changes and evaluation.
- `BackgroundTask` is spawned by an Agent, but `background` owns task state and output.
- `ContextMessage` is consumed by the Agent loop, but `contextMemory` / `wireRecord` owns history and replay.
- `SessionMeta` is about a Session, but it is owned by `sessionMetadata`, not by a broad `session` data bag.
## Persistence models are not all entity CRUD
Before introducing `I{Domain}EntityService`, classify the persistence model:
| Persistence model | Use when | Examples |
|---|---|---|
| **Atomic document** | One typed document per key | `SessionMeta`, `config.toml` |
| **Append-log / event-sourced** | The authoritative record is "what happened" | `wireRecord`, `contextMemory`, `goal`, `plan`, `permission` transitions |
| **Blob / key-value** | Large or content-addressed bytes | media offload, blob store |
| **Registry / catalog** | Global or scoped known items | `workspace`, `toolRegistry` |
| **Ephemeral runtime state** | No durable entity | active turn handle, pending interactions, terminal handles |
See [persistence.md](persistence.md) for the `Store → Storage → backend` rules. A domain EntityService is a business facade over those stores; it is not a replacement for the store layer.
## Naming consequence
Do not name Services after a scope or a god-object-shaped concept:
- ❌ `IAgentEntityService`
- ❌ `IAgentDataService`
- ❌ `ISessionEntityService`
- ❌ `ITurnEntityService` that bundles context, tools, permissions, and telemetry
Name Services after the real owning domain:
- ✅ `ISessionMetadata`
- ✅ `ISessionIndex`
- ✅ `IAgentLifecycleService`
- ✅ `ITurnService`
- ✅ `IBackgroundTaskEntityService`
- ✅ `ICronTaskEntityService`
- ✅ `IPermissionRulesService`
`Session` and `Agent` are valid scope names. They are usually **not** good data-owner names.
## Split conclusion — `session`
`session` is both a Scope and a narrow Domain. Keep the Domain small.
The `session` domain owns only Session-level identity, metadata, lifecycle commands, and Session-level read views:
| Open session scope registry | `sessionLifecycle` | Workspace-scope live handles, one registry per workspace handler (the process-wide handler registry is `workspaceLifecycle`); not the persisted entity table |
| Session commands such as `archive()` | `session` | Orchestrates metadata, agent teardown, and events |
| Persisted session list / get / count | `sessionIndex` | Backend-neutral read model |
| Running / idle / awaiting status | `sessionActivity` | Derived from interactions and active turns; owns no state |
| Background tasks | `background` | Task records / output logs, candidate for entity service |
| Cron tasks | `cron` | Task records, candidate for entity service |
Entity-service conclusion for `agent`:
- ✅ Keep `IAgentLifecycleService` for Agent instance lifecycle.
- ✅ If a persisted Agent identity registry is ever needed, name it after that narrow concern, e.g. `IAgentInstanceRegistry`.
- ❌ Do not create `IAgentEntityService` or `IAgentDataService` that bundles profile, records, tools, permission, goal, plan, background, cron, and turn.
## Split conclusion — `turn`
`turn` is a Domain, but it is **not** currently a separate `LifecycleScope` in code; `ITurnService` is registered at `Agent` scope.
`turn` owns one execution round's runtime state and turn-level facts:
How a domain's Services become the wire surface (`/api/v2`) and WebSocket events. This is a **design-time** decision: which Services are exposed, under what public `resource:action` name, and which events stream.
The transport (`/api/v2` over HTTP + WS) lives in the **edge** layer (`gateway`/`rpc`/`transport`). It borrows business Services by interface; business code never imports it.
The `actionMap` is the single allowlist: only mapped `resource:action` pairs are callable; unknown → `40001`.
## 2. What may be exposed directly
A Service method is directly exposable iff **all** hold:
1. Args are JSON-serializable (no live objects, `AbortSignal`, callbacks, resumer fns).
2. Return is JSON-serializable data or `void` (no `IScopeHandle`, `Turn`, `IProcess`, `AsyncIterable`, `IDisposable`, `Event`).
3. Errors are `KimiError` (coded).
4. It is a command/query, not a factory, stream, byte-store, or sink.
If any fail → add a wire-safe orchestration method to the owning domain Service (e.g. `IAgentPromptService.submit` settles `{turn_id}` instead of returning the live `PromptHandle`) or compose several domain Services at the edge — kap-server's `routes/prompts.ts` is the reference for edge-side composition.
| IExternalHooksService | server-side outbound | not exposed |
| IWireRecord | write-ahead log | internal |
## 5. WS events
A single WebSocket endpoint multiplexes RPC `call`s and event `listen`s over a JSON protocol (the lean counterpart of VSCode's `IMessagePassingProtocol`, carrying the same safety features — see §6):
Server → client: `ready`, `result`, `error`, `event`, `ping`.
`call` reuses the same dispatcher as the HTTP routes (scope + `actionMap`). `listen` subscribes to an `Event<T>` source and forwards each emission as an `event` message, keyed by the client-chosen `id`.
The `eventMap` binds a public event name to the scope's `Event` source (analogous to the `actionMap`):
Error infrastructure for agent-core-v2: base classes, the per-domain code contract, wire serialization, and the conventions domains follow when raising errors. The package-level reference is `packages/agent-core-v2/docs/errors.md`; this topic summarizes the hot-path rules.
Base classes and serialization are **centralized** in `_base/errors`; error **codes** are **decentralized** — each domain owns an `errors.ts` that self-registers its codes and metadata, and the `src/errors.ts` facade aggregates them into the unified `ErrorCodes` const.
## Where things live
- `src/_base/errors/errors.ts`: base classes — `Error2`, `ExpectedError`, `ErrorNoTelemetry`, `BugIndicatingError`, `NotImplementedError`, plus `isError2` and `unwrapErrorCause`.
- `src/_base/errors/codes.ts`: the `ErrorDomain` contract, the registry (`registerErrorDomain` / `errorInfo` / `isErrorCode`), and `CoreErrors` (`internal`, `not_implemented`). The `ErrorCode` union type is derived by `#/errors` from the aggregated domain definitions.
- `src/_base/errors/serialize.ts`: `ErrorPayload`, `isCodedError`, `toErrorPayload`, `fromErrorPayload`. Wire-facing names (`KimiErrorPayload`, `toKimiErrorPayload`) mirror the protocol and are kept as-is.
- `src/<domain>/errors.ts`: the domain's `XxxErrors` descriptor (codes + retryable list + per-code info overrides), self-registered on import.
- `src/errors.ts`: the **facade** — imports every domain's `errors.ts`, builds `ErrorCodes`, re-exports the primitives. Throw sites import from here.
## Conventions (hard rules)
- **Throw a coded error, not a bare string.**`throw new Error2(ErrorCodes.X, …)`. Bare `new Error` only for unreachable guards; `BugIndicatingError` for caller bugs; `NotImplementedError('feature')` for stubs.
- **Define codes in the owning domain**, in `<domain>/errors.ts` as an `XxxErrors` descriptor (`satisfies ErrorDomain` + `registerErrorDomain`), then wire it into the facade. Never add domain codes to `_base/errors`.
- **One `code` per failure mode.** Codes read `domain.reason`. The valid code strings are derived from the aggregated domain definitions (`ErrorCode` in `#/errors` is computed from the `ErrorCodes` aggregate): **add new codes to the owning domain's `errors.ts`** — registration throws on cross-domain collisions. Renaming/removing a code is a major.
- **Translate foreign errors at the boundary.** Provider/HTTP, fs, MCP errors are re-thrown as the owning domain's coded error. `_base/errors` never imports a business domain.
- **Translation is idempotent and cause-preserving.** Translators (`toHostFsError`, `toStorageIoError`) pass through an already-translated error and always keep the original as `cause`.
- **`details` is structured and JSON-serializable; `message` is a short human sentence.** Paths/errnos/scope/key go into `details`, not the message.
- **Cancellation passes through untranslated** (`UserCancellationError` from `_base/utils/abort`) — apply only at boundaries that can actually see cancellation; do not sprinkle the check everywhere.
- **Classify wrapped errors via `unwrapErrorCause`** — errno/status predicates test the unwrapped cause, not the coded wrapper.
- **Branch on `code`, never `instanceof`, across the wire.** In-process, `instanceof Error2` / `isCodedError` are fine.
- `os.process` — `HostProcessError`: `spawn_failed` / `kill_failed`, raw error as `cause`.
- `storage` — `StorageError` (`persistence/interface/storage.ts`): `not_found` / `decode_failed` / `corrupted` / `io_failed` (retryable) / `locked` (retryable). ENOENT keeps absence semantics, never an error. A locked query store throws `storage.locked`; consumers catch it explicitly and fall back — no silent no-op degradation.
- `wire` — `WireError` (`wire/errors.ts`): `DuplicateOpError`, `CycleError`, and `wire.unknown_record` (replay skips unknown records, reports via `onUnexpectedError`, returns `{ unknownRecords }`).
## Red lines (this topic)
- Throw a coded error with a `code`, not a bare string (except unreachable guards / `BugIndicatingError` / `NotImplementedError`).
- Codes live in the owning domain's `errors.ts` and self-register; new codes land in the owning domain first.
- Translate foreign errors at the owning domain's boundary, idempotently, with `cause` and structured `details`; `_base/errors` never imports a business domain.
- Branch on `code` across the wire, never `instanceof`.
Experimental feature-flag gating for agent-core-v2 — an App-scope `IFlagService` resolver plus a writable `IFlagRegistry` catalog that domains contribute their flags to, backed by the `[experimental]` config section.
Gate not-yet-public features behind `IFlagService.enabled(id)`, per the repository hard rule that unreleased behavior must be flag-gated. v1 was a process-global `FlagResolver` singleton over a central `FLAG_DEFINITIONS` array; v2 is a scoped DI service whose flag definitions are registered **decentrally** by each owning domain — there is no central catalog to edit.
- `src/app/flag/flag.ts` — `IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `EXPERIMENTAL_SECTION` (`experimental`) / `ExperimentalConfigSchema` (zod) + the module-level `registerConfigSection(EXPERIMENTAL_SECTION, …)` call that owns the section.
- `src/app/flag/flagService.ts` — `FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`); reads definitions from `IFlagRegistry` and overrides from `IConfigService`; self-registers at App scope.
- `src/app/flag/index.ts` — **removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './app/flag/flagService'`).
- `src/<domain>/flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/agent/toolSelect/flag.ts`). The directory already names the domain, so the file is just `flag.ts`.
- `IFlagRegistry` (DI token, App scope): `register(definition)`, `get(id)`, `list()` — writable catalog. `register` is the **runtime** path (tests, dynamic registration); `IFlagService.registry` exposes the same instance for hosts/UI to enumerate flags without resolving them.
- `registerFlagDefinition(definition)` — the **import-time** path. Domains call this from their `flag.ts` top level; contributions are queued and drained by `FlagRegistryService` when it is instantiated.
- `FlagService` / `FlagRegistryService`: exported for tests and hosts that construct them directly.
## Resolution precedence
Highest wins; env is read live on every call (nothing cached):
3. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
4. Registry `default`.
`explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered.
## Config integration
- The flag domain owns the `[experimental]` section: `src/app/flag/flag.ts` registers it at module load via `registerConfigSection(EXPERIMENTAL_SECTION, ExperimentalConfigSchema, { fromToml, toToml })` (import = register, drained by `ConfigRegistry` at construction); `FlagService` reads overrides from `IConfigService`.
- It subscribes `IConfigService.onDidChange` and refreshes overrides whenever the `experimental` domain changes, so config edits apply live.
- `ConfigRegistry.registerSection` throws if a domain is registered twice — `experimental` is owned exclusively by the flag domain.
- `setConfigOverrides(overrides)` is an imperative escape hatch for tests and hosts without an `IConfigService`; hosts on `IConfigService` should set the `[experimental]` section instead.
Config shape:
```toml
[experimental]
my_feature = false
```
Keys are intentionally loose (`z.record(z.string(), z.boolean())`), so obsolete flags stay inert config.
## Add a flag
Declare the definition in the owning domain's `flag.ts` and call `registerFlagDefinition` at the module top level. There is no central catalog to edit.
`src/<domain>/flag.ts`:
```ts
import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry';
Then ensure the package entry `src/index.ts` imports the flag leaf precisely so the top-level call runs at import time — there is no `src/<domain>/index.ts` barrel:
```ts
// src/index.ts
import './<domain>/flag';
```
`src/index.ts` imports every domain's leaf files precisely (one line per leaf), so the contribution runs during bootstrap, before any scope is created — and therefore before any consumer resolves `IFlagService`.
- `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`.
- `id` must not be `flag`. A duplicate `id` throws when `FlagRegistryService` drains the contributions.
- `FlagId` is `string`, not a literal union: with no central catalog there is nothing to derive it from, so `enabled()` has no compile-time typo-checking. Cover gated behavior with tests instead.
- `surface`: `core` | `tui` | `both` (documentation/grouping only; not used in resolution).
## Consume a flag
Inject `IFlagService` and gate on it. It is resolvable from any scope (App ancestor):
- It cannot live in `_base`: registering/reading the config section requires importing `config`, and `_base` is pure infrastructure that must not know any business domain.
- Scope: `IFlagRegistry` and `IFlagService` are both `App`. Env + config are process-global inputs, so there is no per-session/agent state. Flag definitions are contributed at **import time** (top-level `registerFlagDefinition` calls), so they are queued before any scope is created and drained when `FlagRegistryService` is first instantiated — before `IFlagService` is first resolved.
- Tests build `FlagService` + `FlagRegistryService` directly with a real `ConfigRegistry`/`ConfigService` and an injected env map, then `register` the flags they exercise.
## Red lines (this topic)
- Gate unreleased behavior behind a registered flag; no ad-hoc env toggles.
- Contribute each flag from the **owning domain's**`flag.ts` (`src/<domain>/flag.ts`) via a top-level `registerFlagDefinition` call; there is no central catalog to edit. The directory names the domain, so the file is just `flag.ts`.
- `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`; `id` must not be `flag`.
- `FlagId` is `string` (decentralized registration) — do not reintroduce a central `FLAG_DEFINITIONS` array or a derived literal union.
- `flag` lives at `App` scope — never in `_base`, never per-session.
Write the contract leaf, implementation leaf (with its registration), and the package-entry lines that load them. Each section below introduces one DI building block as you need it. Source lives in `src/_base/di/`.
2. **Impl leaf** — `src/<domain>/<domain>Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, activation, '<domain>')`. The fourth argument is activation; the fifth is the domain.
3. **Entry** — `src/index.ts`: load each leaf precisely — `export * from './<domain>/<domain>';` for the contract and `import './<domain>/<domain>Service';` for the impl (importing the impl runs the registration). **No `src/<domain>/index.ts` barrel.**
4. **Tests** — see test.md.
There is **no central wiring file**: bindings live in each domain's impl file and are collected through import side effects.
## §1 Interface + identity (a global service, no deps)
```ts
// greet/greet.ts
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface IGreeter {
readonly _serviceBrand: undefined; // type marker: tells DI "this is a service"
`createDecorator(name)` produces a `ServiceIdentifier` that is three things at once: a runtime key, a parameter decorator, and a compile-time carrier of the `IGreeter` type.
> **The identity name is globally unique.**`createDecorator` caches by `name`; two domains using the same string collide and share one identity.
```ts
// greet/greetService.ts
import { LifecycleScope } from '#/app/scopes';
import { registerScopedService, ScopeActivation } from '#/_base/di/scope';
import { IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined; // mirrors the interface marker
hello(): string { return 'hi'; }
}
registerScopedService(
LifecycleScope.App, // lifetime: process-wide
IGreeter, // identity
Greeter, // implementation
ScopeActivation.OnScopeCreated, // construct when the App scope is created
'greet', // domain name (for diagnostics)
);
```
The scope a class binds to is an **intrinsic property of the class**, decided at the registration point, not the call site.
The impl's top-level `registerScopedService` runs as soon as the module is imported. There is no `greet/index.ts` barrel — instead, add the leafs to the package entry `src/index.ts`, one line per leaf:
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService'; // this import runs registerScopedService
```
Anyone can now `accessor.get(IGreeter)` the single global instance.
## §2 Constructor injection (your service uses others)
```ts
export class SessionMetadata extends Disposable implements ISessionMetadata {
`@ISessionContext` records "parameter 0 needs `ISessionContext`" on the class metadata; the container fills it when constructing.
Three inviolable constraints:
1. **Do not `new` a class with `@IService` deps** — `new` bypasses registration, scope, and the singleton cache. Inject with `@IX` or `accessor.get(IX)`.
2. **`@IX` decorates constructor parameters only.** Decorating a field/method throws at runtime.
3. **Parameter order depends on how the object is built** — for `createInstance` non-singletons, static params come first (see §7); for scoped services, `@IX` params are conventionally first and any static params need defaults. See service-authoring.md §constructor-conventions.
Consumers resolve by interface and never import the impl class:
```ts
const meta = accessor.get(ISessionMetadata); // type is ISessionMetadata
```
> If you need "a config" rather than "a service", model it as a service (e.g. `IConfigService`) and inject it. If you need a per-turn, parameterized, non-singleton object, see §7.
## §3 Scoped registration (not global)
Swap the `scope` argument to bind to a different tier. Use `ScopeActivation.OnDemand` when the service should be constructed only on its first `get()`:
```ts
registerScopedService(
LifecycleScope.Session,
ISessionMetadata,
SessionMetadata,
ScopeActivation.OnDemand,
'sessionMetadata',
);
```
Remember the visibility rule from orient.md: a service may inject services from its own scope or any ancestor; never from a descendant.
## §4 Releasing resources (`Disposable`)
For a service that subscribes to events, starts timers, or holds handles:
```ts
import { Disposable } from '#/_base/di/lifecycle';
export class WSBroadcastService extends Disposable implements IWSBroadcastService {
- Extend `Disposable`, collect any `IDisposable` with `this._register(d)` (event subscriptions, `toDisposable(fn)`, etc.).
- The container calls `dispose()` automatically when the service is torn down; child resources release in turn.
- Disposal order is deterministic (orient.md): child scopes first; within a scope the Ledger (`src/_base/lifecycle/`) tears entries down in strict reverse registration order, serially — `Disposable` / `DisposableStore` delegate to it.
- Extend `Service` (from `#/_base/di/service`) instead when the unit needs capability calls on `this` (`provide` / `effect` / `on` / `get` / `ref`) — e.g. contributing a record to a `collection` token. `Service` extends `Disposable` (so `_register` is unchanged) and adds the two-phase construction protocol: `provide` / `on` / `effect` calls inside the constructor are buffered and flushed by the kernel after `Reflect.construct`; `get` / `ref` throw inside the constructor — dependencies stay constructor parameters. A manually `new`ed `Service` has no capabilities: every capability call throws.
## §5 Scope activation
`ScopeActivation` is the only construction-timing choice for scoped services:
```ts
export enum ScopeActivation {
OnScopeCreated = 0,
OnDemand = 1,
}
```
```ts
// Default: construct the real instance while the App scope is created.
registerScopedService(
LifecycleScope.App,
ILogService,
LogService,
ScopeActivation.OnScopeCreated,
'log',
);
// Construct the real instance on the first get(IScopeRegistry).
registerScopedService(
LifecycleScope.App,
IScopeRegistry,
ScopeRegistry,
ScopeActivation.OnDemand,
'gateway',
);
```
`ScopeActivation.OnScopeCreated` is the default fourth argument. Scope creation activates every registration using this mode, after constructing its dependencies. An eager constructor failure no longer fails scope creation: the unit lands in sticky `Failed` — scope creation succeeds, resolving the unit rethrows its error, and an explicit `update()` reloads it (see the bootstrap note below). Use it for ordinary services and for constructor side effects that must exist when the scope becomes ready.
`ScopeActivation.OnDemand` stores the descriptor without constructing the service. The first `get()` constructs and caches the real instance directly; later `get()` calls return that same instance. Use it only when construction should wait until the service is actually requested.
Both modes use the same dependency graph and reject cycles with `CyclicDependencyError`.
The complete registration signature is `registerScopedService(scope, id, ctor, activation = ScopeActivation.OnScopeCreated, domain?)`: activation is the fourth argument and domain is the fifth.
**Bootstrap shares the dynamic provide path.** Scope creation (`Scope.createApp` / `Scope.createChild` / `createScopedChildHandle` in `src/_base/di/scope.ts`) submits the scope kind's entire `registerScopedService` batch as ONE cascade transaction via `provideAll`: every token registers before the activation wave runs, so **registration order never matters**, and untracked transitive `createInstance` resolutions succeed inside the batch. A seed occupying a token (the `extra` tuple in `ScopeOptions`) overrides the static registration for that token. `activateScopeServices` is gone — there is no separate static activation path.
## §6 Using a service inside a plain function (`invokeFunction`)
When you do not want a new class and just need a service once, or when you expose a `ServicesAccessor` to the outside:
```ts
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>): T => instantiation.invokeFunction((a) => a.get(id)),
};
```
`invokeFunction(fn)` hands `fn` a `ServicesAccessor` valid **only during that call**.
> **The accessor is valid only during the invocation.** Calling `accessor.get()` after `invokeFunction` returns throws `"service accessor is only valid during the invocation"`. Do not stash it for async use — inject the service in the constructor (§2) if you need it long-term.
## §7 Creating a non-singleton object with deps (`createInstance`)
For a per-turn executor that also has `@IService` deps:
```ts
class TurnRunner {
constructor(
private readonly input: string, // static param: passed by caller
private readonly turn: number, // static param: passed by caller
@ILogService private readonly log: ILogService, // service param: injected by container
Static params come first (you pass them), service params follow (the container fills them), then `Reflect.construct` builds the instance. This object is **not** placed in any scope's singleton cache — every call is a fresh instance — and it is not tracked as a cascade unit either: `createInstance` products are cascade-exempt leaves that no cascade tears down or rebuilds; their owner disposes them.
> This is why service params must follow static params **for `createInstance`**: the container sorts by the parameter positions recorded via `@IX`. `_serviceBrand` lets the compiler tell the two kinds apart. Scoped services built by `registerScopedService` follow a different convention (`@IX` params first, optional static params after) — see service-authoring.md §constructor-conventions.
## §8 Spawning a child scope / child container
For a service that "starts a new session / agent" and needs a child scope, inject `IInstantiationService` itself (every container binds itself as `IInstantiationService`):
```ts
export class ScopeRegistry implements IScopeRegistry {
- `getScopedServiceDescriptors(scope)` returns every descriptor registered at that tier; load them into a `ServiceCollection`.
- `instantiation.createChild(collection)` builds a child container whose parent pointer is the current container — so the child resolves upward to `App` services (the visibility rule).
- Expose the child to the outside by wrapping it in a `ServicesAccessor` via `invokeFunction` (§6).
> Higher-level code usually calls `Scope.createChild(kind, id)` (it does the "filter descriptors + build child" for you, then submits the whole batch through `provideAll` as one cascade transaction — see §5). Drop to the manual `ServiceCollection` form only when you need explicit control; to change bindings on an already-created container, prefer `provide` / `unprovide` / `update` over rebuilding a collection. Before the static batch lands, the scope-creation point runs the kernel's `ScopeUnits` fold (`_base/di/scopeUnits.ts` — materializes the recipes contributed to `ScopeUnits(kind)` as per-scope units) and then the `ScopeOptions.assemble` hook — the session domain uses the hook to construct its seed-adapter units (`session/sessionSeed/sessionSeedAdapters.ts`) so their provided tokens exist before the session services activate.
## §9 Cyclic dependencies (forbidden — refactor)
Business rule: **no cyclic dependencies.** The container rejects them; the correct response is to refactor, not to make it run.
### The container rejects synchronous cycles
If A needs B while being created and B needs A while being created, the container throws `CyclicDependencyError` with a `path` like `['A', 'B', 'A']`. Self-cycles (A depends on itself) are also rejected. This is a protection mechanism telling you the two services' responsibilities are mis-drawn.
### Why cycles are disallowed
- Scope layering makes normal dependencies a DAG (Agent → Session → Workspace → App, resolving upward); a cycle is almost always a design smell.
- "Making the cycle happen to work" turns construction order into an implicit contract — hard to debug.
v2's stance: **the dependency graph must be acyclic.**
### How to refactor (in priority order)
1. **Extract a third service C.** Move the part A and B both need into C; let A and B both depend on C instead of each other. The most common fix.
2. **Decouple with an event.** If A only needs to know about a change in B, have B emit via `IEventService` and A subscribe, rather than A holding a reference to B.
3. **Re-partition scope.** One of them may belong at a different tier — moving it makes the cycle disappear.
### Activation does not break cycles
Both `ScopeActivation.OnScopeCreated` and `ScopeActivation.OnDemand` construct through the same synchronous dependency graph. Changing activation cannot make a cycle valid. On `CyclicDependencyError`, refactor per the above.
| `Scope` / `LifecycleScope` | §3, §8 | the lifetime tree |
| `ScopeActivation` | §3, §5 | choose scope-created or first-`get()` construction |
| `Service` (`_base/di/service`) | §4 | unit base class — `this.provide/effect/on/get/ref` capabilities, two-phase construction |
| `collection<T>(name)` / `CollectionView<T>` (`_base/di/collection`) | §4 | contribution-point token + the fold's live view (provider death withdraws the record) |
| `SyncDescriptor` | (tests / low-level) | package a constructor + static args into a pending descriptor |
> Legacy export (not used in v2, just recognize it): `refineServiceDecorator` is a VS Code leftover DI helper. v2 src/test has zero references; always use `registerScopedService`.
## Red lines (this stage)
- No `new` on a class whose constructor carries `@IService` deps — inject or `accessor.get(IX)`.
- `@IX` decorates constructor params only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services — see service-authoring.md).
- Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique.
- `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use.
- No cyclic dependencies — refactor (extract / event / re-scope); activation does not change cycle detection.
Understand the DI × Scope black box and the file conventions before touching business code.
## The DI black box
When writing business code you declare three things; the container handles the rest (when to construct, whether it is the same instance, ordering, disposal):
- **Who am I** — an identity that is both a runtime key and a compile-time type.
- **Whom do I need** — the dependencies that provide my capabilities.
- **How long do I live** — which lifetime tier I belong to.
Classes talk only to interfaces and never care how an implementation is constructed.
## The four `LifecycleScope` tiers
Lifetimes form a tree, from longest to shortest:
```text
App process-wide, single global instance
└── Workspace one workspace handler (a materialized workspace root)
└── Session one session
└── Agent one agent
```
```ts
// src/app/scopes.ts — the business layer declares the tiers and their order;
// the DI kernel only knows opaque string kinds plus the declared topology.
export enum LifecycleScope {
App = 'app',
Workspace = 'workspace',
Session = 'session',
Agent = 'agent',
}
```
- Later in the topology = shorter life = closer to a leaf.
- "Singleton" means **one per scope**: `ILogService` is global once; each `Session` scope has its own `ISessionMetadata`.
- `kind` must advance along the declared topology in the parent→child direction.
### Visibility rule
A child scope sees its ancestors; a parent never sees its children. Resolution walks *up* the tree:
- ✅ An `Agent` service injects a `Session` or `App` service (found upward).
- ❌ An `App` service injects a `Session` service (the parent does not look down, and the child may not exist yet).
> **Short-lived may inject long-lived; never the reverse.** The tree structure enforces this — it is not a matter of discipline.
### Disposal order
Deterministic: **child scopes die first; within one scope, teardown runs in strict reverse registration order, one entry at a time.** The mechanism is the Ledger (`src/_base/lifecycle/`): ordered effect bookkeeping, dual-track (sync + async disposers), serial reverse-order teardown (never parallel), with the teardown reason (`'scope-close' | 'cascade' | 'unload'`) passed through to every disposer. `Disposable` / `DisposableStore` (`src/_base/di/lifecycle.ts`) delegate to it — "reverse construction order" is a Ledger property, not a container convention. Business code declares which tier it lives in and never disposes by hand.
## Dynamic DI: units and cascades
Registration is not the end of the story. Every unit a container tracks — static registrations and runtime `provide`s alike — lives in a small state machine owned by the scope's cascade engine (`src/_base/di/cascadeEngine.ts`, one per scope container, orchestrating tree-wide). Vocabulary you will meet in errors, tests, and the debug surface:
- **Unit states** — `Pending → Activating → Active`, plus `Unloading` during teardown and a sticky `Failed`. A construction failure parks the unit in `Failed` with no auto-retry: resolving it rethrows its error; an explicit `update()` reloads it.
- **Waiting area** — a unit whose declared dependencies are missing sits `Pending` and auto-activates when they arrive, including cross-scope wake-up when an ancestor gains the token. An `ondemand` unit counts as available: consumers pull it transitively at materialization.
- **Cascade transaction** — every `provide` / `unprovide` / `update` runs as one tree-wide transaction: contagion set from the persistent dependency graph (instance edges, child→parent across scopes) → abort hook → global reverse-topo teardown → apply the change → waiting-area recheck fixpoint → history ring. Static bootstrap shares this path: scope creation submits the kind's whole registration batch as one `provideAll`, so registration order never matters.
## Import boundaries
There is no domain-layer numbering — a domain may import any other domain, guided by the dependency-direction judgment in design.md. The only mechanically enforced import boundaries are (`lint:imports`, `scripts/check-import-boundaries.mjs`):
- v2 never imports v1 (`@moonshot-ai/agent-core` or any subpath).
- The kosong subtree (`src/kosong/{contract,protocol,provider,model}`) keeps its strict internal order (`contract ← protocol ← provider/model`), purity bans (no SDKs in `contract`/`protocol`), and the `provider/bases` registration boundary.
## Comment convention
`packages/agent-core-v2/AGENTS.md` bans comments entirely: no file headers, no section banners, no statement-level narration, no JSDoc (not even on exported symbols) — the code is the source of truth. The only exception is a load-bearing lint-suppression directive (`oxlint-disable` / `eslint-disable`) for a deliberate pattern; other tooling directives (`@ts-expect-error`, …) are banned: fix the underlying lint/type problem instead, and put negative type-safety cases in compiler-asserted fixtures. Scope is carried by the filename: `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md).
## Red lines (this stage)
- Import via the `#/...` alias (mapped to `src/`); never reach into another domain's internals by relative path.
- Short-lived may inject long-lived; never the reverse.
- No comments — not file headers, not beside statements, no JSDoc anywhere; `oxlint-disable` / `eslint-disable` are the only exception.
The target design for the agent-core permission system. Read this when touching `permission`, `permissionMode`, `permissionRules`, or when adding a new permission dimension.
> **The permission system should be a composable, registrable chain of responsibility (a microkernel).** The kernel only runs the chain in order, first hit wins; concrete permission dimensions (policies) are contributed by their owning Domain Services through a registry; tools only declare standardized resource access (`accesses`) in `resolveExecution`, and generic dimensions consume that metadata.
>
> **The chain adjudicates risk only.** A policy node answers "how dangerous is this call, and may the user override that judgment?" — its `ask`/`deny` outcomes are always user-overridable. **Harness constraints are not permissions**: a mechanism that limits the agent for its own correctness (plan-mode write guard, AgentSwarm batch exclusivity, btw side-question fork, goal budget rejection) produces a hard deny with no ask channel and no per-call user exemption. Those live in their owning domains as `onBeforeExecuteTool` veto listeners that call `event.veto(...)` (precedent: `goalService.ts`'s budget/stale rejection). Product reviews (plan review, goal-start review) are likewise not permissions: the owning domain intercepts its tool with a cold `event.waitUntil(factory)` and drives the shared `IAgentToolApprovalService` round-trip itself, so the review only starts once no other listener vetoed the call.
>
> **Do not introduce Casbin** — the hard part here is *decision behavior* (continuations, side effects, RPC, state machines), not "match + scalar decision".
## 1. Problem definition
The permission system answers one question: **for each tool call, in the current agent and current mode — allow / deny / ask the user?** Three traits shape the architecture:
1. **Decisions carry behavior.** Returning `ask` is not an enum value — it is a workflow with an RPC round-trip, hooks, telemetry, state writes, and a continuation; returning `deny` may be the result of running an external hook.
2. **Heterogeneous policies.** Some check a tool-name set, some count same-batch `AgentSwarm` calls, some run a hook, some inspect the plan state machine — no uniform `(sub, obj, act)` shape.
3. **Multi-agent × multi-mode × external extension.** Different agents / modes need different permissions, and outsiders (org admins, plugins) must contribute rules or behavior in a decoupled way.
## 2. Current state (v1) at a glance
Code lives in `packages/agent-core/src/agent/permission/`.
- **Architecture: ordered chain of responsibility, first hit wins.**`PermissionManager` holds `PermissionPolicy[]`; evaluation iterates in order, the first non-`undefined` result wins.
- **`PermissionPolicyResult` is a behavior bundle, not a scalar:** `approve` (with `executionMetadata`), `deny` (with `message`), or `ask` (with `resolveApproval` / `resolveError` continuations).
- **11 dimensions, 19 policies**, hardcoded in `policies/index.ts#createPermissionDecisionPolicies()`. Order is a high-to-low safety cascade: external force → structural deny → state-machine deny → static deny → mode allow → session-memory allow → static ask → static allow → flow allow → sensitive-path ask → default allow → fallback ask.
- **Resource-access declaration:** tools declare accessed resources in `resolveExecution(input)` via `accesses` (`ToolAccesses`, currently `file` and `all`); generic dimensions read `context.execution.accesses`.
### v1 pain points the target design fixes
1. The chain is hardcoded — outsiders cannot contribute.
2. `mode` is an `if` inside each policy (`YoloModeApprove` / `AutoModeApprove` self-guard).
3. No per-agent chain entry point (only scattered `agent.type === 'sub'` checks).
4. No external extension point beyond the single `PreToolUse` hook slot.
## 3. Why not Casbin
- **`policy_effect` is unusable** — composition here is a fixed, intentionally hardcoded safety cascade; the real complexity lives in each policy's `evaluate` behavior, which a Casbin expression cannot absorb. Externally tunable safety knobs are already exposed via `mode` + allow/deny/ask rules.
- **Flexible priority is unusable** — there is no plugin injection point, no multi-subject/RBAC, and a fixed subject (agent/user), so priority collisions do not arise. Casbin's `(sub, obj, act)`, `g()`, and domains would idle.
- **Fundamental mismatch: decisions are not scalars.**`enforce()` maps a request to an effect; agent-core decisions are behavior bundles (continuations, side effects, synthesized results). Even if Casbin computed `ask`, the surrounding behavior would still need to be rewritten — Casbin would degrade to an enum generator.
- **When Casbin becomes worth it:** when the hard part is matching semantics itself — role inheritance, domain isolation, ABAC expressions, policies loaded from a DB. Not before.
## 4. Design-pattern placement
Permission orchestration is a layered combination, not a single pattern:
| Layer | Pattern | Role |
|---|---|---|
| Runtime decision | **Chain of Responsibility** | multiple candidates in order; first hit wins, rest short-circuit |
| Single handler | **Strategy** | each policy is an interchangeable "permission adjudication" algorithm |
| Landing support | **Registry + Factory** | collect plugins; assemble the chain per `(agent, mode)` on demand |
Casbin = single Strategy + data-driven. This design = multiple Strategies + chain-of-responsibility composition. Behavior-heavy systems must choose the latter — behavior cannot be flattened into data rows.
## 5. Target design
### 5.1 Core principles
1. **The chain encodes "permission dimensions", not "tools".** Adding a tool does not lengthen the chain; only adding a dimension adds a node.
2. **Two contribution paths:** high-frequency trivial specifics go through the **data path** (rules); low-frequency new dimensions with behavior go through the **code path** (policies).
3. **Guard/review off-chain, risk on-chain:** harness constraints and product reviews ship with their owning domain as `onBeforeExecuteTool` veto listeners (§5.4); risk dimensions contributed by a domain self-register as chain policies in DI, mirroring v2's "domain self-registers tools".
- `modes` / `agentTypes` are **declarations** — they lift the `if (mode !== 'yolo') return` out of `YoloModeApprove` into metadata.
- `factory`, not `instance`: a node may depend on agent-scoped services (mode, rules) and must be instantiated in the Agent scope — symmetric to `IToolDefinitionRegistry` (App) storing factories and `IToolService` (Agent) instantiating tools.
- **Different `(agent, mode)` produce differently-shaped chains** — under yolo the ask/fallback phases are physically filtered out.
### 5.3 Two contribution paths
| What is being added | Path | Chain length |
|---|---|---|
| New tool, new org rule, new user preference ("deny `Bash(curl *)`") | **Data path**: add a `PermissionRule` to an existing node | unchanged |
| New cross-cutting behavior (custom approval UI, audit log, new mode) | **Code path**: register a new policy node | +1 |
Most growth goes through the data path — node count is bounded by "kinds of behavior"; rule count grows with specifics (rule matching is a cheap Set/glob).
### 5.4 Domain dimensions: guard/review via the executor veto event, policy registration for risk
**Harness constraints and product reviews no longer live on the chain.** A domain that owns one registers an `onBeforeExecuteTool` veto listener and adjudicates through the event:
- The veto event carries no id and no ordering contract. Listeners answer with `event.veto(result)` (first one wins, ends adjudication), `event.allow()` (final pass, ends everything including the permission gate's own listener), `event.pass(metadata)` (pass with an `executionMetadata` trace, ends nothing), or `event.waitUntil(factory)` (defer to a cold factory).
- **Guard** (hard deny): call `event.veto(denyToolExecution(toolApproval.formatDenyMessage(...)))`. An immediate veto suppresses every pending `waitUntil` factory, so a deny can never be preceded by someone else's approval prompt.
- **Review** (product approval): intercept the tool with `event.waitUntil(() => ...requestToolApproval(event, ask, origin))`. The factory is cold — the executor only invokes it after every listener ran without a veto or an allow, so the review's Interaction starts only once the call is otherwise clear to proceed; abstain (no statement) for every case you do not review so user rules still apply.
- **Plain allow**: do NOT `allow()` casually — prefer putting the tool in `default-tool-approve`'s whitelist so user deny/ask rules keep their precedence; reserve `allow()` for cases like the plan-file write guard that must bypass even the permission chain.
**Risk dimensions contributed by a domain still go through the chain** (the registry path below): a domain whose state changes the *risk* verdict registers its policy via `IPermissionPolicyRegistry`, mirroring v2's "domain self-registers tools". A complex domain may register a single **composite** node externally and run a small internal chain, hiding its internal order from the global chain.
### 5.5 Tools declare resources at runtime (`resolveExecution` / `accesses`)
In `resolveExecution(input)`, before execution, declare accessed resources with the `ToolAccesses.*` builders:
- **Enumerable resources** (write/read/edit/grep/glob) → use `accesses`; generic file dimensions cover them automatically.
- **Non-enumerable resources** (bash running arbitrary commands) → do not declare `accesses`; use the `matchesRule` DSL (e.g. `Bash(rm *)` globs by command string).
**kaos's role:** kaos is the execution-environment abstraction (fs/process/pathClass) used by the file dimension for path normalization and judgment — it is **not** the permission-dimension abstraction itself. Permission semantics live one layer above kaos, at "file access".
**v2 evolution:** extend the `ToolResourceAccess` union so non-file resources can be declared structurally:
| approval round-trip | `toolApproval` domain — shared by gate asks and domain reviews | infrastructure |
Pattern: **harness constraints and reviews ship with their owning domain as `onBeforeExecuteTool` veto listeners; risk dimensions ship as chain policies (self-registered once the registry lands); generic dimensions register centrally and apply across tools via the declared `accesses`.**
## 6. Evolution path
Incremental, not big-bang:
1. ~~**Sink domain dimensions.**~~**Done** — plan guard/review, goal-start review, swarm batch exclusivity, and btw deny-all moved out of the chain into their owning domains as `onBeforeExecuteTool` veto listeners (immediate `veto` / `allow` / `pass` statements plus cold `waitUntil` factories for approval round-trips); the shared approval round-trip was extracted to `IAgentToolApprovalService`; `registerPolicy` was removed (btw was its only production user). The chain now holds 12 risk-adjudication nodes only.
2. **Level × routing split.** Separate "risk level" (read-only / read-write / yolo posture — what `yolo-mode-approve` really is) from "interaction routing" (what `auto-mode-approve` / `auto-mode-ask-user-question-deny` really are: route permission asks and reviews without the user). The routing layer lands on the `session/approval` broker; the three remaining mode policies leave the chain here.
3. **Registry + Composer.** Replace the hardcoded `new`s in `PermissionPolicyService` with reads from `IPermissionPolicyRegistry`; lift mode guards into `modes` metadata. Chain shape becomes selectable per `(agent, mode)` and externally extensible.
4. **(On demand) extend resource types.** When non-file resources (network/DB/shell) need structural dimensions, extend the `ToolResourceAccess` union.
5. **(On demand) swap the matching kernel for Casbin.** Only when external rules genuinely need RBAC/ABAC semantics, swap the data-path rule-matching kernel for Casbin. Not before.
## Red lines (this topic)
- Do not introduce Casbin — decisions are behavior bundles, not scalar effects.
- The chain adjudicates risk only. A node whose deny/ask the user cannot per-call exempt is a harness constraint: implement it as an `onBeforeExecuteTool` veto listener in the owning domain (`event.veto(...)` / `event.allow()`), never as a chain policy.
- Product reviews (plan/goal) are not permissions either: the owning domain intercepts its tool with a cold `event.waitUntil(factory)` and drives `IAgentToolApprovalService` itself; the gate only handles chain asks.
- The chain encodes dimensions, not tools: a new tool must not lengthen the chain.
- New specifics go through the data path (rules); only new risk behavior goes through the code path (a policy node).
- Tools only declare `accesses`; generic dimensions consume them. kaos is the execution environment, not the permission abstraction.
- Use `factory` (Agent-scope instantiation), not `instance`, for registered policies.
How business code persists data in `agent-core-v2`: the three-layer model (`Store → Storage → backend`), the naming rules for each layer, and how to decide which layer a domain should depend on. Read this before adding any persistence to a domain.
A domain `I{Domain}EntityService` is a business facade over these layers, not a replacement for them. Before naming or bundling EntityServices by `session` / `agent` / `turn`, read [domain-boundaries.md](domain-boundaries.md).
## The three-layer model
Persistence is split into three layers, each hiding one kind of change:
```text
Business Service
│ inject
▼
┌────────────────────────────────────────┐
│ Store (semantic layer) │ ← access-pattern facade
2. **`ISessionContext.scope(subKey?)`** — persistence scope rooted at the current session; `scope('agents/main')` etc.
3. **`IAgentScopeContext.scope(subKey?)`** — persistence scope rooted at the current agent; `scope('cron')`, `scope('blobs')` etc.
The bootstrap layer decides how each semantic scope maps to concrete addressing. In the file deployment, `FileBootstrapService` reads a `ResolvedEnvironment` (the paths bag) and returns homeDir-relative scopes; a server deployment could bind a different `IBootstrapService` implementation that maps `'sessions'` to a DB table without any business change.
```ts
// ❌ Wrong — path arithmetic on homeDir/sessionDir leaks the file layout
// ✅ Right — the agent already knows its own scope root
const scope = agentCtx.scope('cron');
```
Absolute paths (`sessionDir`, `agentHomedir`) are still available on `IBootstrapService` for the very small number of legacy APIs that expose on-disk paths (session log rotation, background task tail file). Prefer scope strings; ask before adding a new absolute-path caller.
**Do not name a generic Store after a business concept.** `IRecordStore` / `IConfigStore` make a reusable access pattern look like a private store for one feature. Any domain that needs an append-log uses `IAppendLogStore`; any domain that needs an atomic document uses `IAtomicDocumentStore`.
**Exception — business-specific Stores are named after the business.** When a Store captures one domain's unique query semantics (not a generic access pattern), name it after the domain:
```text
ISessionIndex query / enumerate sessions by workspace ← business-specific
```
Test: is the Store's semantics a *generic access pattern* (append-log / atomic-doc / blob) or *one domain's unique query*? Generic → name by pattern; unique → name by domain.
## Storage — a filesystem-specific byte layer
The byte layer is a single `IFileSystemStorageService` interface (read / readStream / write / append / list / delete / watch / flush / close). As the name says, it is **filesystem-specific**: it exposes the two irreducible durable primitives a local filesystem implements optimally — atomic whole-value replacement (`write`, via tmp + rename) and ordered durable extension (`append`, via `open('a')`). The node-fs Store backends (`AppendLogStore`, `JsonAtomicDocumentStore`, `BlobStoreService`) are built on it.
Two backends implement it today, both bound at the composition root:
```ts
// Production — local filesystem rooted at homeDir
collection.set(IFileSystemStorageService, new FileStorageService(homeDir));
// Tests — in-memory backend seeded by the test harness
collection.set(IFileSystemStorageService, new InMemoryStorageService());
```
**Non-filesystem backends (Postgres, S3, Redis) do not implement this interface.** Atomic-rename and byte-append have no native equivalent in those stores, so they implement the **Store** interfaces directly via their own clients instead:
```ts
// Server profile — append-logs on Postgres, atomic documents on Redis.
// Each Store is backed by a native client; IFileSystemStorageService is not involved.
collection.set(IAppendLogStore, new PostgresAppendLogStore(db, 'records'));
collection.set(IAtomicDocumentStore, new RedisDocumentStore(redis, 'config'));
```
Use the `scope` parameter to express **business namespace** within a backend. Do not overload `scope` to route backends — bind a different Store implementation at the composition root instead.
## Store `acquire(scope, key)` — flush-on-dispose handle
Stores that buffer writes expose an `acquire(scope, key)` handle so a business can flush them on disposal:
```ts
export interface IAppendLogStore {
// …
/**
* Acquire a disposable handle for `(scope, key)`. Register it with your
* `Disposable` (via `this._register(...)`); when you are disposed, pending
* appends for that log are flushed. The shared store itself is not disposed.
*/
acquire(scope: string, key: string): IDisposable;
}
```
`IAppendLogStore.acquire` flushes the log's pending appends on dispose — it exists because `append` is fire-and-forget. `IAtomicDocumentStore.acquire` is a no-op today (atomic documents are durable on write) and exists for interface symmetry. Businesses that do not need flush-on-dispose simply do not call `acquire`.
## When the byte layer does not apply
`IFileSystemStorageService` covers only the local-filesystem byte primitives. It is not a universal storage abstraction:
- **Non-filesystem backends** (Postgres / S3 / Redis) implement the **Store** interfaces directly via native clients — they never implement `IFileSystemStorageService`.
- **Blobs** are a Store-level interface (`IBlobStore`) with their own backends; the node-fs `BlobStoreService` sits on `IFileSystemStorageService`, but an `S3BlobStore` would not.
- **A backend has a fast primitive the Store interface cannot express** (e.g. Postgres `COPY`) → as an exception, extend that backend's Store implementation directly. This is an exception, not the default.
## Platform primitives are deployment-coupled, not core abstractions
`hostFs` (local filesystem) is a **platform primitive** used only by local backends (`FileStorageService`, `LocalFileSystemBackend`, `LocalSkillCatalog`, `HostFolderBrowser`). It is **not** a core abstraction and must not appear in business-domain dependency graphs. A server deployment swaps those backends for DB / S3 implementations and never registers `hostFs`.
## Red lines (this topic)
- Business code never contains "how to persist" details (serialization / paths / SQL / append offsets) — if it does, drop a layer.
- Business code never assembles scope strings from paths (`pathe.join / relative / basename` on `homeDir` / `sessionDir` / …). Use `IBootstrapService.scope(name)` for well-known scopes, `ISessionContext.scope(subKey?)` for session-rooted scopes, and `IAgentScopeContext.scope(subKey?)` for agent-rooted scopes.
- Name generic Stores by access pattern (`IAppendLogStore` / `IAtomicDocumentStore` / `IBlobStore`), never by business concept (`IRecordStore` / `IConfigStore`).
- Business-specific Stores (unique query semantics) are named after the domain (`ISessionIndex`).
- `IFileSystemStorageService` is the filesystem byte-layer interface; non-filesystem backends implement the **Store** interfaces directly. Route backends by binding a different Store implementation at the composition root, not by overloading `scope`.
- `hostFs` is a local-only platform primitive; business domains must not import `node:fs` or `hostFs` directly.
- Only the file-backed bootstrap (`FileBootstrapService`) and file backends import `pathe`; business domains do not.
- Do not create a pass-through `Store` that only forwards `read/write` — a Store must hide a real access-pattern concern, or it is noise; use `IFileSystemStorageService` directly instead.
# Subskill — Server align (expose `agent-core-v2` over `server-v2`)
Wire a v2 domain into `packages/kap-server`, and — when the endpoint is part of the established `/api/v1` wire contract — keep the wire shape **byte-for-byte compatible** with what released v1 clients expect. This is the server-side counterpart of [align.md](align.md): `align.md` ports v1 *business logic* into v2; this file exposes the v2 result over HTTP / WS, reusing the v1 wire contract where it already exists.
Use this when the task is "expose the new v2 Service on the server", "add a `/sessions/:sid/...` route to the `/api/v1` surface", or "keep server-v2 speaking the same `/api/v1` contract released clients rely on".
## The one-paragraph mental model
`server-v2` serves **two HTTP surfaces** off the same `agent-core-v2` scope tree:
- **`/api/v2/:sa`** — the native v2 RPC surface, driven by the `actionMap` allowlist (`packages/kap-server/src/transport/actionMap.ts`). One `resource:action` segment maps to one `Service.method`. New v2-native capabilities land here. See [edge-exposure.md](edge-exposure.md).
- **`/api/v1/...`** — the v1-compatible surface, hand-written routes in `packages/kap-server/src/routes/*.ts` that **implement the established v1 wire contract path-for-path and schema-for-schema**, mounted by `registerApiV1Routes.ts`. This surface IS the v1 contract now (the legacy v1 server is gone); it exists so existing v1 clients keep working against server-v2 unchanged.
The two surfaces can point at **different Services** for the same feature. v2's native `IAgentPromptService` serves `/api/v2`; a v1-shaped `IAgentPromptService` serves `/api/v1`. Keeping them separate is what lets v2's domain design stay clean while the wire stays compatible.
## Decision: which surface?
```text
Is the endpoint part of the established /api/v1 wire contract (protocol schema
│ Reuse the protocol schema; add a LegacyService if v2 semantics diverge.
└─ NO → /api/v2 native action (edge-exposure.md).
Add to actionMap, wrapping in a facade if the method fails §2 there.
```
A feature often needs **both**: the v1 mirror so old clients keep working, and the v2 action so new clients get the cleaner shape. Do them as two routes / two action-map entries over the same scope tree.
## The server-align workflow
```text
Pick surface → Read the v1 route (if any) → Reuse / add the protocol schema
→ Choose native Service vs LegacyService → Wire the route / actionMap entry
→ Map errors → Test against the v1 wire shape → Verify
```
### 1. Pick the surface
Apply the decision above. For a v1-matched endpoint, the **spec** is the protocol schema plus the existing mirror routes:
- `packages/kap-server/src/protocol/rest-<resource>.ts` — the wire schema you must match.
- `packages/kap-server/src/routes/<resource>.ts` — the file you are writing (create it if missing); sibling route files show the conventions.
The protocol schema is the source of truth. Do not re-derive the wire shape from memory or from the v2 domain model.
### 2. Reuse (or add) the protocol schema
The wire schema lives in **`packages/kap-server/src/protocol`** under `rest-<resource>.ts` (e.g. `promptSubmissionSchema`, `promptListResponseSchema`, `configResponseSchema`) — or in the owning `agent-core-v2` domain contract when the engine's service speaks the shape. Every `/api/v1` route in `packages/kap-server` imports from it — that single import is what guarantees the server speaks the same shape released clients expect.
Actions:
- **Schema already in protocol** → import it in the server-v2 route and use it in `defineRoute` (`body`, `success.data`, error `dataSchema` / `detailsSchema`). Do **not** re-declare the schema inline in server-v2.
- **Schema missing** → add it to `packages/kap-server/src/protocol/rest-<resource>.ts` first (or to the owning v2 domain contract if its service speaks the shape), then consume it from the route. The shared schema is the source of truth; server-v2 never re-declares a v1 wire schema inline.
- **Schema exists but only v1 uses it** → keep it in `packages/kap-server/src/protocol` and import it into server-v2; do not fork a copy.
#### Schema-fidelity rule (the hard rule)
For a `/api/v1` endpoint, the request and response schemas **must be the established protocol schema** (or a strict superset):
- ✅ **Adding** an optional field is allowed (`field: z.string().optional()`). Old clients ignore it; new clients may send it.
- ❌ **Renaming** a field, **changing** its type, **tightening** its validation, or **changing its meaning** is a wire break — do not do it in a mirror route. If the v2 domain genuinely needs a different shape, that shape belongs on `/api/v2`, not on the `/api/v1` mirror.
- ❌ Re-declaring the schema inline in server-v2 (even if it "looks identical") is forbidden — it drifts. One schema, one home: the owning `agent-core-v2` domain contract or `packages/kap-server/src/protocol`.
Self-check: "would a released v1 client get a byte-identical envelope from `packages/kap-server` for this request?" If you cannot answer yes from the shared schema, the route is wrong.
### 3. Choose native Service vs LegacyService
Resolve the v2 Service that will back the route. Two cases:
**Case A — the v2 native Service already matches the v1 contract.** Use it directly. Most data/command Services (`IConfigService`, `IWorkspaceService`, `IApprovalService`, `IQuestionService`, `IFileStore`, …) land here: the route is a thin adapter that resolves the scope, calls the method, and wraps the result. Examples: `routes/config.ts`, `routes/messages.ts`, `routes/questions.ts`, `routes/files.ts`.
**Case B — the v1 contract needs behavior that would distort the v2 domain.** Introduce a **`*LegacyService`** — an edge adapter that implements the v1 contract **on top of** the v2 native Service, leaving the native Service untouched. The v2 native Service keeps serving `/api/v2`; the LegacyService serves `/api/v1`.
Reach for a LegacyService when **any** hold:
- The v1 endpoint carries state the v2 domain deliberately dropped (e.g. a FIFO queue, a `prompt_id`, idempotent `abort`/`steer`, auto-start-next).
- The v1 method returns a handle/stream that v2 wraps differently, and the v1 clients expect the old envelope shape.
- Matching v1 would force a `Map<sessionId, …>`-at-`App` anti-pattern or a scope/domain-direction violation into the native Service (see [align.md](align.md) red lines).
- The native Service's error set / return type would have to grow v1-only branches.
Do **not** put v1 quirks into the native v2 Service "to keep the route simple". That is the conflict this rule exists to prevent: the native Service serves the v2 architecture; the LegacyService serves the wire contract.
#### LegacyService recipe
A LegacyService is a normal v2 Service (service-authoring.md) with one extra convention: its contract is shaped by the **protocol** types, not by the v2 domain model.
// submit() builds v2-native input, calls the native Service, projects the result
// back into the protocol PromptSubmitResult.
registerScopedService(
LifecycleScope.Agent, // scope = the lifetime of the legacy state
IAgentPromptService,
AgentPromptLegacyService,
ScopeActivation.OnDemand,
'prompt',
);
```
Conventions:
- **Name** the domain `<domain>Legacy` and the interface with the scope prefix, `I<Scope><Domain>LegacyService` (e.g. `prompt` / `IAgentPromptService`), per service-authoring.md.
- **Role is carried by the name** — `<domain>Legacy` marks it as an `edge adapter`; the v1 contract it implements and the native v2 Service it leaves untouched stay evident from its delegation targets (see `prompt.ts`).
- **Scope** = the lifetime of the *legacy* state it holds (the `prompt` queue is per-agent → `LifecycleScope.Agent`). Apply [orient.md](orient.md) / [design.md](design.md) normally — a LegacyService is not exempt from scope rules.
- **Delegate, do not duplicate** business logic. The LegacyService translates the v1 contract into native-Service calls and translates results back; the real work stays in the native Service.
- **Contract types come from the v1 wire schema homes** (the owning v2 domain contract or `kap-server/src/protocol`), so the interface cannot drift from the wire shape.
### 4. Wire the route / actionMap entry
**For `/api/v1` (mirror):** add a route file under `packages/kap-server/src/routes/<resource>.ts` using `defineRoute`, then register it in `registerApiV1Routes.ts`. Resolve the scope from the URL (`session_id` → Session scope, agent → Agent scope via `IAgentLifecycleService.getHandle`), then `accessor.get(IX)` the native or Legacy Service. Match the established verbs, paths (`:sid` / `{session_id}`), and `parseActionSuffix` actions (`:steer`, `:abort`) exactly — sibling routes under `packages/kap-server/src/routes/` are the reference.
```ts
const route = defineRoute(
{
method: 'POST',
path: '/sessions/{session_id}/prompts',
body: promptSubmissionSchema, // ← from kap-server/src/protocol
params: sessionIdParamSchema,
success: { data: promptSubmitResultSchema }, // ← from kap-server/src/protocol
**For `/api/v2` (native):** add a `resource:action` entry to `actionMap` ([edge-exposure.md](edge-exposure.md) §3). If the method fails the direct-exposure rules (returns a handle / stream / bytes, takes a live object), add a wire-safe orchestration method to the owning domain Service first — as `prompts:submit` maps to `IAgentPromptService.submit`, which settles `{turn_id}` engine-side instead of returning the live `PromptHandle`.
### 5. Map errors
The route translates domain `KimiError` codes into protocol `ErrorCode` numbers. Two registries must stay in sync:
- **Domain code** — register in `agent-core-v2/src/errors.ts` (`ErrorCodes`) and throw from the Service (errors.md). Co-located domain errors go in `<domain>Legacy/errors.ts` (e.g. `prompt.not_found`, `session.busy`).
- **Wire code** — register the matching number in `packages/kap-server/src/protocol/error-codes.ts` and reference it in the route's `errors` map and `sendMappedError`.
Match the v1 route's status codes and idempotent-conflict envelopes (e.g. `prompt.already_completed` → `40903` with `{ data: { aborted: false } }`). The error envelope is part of the wire contract — it is covered by the same schema-fidelity rule.
### 6. Test against the v1 wire shape
Add a `packages/kap-server/test/<resource>.test.ts` that boots the server and hits the route. Assert on the **envelope + protocol shape**, not on the v2 domain internals:
- the fields v1 clients read are present with the same names/types.
Where the route mirrors v1, the test is the regression guard for the schema-fidelity rule: if someone drifts the protocol schema or the projection, this test breaks.
### 7. Verify
- `pnpm -C packages/kap-server test` — server routes green.
- `pnpm -C packages/kap-server test` — server routes green (incl. any wire-schema guards).
- `pnpm -C packages/agent-core-v2 run lint:imports` — the import boundaries (v1 ban, kosong subtree) still hold for a LegacyService.
- `pnpm -C packages/klient test` (optionally with `KIMI_SERVER_URL` for the live legacy suites) when a v1 parity scenario exists.
## Worked example — porting v1 `/sessions/:sid/prompts`
This is the reference alignment (commits `feat(server-v2): port v1 /sessions/:sid/prompts routes`, `feat(server-v2): return turn ids for prompt actions`). It shows all three decisions at once.
**The mismatch.** v1 `IPromptService` is a per-agent *scheduler*: it owns a FIFO queue, assigns `prompt_id`s, supports `steer`/`abort`, and auto-starts the next queued prompt when a turn settles. v2's native `IAgentPromptService` is a *turn driver*: a submission *is* a turn, there is no queue and no `prompt_id`. Forcing the queue into the v2 native Service would distort the v2 domain.
**The split.**
- `/api/v2` keeps the native shape — `prompts:submit` / `steer` / `undo` / `clear` / `cancel` map to the domain Services (`IAgentPromptService.submit` / `submitSteer`, `IAgentConversationUndoService.undo`, `IAgentLoopService.cancelFromUser`) in `actionMap`.
- `/api/v1` gets an `AgentPromptLegacyService` (`prompt/`, `LifecycleScope.Agent`) that re-implements the v1 scheduler — queue, `prompt_id`, steer/abort, auto-start-next — **on top of** the native `IAgentPromptService`. The `/api/v1` routes consume the LegacyService.
**The schema.** Both surfaces import `promptSubmissionSchema` / `promptSubmitResultSchema` / `promptListResponseSchema` / `promptSteerRequestSchema` / `promptSteerResultSchema` / `promptAbortResponseSchema` from the shared v1 wire schemas (see `packages/kap-server/src/protocol`). The `/api/v1` and `/api/v2` routes are therefore compatible with released clients by construction; the LegacyService projects v2 turn results back into those protocol shapes.
**The errors.** v1 codes (`prompt.not_found`, `session.busy`, `prompt.already_completed`) are registered in `agent-core-v2` (`prompt/errors.ts`) and in `packages/kap-server/src/protocol` (`error-codes.ts`), then mapped in the route's `sendMappedError` — including the idempotent `prompt.already_completed` → `40903 { data: { aborted: false } }`.
**The lesson.** When the v1 contract and the v2 domain disagree, add an adapter (LegacyService) at the edge; do not let the wire contract leak into the native domain. The two surfaces share the protocol schema but not the Service.
## Migration checklist
Before submitting a server-align change:
- [ ] Surface chosen deliberately: `/api/v1` mirror for a v1-matched endpoint, `/api/v2` for a new native capability (both if needed).
- [ ] For a `/api/v1` mirror, the route matches the established v1 contract (protocol schema + sibling routes) path-for-path, verb-for-verb, action-for-action.
- [ ] Request and response schemas come from their owning home (the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`); no inline re-declaration in server-v2.
- [ ] Existing schema fields are unchanged in name, type, and semantics; only optional fields added (if any).
- [ ] Native v2 Service left clean; v1-only behavior isolated in a `<domain>Legacy` / `I<Domain>LegacyService` edge adapter when the semantics diverge.
- [ ] LegacyService registered with the correct `LifecycleScope` and named as the `<domain>Legacy` edge adapter preserving the native Service.
- [ ] Domain error codes registered in `agent-core-v2`; wire codes registered in `packages/kap-server/src/protocol`; route maps them in `sendMappedError`, matching v1's status codes and idempotent envelopes.
- [ ] Route resolves the scope from the URL by `accessor.get(IX)`; no cached scope; finishes before disposal.
- [ ] Tests assert the wire envelope + protocol shape; wire-shape guards added/updated where the route mirrors v1.
- [ ] `lint:imports` passes; the LegacyService did not invert scope direction.
## Red lines (this subskill)
- One wire schema, one home: the owning `agent-core-v2` domain contract or `packages/kap-server/src/protocol`. Never re-declare a v1 wire schema inline in server-v2.
- A `/api/v1` mirror route must keep every existing schema field's name, type, and semantics; only optional additions are allowed. A different shape belongs on `/api/v2`, not on the mirror.
- Do not distort the native v2 Service to satisfy a v1 quirk — add a `<domain>Legacy` edge adapter instead. The native Service serves the v2 architecture; the LegacyService serves the wire contract.
- A LegacyService is still a v2 Service: it follows scope, domain-direction, and DI rules. "Edge adapter" describes its role, not an exemption.
- The established wire schema (in its owning home — the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`) plus the existing mirror routes are the spec for a `/api/v1` route — match them; do not re-derive the wire shape from the v2 domain model or from memory.
- Register every new error code in **both**`agent-core-v2` and `packages/kap-server/src/protocol/error-codes.ts`; an unmapped code is a wire break.
- Events stream over WS (`listen`), never over the REST mirror; do not invent REST polling for something v1 pushed as an event.
How to write a Service in `packages/agent-core-v2`: file layout, naming, what goes in the contract vs the impl, interface style, constructor / field conventions, events, multi-Service domains, and the comment rules. This is the day-to-day reference for stage 3 (implement.md covers the DI *mechanics*; this file covers the *authoring details*).
## File layout
One folder per domain, **camelCase**: `session/`, `sessionActivity/`, `contextMemory/`, `toolDedup/`. Inside, six kinds of files:
```text
<domain>/
├── <name>.ts ← interface file: exactly one IXxx + its createDecorator + the types it owns
├── <name>Service.ts ← impl file: exactly one class + exactly one registerScopedService(...)
├── <concern>.ts ← pure function(s): no Service suffix, no class, no registration
├── <targetDomain>.ts ← contribution file (common): registers into another domain's extension point
└── <domain>.types.ts ← shared types that no single interface owns
```
- **Strictly one service per file.** An interface file holds exactly one injectable interface and exactly one `createDecorator(...)`; an impl file holds exactly one service implementation class and exactly one `registerScopedService(...)`. No exceptions for "tightly-coupled" groups: even same-scope collaborators each get their own `<name>.ts` + `<name>Service.ts` pair.
- **Scope is in the filename.**`workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)).
- A domain therefore has as many impl files as it has services (e.g. `logService.ts` for the App `ILogService`, `sessionLogService.ts` for the Session `ISessionLogService`). See [Multi-Service domains](#multi-service-domains).
The package entry `src/index.ts` imports and `export *`s every domain's leaf files precisely (one line per leaf), so importing the package still runs every `registerScopedService(...)` side effect — exactly as the old per-domain barrels did.
## Naming
### Interfaces and classes
| Artifact | Rule | Example |
|---|---|---|
| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Workspace` / `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `IWorkspaceDirs`, `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) |
| Class | the interface name minus the leading `I`, plus `Service` if it does not already end in `Service`; `implements` the interface | `SessionLogService implements ISessionLogService`, `AppendLogStoreService implements IAppendLogStore` |
| Decorator string | lowerCamelCase of the interface name minus the leading `I`; **globally unique and stable** (it surfaces in `CyclicDependencyError.path` and "no service registered" errors) | `createDecorator<ISessionLogService>('sessionLogService')` |
| Model / non-service types | PascalCase, no `I` prefix | `SessionMeta`, `LogEntry`, `ConfigSection` |
The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Workspace, Session and Agent services always carry `Workspace` / `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names.
> Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, and `ISessionEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md).
### File names
File names derive from the interface / class names so that scope and role are visible in the tree:
| File kind | Rule | Example (interface → file) |
|---|---|---|
| Interface file | interface name minus leading `I`, minus trailing `Service` if present; acronym-aware lowerCamelCase | `ISessionLogService` → `sessionLog.ts`; `IAppendLogStore` → `appendLogStore.ts`; `ILogService` → `log.ts` |
| Impl file | the class name; acronym-aware lowerCamelCase | `SessionLogService` → `sessionLogService.ts`; `AppendLogStoreService` → `appendLogStoreService.ts` |
| Pure-function file | the function / concern name; no `Service` suffix | `formatLogEntry.ts`, `levelEnabled.ts` |
| Contribution file (common) | the **target** domain name | `config.ts` (registers a config section), `tool.ts`, `flag.ts` |
Acronym-aware lowerCamelCase lowercases a leading acronym as a group: `ILLMRequester` → `llmRequester.ts`, `IWSGateway` → `wsGateway.ts`, `IOAuthToolkit` → `oauthToolkit.ts`, `IMcpServerService` → `mcpServerService.ts`.
Because the impl class always ends in `Service` and the interface file never does, the two files of one service never collide — even for `Store` / `Registry` / `Resolver` interfaces (`IAppendLogStore` → `appendLogStore.ts` + `appendLogStoreService.ts`).
## The contract file (`<domain>.ts`)
Holds the public surface of the domain. A typical contract:
```ts
/**
* `greet` domain (Ln) — one-line role.
*
* Defines the `Greeting` model and the `IGreeter` used by … Bound at … scope.
*/
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface Greeting { // model — no _serviceBrand
readonly message: string;
}
export interface IGreeter { // injectable service — carries _serviceBrand
- **Service interface(s)** — the contract consumers depend on.
- **Decorator(s)** — one `createDecorator` per injectable service.
- **Helper types and pure functions** tightly bound to the contract — e.g. option bags, `satisfies`-checked seeds, predicate functions like `levelEnabled`.
### Which interfaces carry `_serviceBrand`
Only interfaces used as a **DI token** carry `readonly _serviceBrand: undefined`. Everything else does not:
- ✅ Service interface resolved via `@IX` / `accessor.get(IX)` → carries `_serviceBrand`.
- ❌ Base interface extended by a service (e.g. `ILogger` extended by `ILogService`) → no `_serviceBrand`.
- ❌ Plain model / data interface (`LogEntry`, `SessionMeta`) → no `_serviceBrand`.
```ts
export interface ILogger { // base interface — no brand
- **Imports** — `LifecycleScope` + `ScopeActivation` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/<domain>` alias; the contract's types + decorator via a relative `./<domain>` import.
- **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file.
- **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration.
Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this` — `provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable` (leave a NOTE comment saying so).
## Constructor conventions
- Declare every dependency with `@IX` on a constructor parameter.
- Use `private readonly` (or `protected readonly`) to store a used dependency as a field.
- For an injected dependency the class does **not** directly use (e.g. passed through, or only needed to force construction order), drop the visibility modifier and prefix with `_`: `@IEventService _event: IEventService`.
- Service parameters and static parameters may both appear; the ordering rule depends on how the object is created — see below.
### Parameter order: scoped service vs `createInstance`
- **`registerScopedService` services** — the container injects only the `@IX` parameters; any static parameters must have defaults and are left at their default when the container builds the instance. Order is therefore not enforced by the container, but the common style is **`@IX` parameters first, optional static parameters after**:
- **`createInstance` objects** (non-singletons built with `instantiation.createInstance(Ctor, …staticArgs)`) — static parameters **must come first**, service parameters after, because the caller passes the static prefix positionally:
```ts
constructor(
private readonly input: string, // static — passed by caller
@ILogService private readonly log: ILogService, // service — injected
) {}
```
### Factory methods
A scoped Service may expose a factory method that returns a **new** instance of itself (or a related class) with extra context bound — e.g. `ILogger.child(ctx)` returns `new LogService(this.writer, { …this.bound, …ctx }, this._level)`. This is not a DI violation: it is an explicit factory, not a request for the container to build a Service. Do not use it to circumvent scope or singleton semantics.
## Fields and state
- `private readonly` for fields set once at construction (injected deps, derived config).
- `readonly` public fields only for immutable exposed state; prefer a getter (`get level()`) when the value can change.
- Keep state minimal — a Service owns only the state that matches its scope's identity (design.md §2). Anything else belongs in a different Service.
### Runtime state goes into the per-scope state container
Workspace/Session/Agent-scope Services register their runtime state into the scope's state container (`IWorkspaceStateService` / `ISessionStateService` / `IAgentStateService`, all over `_base`'s `StateRegistry`) instead of holding it in bare instance fields, so per-scope state lives in one observable place (`snapshot()` / `onDidChange`) and dies with the scope. Reference: `session/interaction/interactionService.ts`.
- Declare keys in the domain file and export them: `export const interactionPendingKey = defineState<Map<string, Pending>>('interaction.pending', () => new Map())` — `<domain>.<field>` naming, factory initializers.
- Inject `@ISessionStateService private readonly states` (or the Agent token) and `this.states.register(key)` per key at the top of the constructor.
- Replace the field with accessors: a getter for collections only mutated in place (`this.foo.add(...)` keeps working — the container stores references, never clones); add a setter routed through `states.set` for reassigned scalars. Call sites stay unchanged.
- Values must be plain data: scalars, arrays, and literal objects/Maps/Sets built from them. Never register class instances, resource handles (disposables, abort controllers, Promise locks), or objects holding service references — the regression precedent: one registry key whose class instances reached the whole DI graph deep-copied to hundreds of MB on `snapshot()` and OOM-killed the server. This means registries whose entries carry resources (the tool registry, the task map, prompt queues) stay as instance fields alongside Emitters, hook slots, disposable slots, waiter arrays, caches, and queue instances.
- `snapshot()` additionally recurses plain data only: values with a custom prototype collapse to a `'(ClassName)'` marker — a `_base`-level backstop, not a license to register resource-bearing values.
- Durable, replayable state does NOT belong here — it stays on wire Models. The container is memory-only.
## Events
v2 has two distinct event mechanisms. Pick by audience:
### `Event<T>` / `Emitter` — typed property on a Service
Use when a Service exposes a typed event its consumers subscribe to. Lives in `'#/_base/event'`.
```ts
// contract
import type { Event } from '#/_base/event';
export interface IConfigService {
readonly onDidChange: Event<ConfigChangedEvent>;
}
// impl
import { Emitter, type Event } from '#/_base/event';
export class ConfigService extends Disposable implements IConfigService {
- Back the public `Event<T>` with a private `Emitter<T>`, registered with `this._register(...)` so it disposes with the Service.
- Naming: `onDid…` for "happened" (past tense, after the fact); `onWill…` for "about to happen" (may allow `waitUntil` participation / veto — see `AsyncEmitter` / `IWaitUntil` in `'#/_base/event'`).
- A service must be constructed before consumers can subscribe to its events. Use the default `OnScopeCreated` activation when subscriptions must be available as soon as the scope is ready.
### `IEventService` — global pub-sub bus
Use to broadcast protocol events across domains. Lives in `'#/event'`.
Inject `@IEventService` and `publish(...)`; `subscribe(...)` returns an `IDisposable` to register with `this._register(...)`. This is the bus for "a fact happened, react if you care" (design.md §4) — not for typed per-Service events.
## Multi-Service domains
A domain may define several Services. Each Service gets its own pair of files regardless of scope or coupling:
- **One pair per Service** → `<name>.ts` for the contract + `<name>Service.ts` for the implementation.
- **Different scopes** → the scope prefix in the Service name makes this obvious (`logService.ts` for App `ILogService`, `sessionLogService.ts` for Session `ISessionLogService`).
- **Same interface, multiple role tokens** (e.g. `IAtomicDocumentStore` and `IAtomicTomlDocumentStore` share one interface type but are distinct DI tokens) → each token is its own Service identity and must be registered and resolved independently.
There is no `index.ts` barrel: consumers import each contract/impl from its precise leaf path (e.g. `import { ILogService } from '#/log/log'`), never the domain directory.
## No barrel — the package entry loads leafs precisely
A domain has **no `index.ts` barrel**. Its files are the contract leaf (`<name>.ts`) and the impl leaf (`<name>Service.ts`), and consumers import the precise file — never the directory:
```ts
import { IGreeter, type Greeting } from '#/greet/greet';
```
Self-registration is unchanged: `greetService.ts` keeps its top-level `registerScopedService(...)`. The package entry `src/index.ts` loads the domain's leafs precisely — `export *` for the contract, a side-effect `import` for the impl — one line per leaf:
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService';
```
Importing the package therefore fires every `register*` side effect, exactly as the old per-domain barrels did. When you add a new domain, write the contract + impl leafs (with their top-level `register*`), then add the leaf path(s) to `src/index.ts`. **Do not create an `index.ts`.**
- Load the impl file too — its top-level `registerScopedService(...)` only runs when the module is imported.
- `export *` helper modules only if they are part of the domain's public surface.
## Comments
- **No comments** (orient.md): no file headers, no statement-level narration, no JSDoc — not on exported symbols either; the only exception is a load-bearing `oxlint-disable` / `eslint-disable` directive.
- **Methods and fields carry no comments.** Well-named identifiers and types say *what*; the code is the source of truth for *how*.
- For unimplemented stubs, throw `NotImplementedError('feature')` rather than `throw new Error('TODO: …')` (errors.md).
## Complete minimal example
```ts
// greet/greet.ts
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
- One folder per domain, camelCase; one service per file pair: contract `<name>.ts` + impl `<name>Service.ts`; **no `index.ts` barrel** — `src/index.ts` loads each leaf file precisely.
- Exactly one injectable interface and one `createDecorator(...)` per contract file.
- Exactly one service implementation class and one `registerScopedService(...)` per impl file.
- `IXxxService` / `XxxService` naming; decorator string is lowerCamelCase, globally unique, and stable.
- Name Services by owning domain, never by scope (`IAgentEntityService`, `ISessionEntityService`).
- `_serviceBrand` only on interfaces used as a DI token — never on base interfaces or plain models.
- Sync methods return concrete types, async return `Promise<T>`; do not `Promise`-wrap sync work.
- `createInstance` objects put static parameters before service parameters; scoped services put `@IX` parameters first (static params need defaults).
- Never `new` a `@IService`-carrying Service — except inside an explicit factory method, which is not a DI request.
- Events: typed per-Service event → `Event<T>`/`Emitter` from `'#/_base/event'`; cross-domain broadcast → `IEventService` from `'#/event'`.
- `src/index.ts` must import/export every leaf file (including the impl) so each `register*` side effect runs.
- No comments by default (orient.md); stubs throw `NotImplementedError`.
Telemetry infrastructure for agent-core-v2: how business services emit events, how context propagates, and how events reach a destination through appenders.
Telemetry is a **layer-1 root** domain (alongside `log`): the facade lives at `App` scope (a per-Agent ambient context service is bound at `Agent` scope), stateless, with no business-domain dependencies. It is a thin facade — enrichment, batching, and transport belong to the appenders, not to this layer.
- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent<P>` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent<P>`.
- `src/app/telemetry/agentTelemetryContext.ts` + `agentTelemetryContextService.ts`: `IAgentTelemetryContextService` — Agent-scoped mutable request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) snapshot into turn telemetry at launch. Agent identity (`agent_id`) is not part of it — identity is bound by the Agent-scoped `ITelemetryService` view.
- `src/app/telemetry/consoleAppender.ts`: `ConsoleAppender` — echoes events to a log function (dev / debug).
- `src/app/telemetry/cloudAppender.ts`: `CloudAppender` — sanitizes + PII-cleans properties, batches + enriches + posts to the telemetry endpoint.
- `src/app/telemetry/cloudTransport.ts`: `CloudTransport` — HTTP transport behind `CloudAppender`.
- `src/app/telemetry/privacy.ts`: outbound PII redaction (`cleanTelemetryProperties`) — URLs, emails, tokens, and absolute file paths become `<REDACTED: ...>` labels; `node_modules/` tails are kept.
## Emitting events (business services)
Inject `ITelemetryService` and call `track2` with a registered event:
```ts
import { ITelemetryService } from '#/app/telemetry/telemetry';
`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent<P>({ owner, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent<P>` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
`TelemetryService.track` merges the bound context into the properties and fans the event out to every registered appender. A single throwing appender is isolated via `onUnexpectedError` and never blocks the rest.
### Context (sessionId / agent_id / turn_id)
The root service carries a bound context (`sessionId`) that is merged into every event, and each Agent scope gets its own telemetry view seeded with `agent_id` (by `agentLifecycle`), so Agent-scoped services emit their identity without call-site plumbing. Mutable per-agent request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) lives in `IAgentTelemetryContextService` and is snapshot into a per-turn view at turn launch. Derive a scoped view with `withContext`:
`withContext(patch)` returns a lightweight forwarding view: transport state (appenders, enabled flag) stays with the root, so later `addAppender` / `setEnabled` calls apply to every view, and per-call properties override bound context on key collision. `setContext(patch)` on the root mutates the root context and propagates to appenders that implement `setContext`; on a view it mutates only that view's own context.
## Appenders (destinations)
An appender is the destination an event is fanned out to. It is **not a DI Service** — it is a plain object implementing `ITelemetryAppender`, held by `TelemetryService`.
`addAppender` returns an `IDisposable` that removes the appender when disposed. `setAppender(appender)` resets to a single appender (mainly for tests). `removeAppender(appender)` drops one.
> There is no production bootstrap wired yet — `TelemetryService` defaults to `[nullTelemetryAppender]`, so `track(...)` is a no-op until `addAppender` is called at startup.
## Lifecycle
- `setEnabled(false)` drops `track` (service-level switch); `setEnabled(true)` resumes. `flush` / `shutdown` are unaffected by the switch.
- `flush()` / `shutdown()` fan out to all appenders concurrently; a single rejecting appender is swallowed. Await `shutdown()` before process exit so buffered events (e.g. in `CloudAppender`) are sent.
## Red lines (this topic)
- Business services depend only on `ITelemetryService` — never import an appender class.
- Telemetry is layer-1 root: do not inject any business-domain service into it, and keep the facade at `App` scope (only the ambient context service binds at `Agent`).
- Appenders are plain `ITelemetryAppender` objects, not DI Services — register them with `addAppender`, never via `registerScopedService`.
- `track` is fire-and-forget and must not throw; appender `track` must be synchronous — buffer and send asynchronously via `flush` / `shutdown`.
- Await `telemetry.shutdown()` before process exit when a buffering appender is registered.
- Keep event names stable; register every business event in `events.ts` and emit via `track2` — properties must be JSON-serializable primitives (non-primitives are dropped with a warning by `CloudAppender`).
- Agent identity is ambient: agent-scope events go through `defineAgentTelemetryEvent` and get `agent_id` from the scoped telemetry view — do not pass `agent_id` at business call sites (per-event identities such as `subagent_created` and the cron events are the exception).
Exercise the **same path production uses**: a service is reached by its interface through the container, its `@IService` dependencies are resolved from the container, and — where the scope layer matters — through the scope tree. Tests that `new` a service and paper over its constructor with hand-rolled objects bypass that path and let the `registerScopedService(IX → Impl)` binding rot untested.
`@IService` parameter decorators run under vitest (the build uses `experimentalDecorators`), so fixtures declare dependencies exactly like production code. There is **no**`param()` helper, no manual `(Id as …)(Ctor, '', 0)`, and no capturing `accessor` inside a constructor to synchronously `.get()` a peer.
## The one rule
**Resolve the system under test by its interface, through the container. Never call `new` on a production service whose constructor carries `@IService` dependencies.**
```ts
// ✅ resolve by interface — the IX → Sut binding is exercised
ix.set(IMessageService, new SyncDescriptor(MessageService));
const svc = ix.get(IMessageService);
// ❌ construct the implementation directly — the registration is never run
const svc = new MessageService(stubContext);
```
Resolving by interface is what makes `registerScopedService(ISut, Sut, …)` part of the test. Constructing the class directly (or via `ix.createInstance(Sut)`) tests the class in isolation but leaves the binding, the scope layer, and the delayed/eager flag unverified.
Pure functions, value objects, and services with **no**`@IService` dependencies may be constructed directly.
The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — annotate it with a comment explaining why.
## Two harnesses
Pick the harness by *whether the scope layer is part of what you are testing*.
| Under test | Harness | Resolve the SUT with |
|---|---|---|
| A single service's behavior (unit) | `TestInstantiationService` (flat) | `ix.get(ISut)` after `ix.set(ISut, new SyncDescriptor(Sut))` |
| Cross-scope wiring, or which layer a service lives in | `createScopedTestHost` (scope tree) | `host.<scope>.accessor.get(ISut)` |
### Unit harness — `TestInstantiationService`
Default for domain service unit tests. It is an `InstantiationService` that also implements `ServicesAccessor` (so you can `ix.get(...)` directly) and owns sinon (so `dispose()` restores stubs).
```ts
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices } from '#/_base/di/test';
import type { TestInstantiationService } from '#/_base/di/test';
import { registerRecordsServices } from '../records/stubs';
describe('XxxService', () => {
let disposables: DisposableStore;
let ix: TestInstantiationService;
beforeEach(() => {
disposables = new DisposableStore();
ix = createServices(disposables, {
base: [registerRecordsServices],
additionalServices: (reg) => {
reg.define(IContextService, ContextService); // 1. real collaborator, by interface
reg.define(IXxxService, XxxService); // 2. system under test, by interface
},
});
});
afterEach(() => disposables.dispose());
it('does the thing', () => {
const svc = ix.get(IXxxService); // 3. resolve by interface
expect(svc.thing()).toBe('…');
});
});
```
`createServices` builds the container from domain **service groups** plus per-test overrides (see Service groups). Reach for `ix.stub(...)` / `ix.set(...)` directly only inside an `it` when a single test needs to swap a registration:
- single method: `ix.stub(IId, 'method', value)` returns a sinon stub; `ix.spy(IId, 'method')` returns a spy;
- a prebuilt instance or descriptor: `ix.set(IId, instance)` / `ix.set(IId, new SyncDescriptor(Impl))`;
- when a collaborator's behavior must vary per test, model it as a `Test*Service` subclass whose methods read suite-scoped `let` variables rather than rebuilding the container each test.
### Scope harness — `createScopedTestHost`
Reach for this only when *which layer a service lives in* is itself the thing being asserted, or when the SUT reads from parent/child scopes. It builds the real `Scope` tree and resolves through it.
```ts
import { beforeEach, describe, expect, it } from 'vitest';
import { LifecycleScope } from '#/app/scopes';
import {
ScopeActivation,
_clearScopedRegistryForTests,
registerScopedService,
} from '#/_base/di/scope';
import { createScopedTestHost, stubPair } from '#/_base/di/test';
describe('XxxService (scoped)', () => {
beforeEach(() => {
_clearScopedRegistryForTests();
registerScopedService(
LifecycleScope.Agent,
IXxxService,
XxxService,
ScopeActivation.OnDemand,
'xxx',
);
});
it('resolves from the Agent scope with ancestor deps injected', () => {
const svc = agent.accessor.get(IXxxService); // by interface
expect(svc.thing()).toBe('…');
host.dispose();
});
});
```
Always `_clearScopedRegistryForTests()` and re-register explicitly in `beforeEach`. Do not rely on a production module's top-level `registerScopedService(...)` side effect: import order then becomes part of the test, and another suite's `_clearScopedRegistryForTests()` can wipe it.
## Register the SUT by interface
Whichever harness you use, the SUT is registered under its interface (`ix.set(IX, new SyncDescriptor(Impl))` or `registerScopedService(scope, IX, Impl, …)`) and resolved by that interface. This is non-negotiable: it is the only thing that keeps the production registration honest.
A test that does `ix.createInstance(Impl)` is testing the class, not the service. Convert those (see Migration).
## Shared stubs
Hand-rolled stubs (`noopLog`, `noneEvent`, `unusedRecords`, …) must not be copied between test files. Each domain that owns a frequently-stubbed interface exports a stub from a `stubs.ts`**in the `test/` tree**, never from `src/`:
```text
test/log/stubs.ts → stubLog() / stubLogger()
test/turn/stubs.ts → stubTurn()
test/records/stubs.ts → stubAgentRecords()
test/environment/stubs.ts → stubEnvironment()
```
All test support lives under `test/` so test-only code stays out of the production source tree. Because `tsdown` builds from `src/index.ts`, anything under `test/` is unreachable from the entry and is never bundled into `dist/`.
Conventions:
- export a **factory** (`stubXxx()`), not a shared singleton, so tests cannot leak state through a stub;
- name it `stub<Interface>` — e.g. `stubAgentRecords`;
- the stub satisfies the full interface so the compiler, not a cast, guarantees it stays in sync;
- import it with a **relative path** — `./stubs` from the same domain's tests, `../<domain>/stubs` from another domain. Never import stubs from `#/…` (that alias is for production `src/`) and never import one test file from another;
- a `stubs.ts` may import its domain's production types via `#/<domain>/…`.
If a stub is needed by two test files, it belongs in that domain's `test/<domain>/stubs.ts`.
## Service groups
Most unit tests stub the same handful of collaborators (`ILogService`, `IAgentRecords`, `IConfigService`, `ITelemetryService`, …). Rather than repeat `ix.stub(...)` lines in every `beforeEach`, each domain exports a `register*Services` function from its `stubs.ts` that registers the default test doubles for that domain:
```ts
// test/log/stubs.ts
export function registerLogServices(reg: ServiceRegistration): void {
- `base` — an ordered list of service groups. Each group's registrations are deduped (first writer wins), so groups supply safe defaults without clobbering each other.
- `additionalServices` — applied after `base`. Registrations here **overwrite** any base default, so a test can swap a stub for a spy, register the system under test, or supply a one-off collaborator.
reg.define(IAgentRecords, spyRecords); // override a base default
reg.define(IXxxService, XxxService); // system under test
},
});
```
`ServiceRegistration` offers three verbs:
- `define(id, Ctor)` — lazy `SyncDescriptor`; the service is instantiated on first resolve. Use for real collaborators and the system under test.
- `defineInstance(id, instance)` — a fully-built instance (a fake such as `stubLog()`, or `new ConfigRegistry()`).
- `definePartialInstance(id, { ... })` — a partial mock; only the supplied members are provided. Use for collaborators the test does not exercise.
Conventions:
- a group registers the domain's services **as dependencies** (a fake, or a `{}` partial when no fake exists yet). When a service is the system under test, the test registers the real implementation via `additionalServices` and does not rely on the group's default for it;
- keep groups small and domain-local. A service that is almost always the system under test, or that every consumer configures differently, should not have a group — register it inline via `additionalServices`;
- import groups with a **relative path** (`../<domain>/stubs`), never from `#/…`.
`createServices` defaults to `strict: false` (missing dependencies warn rather than throw), matching `new TestInstantiationService()`. Pass `strict: true` to surface unregistered `@IService` dependencies.
## Declaring dependencies
Always use `@IService` constructor decorators — in fixtures and in production services alike.
Because the decorator runs when the class is defined, the `createDecorator` identifier must be initialized **before** the class that uses it. Declare the identifier, then the class:
```ts
const IDep = createDecorator<IDep>('dep');
class Consumer {
constructor(@IDep private readonly dep: IDep) {}
}
```
For two services that depend on each other (a cycle), declare both identifiers first, then both classes, so neither class references an uninitialized binding.
Declare fixtures at module top, interface + decorator + implementation co-located, and keep `_serviceBrand` on the interface when it represents a real service — `GetLeadingNonServiceArgs` relies on the brand to tell service parameters apart from static ones. Pure throwaway fixtures may omit `_serviceBrand`.
## Lifecycle / teardown
One `DisposableStore` per suite. Add the **container** and any event subscriptions to it; dispose in `afterEach`.
Do **not** add the system-under-test itself to the store. `TestInstantiationService` disposes every service it creates when the container is disposed, so `ix.get(IX)` instances are cleaned up automatically via `disposables.add(ix)`. Wrapping the SUT in `disposables.add(...)` would double-dispose it. For the same reason, do not call `svc.dispose()` at the end of a test unless you are asserting something about disposal itself.
Scope-host tests call `host.dispose()` in `afterEach` (or at the end of the `it`). Route teardown through the store so ordering is deterministic and nothing leaks when a test fails mid-way.
## Cascade: asserting unit state
The cascade engine's test vocabulary lives in two files: `test/_base/di/cascade.test.ts` (the mechanism matrix, including cross-scope orchestration) and `test/_base/di/provide.test.ts` (provide/unprovide semantics).
- **Assert unit states, not internals.** Every container exposes its engine as `container.cascade`: `stateOf(IX)` → `'Pending' | 'Activating' | 'Active' | 'Unloading' | 'Failed'`; `failureOf(IX)` → the sticky error of a `Failed` unit; `pendingSnapshot()` → the waiting-area contents.
- **The waiting area parks units with unregistered dependencies** — a unit whose declared deps are missing stays `Pending` (no throw), so a test must seed the full dependency chain. Example: a root→agent chain with no session container must seed the session-scope dependency explicitly — `ix.set(ISessionStateService, new SessionStateService())` in `test/session/agentLifecycle/agentLifecycle.test.ts` — or the dependent unit never activates.
- **Eager activation failure is sticky `Failed`, not a scope-creation throw.** Assert state + rethrow: `expect(ix.cascade.stateOf(IX)).toBe('Failed')`, then `expect(() => ix.invokeFunction((a) => a.get(IX))).toThrow(…)`. Do not expect scope/host creation itself to throw for a failing eager constructor.
## Assertions and naming
- One behavior per `it`; describe observable behavior (`child shadows parent registration`), not implementation (`calls _getOrCreateServiceInstance`).
- For cycles, assert `CyclicDependencyError` and its `path` array (e.g. `['A', 'B', 'A']`), not merely `toThrow`.
- For disposal order, capture events in an array and assert the sequence (`['C', 'B', 'A']` — children before parents).
## Migrating existing tests
Most legacy tests build the SUT with `ix.createInstance(Impl)`. Converting one is mechanical:
1. import the interface (`IX`) and the descriptor;
2. register the SUT by interface — `reg.define(IX, Impl)` inside `additionalServices` (or `ix.set(IX, new SyncDescriptor(Impl))`);
3. replace `ix.createInstance(Impl)` with `ix.get(IX)`;
4. drop the `disposables.add(...)` wrapper around the SUT and any trailing `svc.dispose()` — the container disposes it;
5. replace any hand-rolled collaborator object with the domain's shared stub or service group (or add one to `test/<domain>/stubs.ts` if it does not exist);
6. delete now-unused imports.
Before / after:
```ts
// before
const svc = ix.createInstance(MessageService);
// after — registration in beforeEach additionalServices
reg.define(IMessageService, MessageService);
// after — resolution in the test body
const svc = ix.get(IMessageService);
```
## Red lines (this stage)
- Resolve the SUT by interface — never `new` a production service with `@IService` deps; prefer `ix.get(IX)` over `ix.createInstance(Impl)`.
- Shared stubs live in `test/<domain>/stubs.ts` (never `src/`); import by relative path, never `#/...`.
- Scope tests call `_clearScopedRegistryForTests()` and re-register explicitly in `beforeEach`; do not rely on production import-order side effects.
- One `DisposableStore` per suite; add the container, dispose in `afterEach`; do not add the SUT itself.
- Declare fixture dependencies with `@IService`; initialize `createDecorator` identifiers before the classes that use them.
## Changesets (when the change ships through the CLI)
If the change is user-facing and ships through the CLI, generate a changeset with the repository's `gen-changesets` skill (root `AGENTS.md` workflow). `agent-core-v2` is an internal package; if its change enters the CLI bundle, the changeset lists `@moonshot-ai/kimi-code` and describes the real change — do not present an internal-only change as a user-facing feature. Never write a `major` bump without explicit user confirmation.
## Pre-submit checklist
Walk the stages you touched and confirm:
- **Design** — scope follows state identity; no `Map<sessionId, …>` at `App`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around.
- **Implement** — no `new` on `@IService`-carrying classes; `@IX` on constructor params only (service params after static params); interface + impl carry `_serviceBrand`; decorator names unique; coded errors only; flags for unreleased behavior.
- **Test** — SUT resolved by interface; stubs under `test/`; scope tests re-register after `_clearScopedRegistryForTests()`; teardown through one `DisposableStore`.
- **Files** — no comments at all (no JSDoc either; only load-bearing `oxlint-disable` / `eslint-disable` survive); registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.
Then re-read the [global red lines](SKILL.md#global-red-lines) once — they catch most cross-stage mistakes in a single scan.
## Red lines (this stage)
- Do not skip `lint:imports` — it is the only automated check for the v1-import ban and the kosong subtree rules.
- Do not list internal packages in a changeset when the change enters the CLI bundle — list `@moonshot-ai/kimi-code` and describe the real change.
- Never write a `major` changeset without explicit user confirmation.
description: Use ONLY for code review and test write/review guidance in `packages/agent-core-v2` (the DI × Scope agent engine). Does NOT apply to the legacy `packages/agent-core` or to any other package — for those, do not load this skill. Groups the review and testing lenses used for agent-core-v2 — `slop` (single-level-of-abstraction / layered error-handling review, invoked only on explicit request) and `test` (contract-driven per-test rules for both authoring and reviewing tests). Apply the sub-skill that matches the task; do not apply `slop` unprompted.
has-sub-skill: true
---
# kc-review
> **Scope: `packages/agent-core-v2` only.** These lenses are calibrated for the v2 engine (DI × Scope). Do not apply them to the legacy `packages/agent-core` or to other packages.
A bundle of the lenses used when reviewing or testing `packages/agent-core-v2`. Each sub-skill is self-contained; invoke the one that matches the task.
## Sub-skills
- **`slop/`** — Single Level of Abstraction & layered error handling. A *review dimension*: a function should read as a straight-line description of its own layer, with errors handled above or below. The agent reports detections and measurements, not severity grades. **Invoke only when the user explicitly asks for this lens** — do not apply it unprompted to general reviews or refactors.
- **`test/`** — Per-test rules behind "test the contract / responsibility, not the implementation," serving two modes. **Write mode:** author a test — one behavior per `it`, drive through the public surface, stub only the true external boundary, control time/config via documented knobs, keep tests clear, isolated, and refactor-resilient (CCCR). **Review mode:** audit existing tests against the same rules and report findings with `file:line`. Use when writing, modifying, or reviewing tests, or when asked how to write a good single test.
## Routing
- Reviewing code structure / abstraction layers / where error handling belongs → `slop` (only on explicit request).
- Writing or modifying tests, reviewing test quality, or advising on a single test → `test`.
description: Invoke only when the user explicitly asks to review code through the "single level of abstraction / layered error handling" lens — a function does only its own layer's business logic while errors are handled above or below. The agent reports detections, raw-count measurements, and move directions. Apply only when the user explicitly requests this lens.
---
# Single Level of Abstraction & Layered Error Handling
North star: **a function should read as a straight-line description of what its own layer does. Anything that is not that — input validation, error handling, error-to-response translation, logging, retries, low-level mechanics — belongs to a layer above or below, not inline.**
This is a review dimension, not a hard rule. See "Exemption checklist" at the end.
## Scope of this skill — detect and measure
The agent applying this lens is a **sensor**. Its one job is to report *whether* a function mixes levels and *by how much*; deciding *how serious* it is belongs downstream. Severity labels (`Block` / `Request changes` / `Nit`) compress a continuous quantity into an uncalibrated three-point scale and are the main source of review-to-review variance, so they are produced downstream — by a deterministic rubric, anchored examples, or a human — from the facts the agent reports.
The agent's output is exactly these four things:
- **Detection (yes/no):** does this statement / block / function violate a rule of the lens?
- **Measurement (raw factual counts only):** mechanically countable quantities — body size, control-flow keywords, named syntactic shapes (see "Quantify"). Anything that first requires classifying a line (core/foreign, happy/error, high/low level) is recorded under detection, not here.
- **Direction (where it moves):** for each foreign concern, the destination layer — push **down** into a value / parser / infra helper, or push **up** into the edge handler.
- **Exemption flags:** which items, if any, hit the exemption checklist — recorded, not weighed.
Severity grades, merge/block verdicts, and "is splitting worth it" calls live downstream, derived from the four items above.
## When to use
Apply this lens only when the user asks for it explicitly (for example "用单一抽象层次审视一下", "check whether this function does too much", "errors should be handled above/below, right?"). Leave general reviews and refactors to other lenses unless the user names this one.
## The principle
One function, one level of abstraction, one responsibility. Three mutually reinforcing rules:
1. **Single Level of Abstraction (SLAP).** Every statement inside a function sits at the same conceptual level. High-level intent ("reserve inventory, charge payment, create the order") must not be interleaved with low-level mechanics (building headers, escaping strings, opening sockets, parsing bytes). If some lines read as "what" and others as "how", they belong in different functions.
2. **Error handling is its own concern (Clean Code).** A function either does the work or handles the error — not both. Business logic describes the happy path and *signals* failure (throw or return a result); the catch, mapping, logging, and recovery live in a dedicated handler, usually one layer up. Prefer exceptions / result types over threaded check-and-return ladders that interrupt the main flow.
3. **Separation of concerns by layer.** Each layer owns exactly one kind of knowledge: low-level code knows formats and protocols; mid-level code knows business rules; edge code knows the outside world (HTTP / CLI / UI). A function that knows two of these at once is leaking a layer.
The combined test: **could you explain this function to someone without using the word "and"?** If the explanation is "it reserves stock AND validates the email format AND maps the error to a status code AND logs to metrics", it is doing more than its layer's job.
Concerns that usually do **not** belong in a business function:
- Format / range / null validation that a lower value or parser could guarantee once.
- Mapping domain failures to an external protocol (status code, exit code, UI message) — that is the edge layer's job.
- Catch-and-swallow, retry loops, backoff, timeout, circuit breaking around a single call — infrastructure, push down.
- Cross-cutting telemetry / log / metric noise woven through every step — extract or push to a wrapper.
- Check-and-return ladders that occupy more space than the business core — replace with signal + a handler above.
## Methodology — fixing a function that violates it
Work top-down. Never start by shuffling lines.
1. **Name the level.** In one sentence, write what this function is for at its own layer. If you cannot, the function has no clear level — split before polishing.
2. **Classify every statement.** Tag each line or block as: **core** (this layer's business), **down** (a detail a lower abstraction should own), **up** (a concern an upper / edge layer should own), or **cross-cutting** (log / metric / retry). Unlabeled lines are where the mess hides — do not "just leave them".
3. **Decide down vs. up for each foreign item.**
- Push **down** when it is a guarantee a lower building block can provide: a value that can only be constructed valid, a parser that returns a typed result, an infra helper that already retries. The business function then assumes validity and stays clean.
- Push **up** when it is about translating or reacting to failure for the outside world: status codes, messages, exit codes, aggregation of many errors. The edge layer catches once and maps; business code just signals.
- Rule of thumb: if removing it would change what the business rule says, it is core and stays; if removing it only changes how a failure is reported or a detail is computed, it moves.
4. **Extract, do not interleave.** Pull each foreign concern into its own named function or layer. Keep the original function as a readable sequence of same-level calls. For error handling specifically, separate the work body from the recovery body into distinct functions so neither clutters the other.
5. **Signal, do not handle, in the middle.** Mid-layer business functions throw / return and let the right layer react. Do not catch-and-log-and-continue in business code unless continuing is itself the business rule.
6. **Re-read for level.** After the moves, every remaining line should be explainable at the same altitude. If not, repeat from step 1.
Keep the change minimal: move the smallest thing that restores the level. Do not invent abstractions, frameworks, or generic "handler" machinery beyond what the function actually needs. Three straight-line, same-level calls beat a premature pipeline.
## Review method — applying the lens to a diff
Read each changed or touched function and, for each check, record only: **the hit (yes/no) plus evidence (`file:line`)**, and — where the check points at a construct — a raw factual count from "Quantify".
1. **Altitude check.** Are all lines at the same level of abstraction? Record each place where a "what" line is immediately followed by a "how" block (or vice versa) inside the same function, with `file:line`.
2. **Happy-path check.** Can you read the business intent top to bottom without stepping through error branches? Record whether error handling sits inline between business steps (yes/no + `file:line`), supported by raw counts from "Quantify" (e.g. number of `catch` clauses, `continue` statements).
3. **Ownership check.** For each validation, catch, mapping, log, retry: is this layer the rightful owner, or is it borrowed from above / below? Record each borrowed item with `file:line` and its destination (down / up), using the rules from the methodology.
4. **Layer-leak check.** Does a business function mention an external protocol (status code, exit code, UI text, wire field)? Does an edge function contain a business rule? Record each leak candidate with `file:line` and whether it names an *external* protocol or an *internal* domain shape.
5. **Explanation test.** Describe the function in one sentence with no "and". Record whether "and" was needed; if so, list the proposed split as candidate moves (down / up).
### Quantify — report only raw factual counts
Report only quantities that can be counted **mechanically from the text**. Anything that first requires classifying a line (core vs foreign, happy-path vs error-handling, high-level vs low-level) is recorded under detection (the five checks above) as evidence, not as a number here.
Report, per function:
- **Body size** — lines and/or statements of the function body; state the basis (e.g. "statements, excluding lone braces").
- **Named syntactic shapes a check points at** — when a check cites a construct, count it verbatim and name the exact token: e.g. number of object literals, string literals, `.trim()` calls, `.length` reads, `origin.` property reads, spread `[...x]` operations.
- **Recovery presence (raw)** — number of `catch` clauses, and number of log / metric calls inside them.
Quantities that embed a prior classification — out-of-level vs core counts, guard-to-core ratios, happy-path vs error-handling volume, "repeated boundary checks a lower layer could guarantee once", "low-level literals in a high-level flow" — are captured as evidence under the relevant check (`file:line` + the verbatim tokens). A downstream rubric derives any ratio from those raw facts.
### Red flags
Record each as evidence (yes/no + `file:line`); these are candidates, not verdicts:
- A body that is mostly check-and-return / check-and-throw ladders around a thin core.
- A recovery block that logs, maps, and returns inline, sitting next to business steps.
- A function that both computes a value and decides how that value's failure is shown to the user.
- Low-level literals (byte offsets, header strings, format codes) inside a high-level workflow.
- A name that needs "And" / "Or" / "With" to be honest, or a name so vague ("handle", "process", "do") that it hides multiple levels.
- Catch-and-swallow that hides a failure the caller needed to see.
- Defensive null / format checks repeated at every call site instead of guaranteed once at the boundary.
### Severity grading belongs downstream
The agent's facts (detections, raw counts, directions, exemptions) feed a downstream grade; the agent reports those facts and stops there. Grades compress a continuous quantity into an uncalibrated three-point scale and are exactly where identical evidence gets labeled differently across runs. Grading happens above the agent:
- A **deterministic rubric** — a versioned threshold table over the raw counts from "Quantify"; or
- **Anchored examples** — the reviewer judges relative to repo-known reference functions rather than against an absolute adjective like "materially"; or
- A **human**, for items that land near a threshold boundary.
If a downstream consumer still asks the agent for a grade, the agent returns the underlying facts and the threshold band it would fall under, with `confidence: low` on boundary cases; the grade itself is produced downstream.
### How to report findings
Report **evidence + direction**. Lead with the location and the level, then the proposed move. Prefer "this block is one level lower than the rest of the function (`file:line`) — move it **down** into X" over "this is ugly" or "this is a request-changes". The destination layer (down into a value / parser / infra helper, or up into the edge handler) is the actionable output and the deliverable. Attach the "Quantify" numbers and any exemption flags to each finding.
## Exemption checklist
This is a lens, not a law. For each foreign concern, check whether any exemption below applies and **record the hit (yes/no) plus the reason**. The agent records exemptions as facts; a recorded exemption is then used downstream to cap the grade (e.g. to `Nit`) deterministically.
- **Tiny function:** the function is small enough that splitting would add indirection with no reader benefit.
- **Foreign concern is the single job:** the "foreign" concern is in fact the function's one purpose — a dedicated error mapper, a validator, an infra wrapper, or an index-bookkeeping helper whose low-level arithmetic *is* its level.
- **Atomicity / correctness / performance:** the steps genuinely must stay together (e.g. a re-check after an `await` to guard state that may have changed).
- **Edge-translator role:** an edge / handler function whose job is to translate an external event into internal indices; naming the wire fields is its job.
Keep a split that would make the code harder to read as a recorded candidate for downstream review. When the evidence lands on an exemption boundary, record both sides and set `confidence: low`.
## Output contract
Return, per function, items 1–5 only:
1. **Level statement** — one sentence: what the function is for at its own layer.
2. **Per-check results** — for each of the five review checks: `hit: yes/no`, evidence `file:line`, and (only where the check points at a construct) a raw factual count.
3. **Measurements** — the raw factual counts from "Quantify".
5. **Proposed moves** — for each foreign concern: `file:line` → destination (down into X / up into Y). This is the actionable deliverable.
Severity grades, block/merge verdicts, and "worth splitting" calls live downstream, derived from items 1–4. When a consumer asks for a label, hand back items 1–4 and the threshold band, with `confidence: low` on boundary cases.
description: Use when writing or reviewing tests, or when asked how to write a good single test. Encodes the per-test rules behind the "test the contract / responsibility, not the implementation" principle — name and structure one behavior per `it`, drive through the public surface, stub only true external boundaries, control time and config via documented knobs, and keep tests clear, isolated, and refactor-resilient. The same rules drive both authoring (write mode) and auditing existing tests (review mode).
---
# Tests — write & review
Per-test rules that operationalize one principle: **test the contract / responsibility, not the implementation**. This is the how-to for a single `it`, and the lens for reviewing one.
## Two modes, one rule set
- **Write mode** — authoring a test. Apply the rules below to produce it.
- **Review mode** — auditing an existing test or test diff. Apply the same rules as a checklist; report each violation with `file:line`, the rule it breaks, and the fix. See "Review mode" near the end.
The rules are identical in both modes — only the posture changes (produce vs. audit).
## Test contract, not implementation
- Drive the system through its **public control plane** and assert on **observable effects** (returned values, persisted state, emitted events, injected messages), never on source details.
- Resolve collaborators through their contract — the interface plus its identifier — not the module that binds a concrete implementation.
- Do not reach into private fields or add backdoors "for testing". If you feel the need, the seam is wrong — fix the design, not the test.
## One behavior per `it`
Each `it` covers exactly one responsibility / scenario. If the name needs "and", split it.
```ts
it('returns 401 when the caller is unauthorized', ...);
it('does not double-fire when the same tick repeats', ...);
```
## Name and structure
- `describe('<slice> (<responsibilities>)'` — name the **responsibility**, not the class.
- An `it(...)` reads as a sentence, but it must still encode three things — the **behavior / method**, the **state or condition**, and the **expected outcome**: `it('<behavior> when <condition>, <outcome>')`. A name like `does X when Y` with no result is too vague to fail usefully.
- Use spaces, not the Java-style `method_state_outcome` underscores — that convention exists only because Java test methods cannot contain spaces. A string-named test reads fine as a sentence.
- Good: `it('returns 401 when the caller is unauthorized')` · `it('advances the cursor and does not double-fire on a repeat tick')`
- Bad: `it('works')` · `it('handles auth correctly')` — no condition, no outcome
- Arrange / Act / Assert. A short `// Given``// When``// Then` is fine when it aids reading; do not paste it mechanically on trivial tests.
## Build a small rig
When several tests share setup, write a factory (`rig()`, `createHost()`, whatever fits the codebase) that returns the **smallest surface the test needs**. Tests reach into the rig; they do not rebuild the world each time. Keep the rig dumb: wiring only, no assertions.
## Stub only the real external boundary
Default to real collaborators wired the way production wires them. Stub the **minimum seam** that is genuinely external:
- A remote / model / service boundary — spy on the contract method (the interface), and capture what the system sends across it. Do not stand up the real external thing.
- Network / other-process boundaries — stub at the boundary, not the internals.
- Time, timers, jitter — use the documented control knobs the system exposes (env, an injected clock, a manual tick). Do **not** use fake timers or real `setTimeout` to drive time.
- Env / config knobs are usually snapshotted at bootstrap — set them **before** building the system under test, and restore them in `afterEach`.
## Keep tests DAMP and keep cause next to effect
- DAMP over DRY: use **literal expected values** in assertions; do not compute the expectation with the same logic as the code under test.
- Keep the key preconditions inside the `it` (or its rig), where the reader can see cause next to effect. Reserve `beforeEach` for cross-cutting plumbing (env snapshot, cleanup), not for hiding the scenario's setup.
```ts
// Good — the expected value is a literal the reader can check.
expect(discount).toBe(15);
// Bad — re-derives the expectation; mirrors the implementation.
expect(discount).toBe(price * rate);
```
## Assert only what is relevant
Assert the effect that proves the contract. Use matchers / partial-object matching to ignore incidental fields. Do not assert internal counters, call orders, or shapes the user cannot rely on.
## Isolate and clean up (no flakes)
Every test must be hermetic and order-independent. In `afterEach`:
- restore every mock / spy
- restore every env var you touched (snapshot in `beforeEach`)
- dispose the host / container and reset its reference
No dependence on wall-clock time, run order, or leftover on-disk state — give each scenario its own isolated identity / workspace when state persists.
## Quality bar: CCCR
Before finishing, check each test against:
- **Clarity** — a stranger can tell what broke from the failure message alone.
- **Completeness** — covers the responsibility's success, error, and boundary paths.
- **Conciseness** — no duplicate or speculative cases; one scenario per `it`.
- **Resilience** — survives an internal refactor with no test change (because it asserts contract, not implementation).
## Per-file scenario header
Start each test file with a short header comment: the **scenario**, the **responsibilities** asserted, the **wiring** (which collaborators are real vs. the single stubbed boundary), and how to run it.
## Review mode — auditing existing tests
Apply the rules above as a checklist against each test in scope (a file, a diff, or a named `it`). For every hit, report `file:line` + the rule it breaks + the fix; do not rewrite unless asked. Lead with the contract question: *what observable behavior does this test prove, and would it survive a refactor?*
Check, in order:
1. **Contract, not implementation** — asserts observable effects, not private fields, call order, or internal shapes the user cannot rely on.
2. **One behavior per `it`** — the name carries behavior + condition + outcome; "and" in the name means a split is owed.
3. **Boundary discipline** — only the true external seam is stubbed; time is driven by documented knobs, not fake timers / real `setTimeout`.
4. **DAMP expectations** — expected values are literals, not re-derived by the code under test's logic.
5. **Isolation** — mocks / spies / env / host restored in `afterEach`; no wall-clock, run-order, or leftover on-disk dependence.
Report findings as evidence + fix, e.g. "`foo.test.ts:42` asserts on `service.internalMap` (contract) — assert the returned value instead." If a test passes the lens, say so briefly; silence on a rule means it held.
## Quick checklist (write & review)
- Resolved through the contract; no concrete-impl import
- One behavior per `it`; name carries behavior + condition + outcome; AAA
- Stubbed only the true external seam; time via knobs, not fake timers
- Literal expectations; relevant assertions only
- Mocks / env / host restored in `afterEach`; hermetic, no flakes
@ -5,7 +5,7 @@ description: Use when generating changesets in the kimi-code repository — deci
# Generate Changesets
The only user-facing published package is the CLI: `@moonshot-ai/kimi-code`. All other `@moonshot-ai/*` packages (sdk, agent-core, kosong, kaos, oauth, telemetry, and so on) are internal.
The only user-facing published package is the CLI: `@moonshot-ai/kimi-code`. All other `@moonshot-ai/*` packages (sdk, kosong, oauth, telemetry, and so on) are internal.
## 1. Whether to Write
@ -32,6 +32,7 @@ Fix occasional loss of tool call results in long conversations.
Wording:
- One short, user-facing English sentence that states only what changed. Drop trailing clauses that explain the cause, the benefit, or the mechanism.
- When a default flips or an existing behavior is removed, name the behavior users lose, not only the new default. State the escape hatch if one exists, e.g. `Stop hot-reloading config, skills, and AGENTS.md by default; set [watch] enabled = true or KIMI_CODE_WATCH=1 to turn it back on.`; if there is none, say plainly that the old behavior is no longer available.
- New features: say plainly what it is plus one line on how to use it, e.g. `Add the /foo slash command to list active sessions. Run /foo to see them.`
- Experimental features: also state how to enable them (the flag, config key, or env var).
- No file, class, or function names, and no PR numbers. No vague words like refactor, optimize, or improve. No real internal identifiers — use neutral placeholders such as `example.com` or `YOUR_API_KEY`.
@ -60,4 +61,4 @@ Wording:
4. **Show the changeset text to whoever requested the work and get their confirmation before committing.**
5. Do not guess at changes you do not understand: finish the parts that are clear, then list what is unclear and ask whether you may dig into the code.
Before a release, review the accumulated `.changeset/` entries and delete the non-user-facing ones — the release PR regenerates from `.changeset/` on main, so deleting a file removes its changelog entry without touching shipped code.
Before a release, review the accumulated `.changeset/` entries and delete the non-user-facing ones — the release PR regenerates from `.changeset/` on the selected release branch, so deleting a file removes its changelog entry without touching shipped code.
Select the PR targeting the intended `release/*` branch, not the legacy main version PR. Report its latest head CI link and result alongside the preview.
```bash
gh pr list --state open --search "ci: release packages in:title" \
@ -126,7 +126,12 @@ Public-text rule: do not copy real internal endpoints, key names, account names,
### 4. Merge, Deduplicate, And Classify Entries
Before classifying, merge related entries and drop redundant ones from the user-facing changelog:
First, count the stripped upstream entries in the version range being synced. The count decides how much curation happens before the human review checkpoint:
- **10 entries or fewer: list everything and let the user decide.** Classify every entry into its section and list them all — do not fold, merge, or drop entries on your own, and do not write a catch-all line. At the human review checkpoint (step 8), present the full list with advisory notes (which entries look low-signal under the two gates below, and why) so the user decides what stays, what merges, and what folds.
- **More than 10 entries: curate first, then review.** Merge related entries and fold low-signal ones into the catch-all line before writing the page, using the rules below. The reviewer still owns the final cutoff — surface the borderline calls instead of resolving them silently.
The rules below are the shared curation standard: they drive the curator's fold/merge decisions when there are more than 10 entries, and they drive the advisory notes when there are 10 or fewer.
- **Curate for end users: collapse low-signal entries into one catch-all line.** The docs changelog is the only curated, user-facing outlet; the full entry list always remains in the upstream package changelog, so hiding detail here loses nothing. Apply two gates to every candidate entry. Gate 1, the reader-action test: **after reading this, is there something the reader must do, or something they must re-evaluate?** Gate 2, the channel test: **is the changelog the only channel that can deliver this?** The changelog is the channel of last resort — when the product itself surfaces the information in context, at the moment of need, to exactly the affected users, the entry is redundant no matter how real the improvement is. "Surfaced" means pushed into the user's path, not merely present on screen: an event-triggered card, prompt, or post-install screen forces the encounter, while a toggle, menu item, command, or settings page only waits to be found. Users do not explore — a capability that lives only in ambient UI is effectively undiscoverable, so the changelog must announce it. What in-product surfacing cannot deliver: hidden controls (env vars, config keys, opt-out flags nobody would find unprompted), invalidations of existing habits or expectations (in-product discovery comes as confusion), and capabilities users would not know to seek. An entry that fails either gate folds. Anchor both gates to the changelog's reader, never to the bug's victim: someone who hit a loud failure does not need the changelog to confirm the fix — the product working again is the notification — and a reader who never hit it gets nothing from the entry.
- `Features`: keep when users would try it or must react to it — new capabilities create demand readers did not know to seek. Collapse only behavior that takes effect solely behind an experimental flag.
@ -324,7 +329,7 @@ Check:
- Each version has the same section set and order on both pages.
- Each section has the same number of entries on both pages.
- Within each section, the most valuable, obvious, and larger entries appear before smaller or narrower entries.
- Low-signal entries were collapsed into the single catch-all line, placed last under `Bug Fixes` — or under `Polish` when nothing folded is a fix (both the reader-action test and the channel test applied); the catch-all wording matches what was folded and never claims fixes that did not happen; section sizes stay within the density defaults (about 2 Polish, 3 Bug Fixes) unless extra qualifying entries were deliberately kept and flagged for review. The catch-all line ends with the upstream changelog pointer (file link, no version anchor).
- Entry count decided the curation level: a version range with 10 or fewer upstream entries lists everything classified, with no catch-all line (fold/merge decisions deferred to the review checkpoint); with more than 10, low-signal entries were collapsed into the single catch-all line, placed last under `Bug Fixes` — or under `Polish` when nothing folded is a fix (both the reader-action test and the channel test applied); the catch-all wording matches what was folded and never claims fixes that did not happen; section sizes stay within the density defaults (about 2 Polish, 3 Bug Fixes) unless extra qualifying entries were deliberately kept and flagged for review. The catch-all line ends with the upstream changelog pointer (file link, no version anchor).
- PR links and commit hashes were stripped.
- No `Thanks ...!` credit remains (remove it every time).
- Real internal identifiers were replaced with neutral placeholders.
2. Summarize synced versions, section counts, and anything that needed manual classification. List every entry folded into a catch-all line (short titles, one line each), any section that exceeds the density defaults, and every borderline call flagged during curation — the reviewer cannot own a cutoff they cannot see.
2. Summarize synced versions, section counts, and anything that needed manual classification. When the synced version range had 10 or fewer upstream entries and everything was listed, present the full entry list with advisory notes (which entries look low-signal and why) and ask the user to decide what stays, what merges, and what folds. When entries were folded into a catch-all line (more than 10 upstream entries), list every folded entry (short titles, one line each), any section that exceeds the density defaults, and every borderline call flagged during curation — the reviewer cannot own a cutoff they cannot see.
3. Tell the user to reply when they are done reviewing, or to ask for edits.
4. Do **not** commit, push, or open a PR until the user explicitly says review is complete, or asks to proceed.
@ -434,6 +439,7 @@ Return the PR URL to the user when done.
- Never edit upstream `apps/kimi-code/CHANGELOG.md`.
- Do not backfill unreleased `.changeset/*.md` drafts into the docs site.
- If upstream wording is wrong, leave upstream alone and fix it in a future changeset.
- A version range with 10 or fewer upstream entries is listed in full; fold/merge/drop decisions belong to the user at the review checkpoint, not the curator.
- Always sync on a `docs/changelog-sync-*` branch and open a PR; never push changelog docs sync directly to `main`.
- Wait for the human review checkpoint before committing, pushing, or opening a PR.
@ -446,7 +452,8 @@ Return the PR URL to the user when done.
| Leaving the `Thanks ...!` credit in docs | Remove it every time, including the multi-author form |
| Leaving near-duplicate micro-tweaks as separate bullets | Merge small tweaks to the same surface into one higher-level entry (e.g. composer height + font → composer's default styling) |
| Listing many narrow fixes to the same surface as separate bullets | When three or more fixes target the same UI area or the same class of problem, merge them into one higher-level fix entry; keep genuinely distinct or high-value fixes standalone |
| Listing low-signal fixes or internal changes as standalone bullets | Collapse them into the single catch-all line (`Fix several known issues.`) placed last under Bug Fixes; treat the section-size defaults (about 2 Polish, 3 Bug Fixes) as a density guard, not a quota |
| Listing low-signal fixes or internal changes as standalone bullets when curating (>10 entries) | Collapse them into the single catch-all line (`Fix several known issues.`) placed last under Bug Fixes; treat the section-size defaults (about 2 Polish, 3 Bug Fixes) as a density guard, not a quota |
| Folding, merging, or dropping entries on your own when the version range has 10 or fewer upstream entries | List everything classified, with no catch-all line, and let the user decide at the review checkpoint |
| Folding a qualifying entry just to hit the section-size default | The defaults are density guards; keep entries that genuinely pass the reader-action test and flag the overflow for the human reviewer |
| Keeping a fix because it was severe or hard-won | Severity makes the announcement redundant — the fix itself notifies whoever was hit; keep only behavior-change fixes and retrospective notices with a concrete, locatable action |
| Keeping an improvement the product surfaces in context (recovery cards, post-install guidance, progress displays) | The product is the better channel — right users, moment of need; fold it (channel test) |
| Behavior | Before | After | Who relies on the old behavior | Escape hatch |
|---|---|---|---|---|
| reasoning parsing when `reasoning_details` is an array | string `reasoning` also read | string ignored | OpenAI-compatible gateways that send both (OpenRouter dialect) | none |
- `src/tui/commands/` — slash-command declaration, parsing, ordering, and dynamic skill-command generation. Parsing and types only; execution is dispatched from `KimiTUI`'s slash-command handler section, and complex execution sinks into `utils` or focused components.
- `src/tui/components/` — pi-tui components by UI type: `chrome/` (footer, todo, welcome, loader, device code), `dialogs/` (selectors, approval/question panels, settings popups that replace the editor), `editor/` (input box + mention provider), `media/` (image, diff, code highlight),`markdown/` (the app's Markdown wrapper and mermaid art),`messages/` (transcript blocks + tool-renderers), `panes/` (activity, queue).
- `src/tui/reverse-rpc/` — adapts SDK approval/question callbacks into UI panel data and the user's choice back into an SDK response.
- `src/tui/theme/` — themes, color tokens, style helpers, pi-tui markdown theme, terminal-background detection. The single source of truth for color.
- `src/tui/utils/` — TUI-only utilities (need `TUIState` or a component). App-wide, UI-independent helpers go in `src/utils/`.
@ -68,7 +68,7 @@ Themes are managed centrally under `src/tui/theme/`:
- `bundle.ts` — packs `colors`, `styles`, `markdownTheme` into a `KimiTUIThemeBundle`.
- `index.ts` / `detect.ts` — theme type and auto/dark/light resolution.
> **Keep the color-token set in sync.**`ColorPalette` in `colors.ts` is the source of truth for color tokens. When you add, rename, or remove one, update its mirrors in the same change: the custom-theme JSON schema (`apps/kimi-code/src/tui/theme/theme-schema.json`), the token tables in the custom-theme docs (`docs/en/customization/themes.md` and `docs/zh/customization/themes.md`), and the token table in the `custom-theme` built-in skill (`packages/agent-core/src/skill/builtin/custom-theme.md`).
> **Keep the color-token set in sync.**`ColorPalette` in `colors.ts` is the source of truth for color tokens. When you add, rename, or remove one, update its mirrors in the same change: the custom-theme JSON schema (`apps/kimi-code/src/tui/theme/theme-schema.json`), the token tables in the custom-theme docs (`docs/en/customization/themes.md` and `docs/zh/customization/themes.md`), and the token table in the `custom-theme` built-in skill (`packages/agent-core-v2/src/features/skill/catalog/builtin/custom-theme.md`).
All other workspace packages are private internal packages, are not published to npm, and are excluded via `ignore` in `.changeset/config.json`:
- `@moonshot-ai/agent-core`
- `@moonshot-ai/kaos`
- `@moonshot-ai/kimi-code-oauth`
- `@moonshot-ai/kimi-telemetry`
- `@moonshot-ai/kosong`
- `@moonshot-ai/migration-legacy`
- `@moonshot-ai/protocol`
- `@moonshot-ai/vis`
- `@moonshot-ai/vis-server`
- `@moonshot-ai/vis-web`
@ -102,23 +99,27 @@ Commit messages must follow Conventional Commit style. Do not include any author
### 4. CI generates the release PR
Once the changeset file is merged into `main`, `.github/workflows/release.yml` uses `changesets/action@v1` to create or update a release PR.
Changesets accumulate on `main`. Create `release/<cycle>` from the selected commit; pushes to that branch use `changesets/action@v1` to create or update its release PR. Manual **Release** runs on a release branch only prepare the PR. The workflow explicitly dispatches CI for the generated version branch.
Once the release PR is merged into `main`, the same workflow runs:
Review the version PR, latest CI and CLI/extension pre-changelog together. Once the matching version PR is merged into `release/<cycle>`, publication waits for that exact commit’s push CI. Ordinary pushes do not publish. The existing publication steps then run:
- `pnpm install --frozen-lockfile`
- `pnpm build`
- `pnpm changeset publish`
The packages are then published via npm Trusted Publishing, and a GitHub Release is created.
The packages are then published via npm Trusted Publishing, and a GitHub Release is created. CLI native builds still depend on Changesets reporting a newly published CLI package; npm-succeeded/native-failed whole-run recovery is not changed by this migration.
VS Code uses the same approved release commit and CI gate, publishing only when the extension version changed in that push. It retains its existing build and marketplace checks. It no longer publishes on main pushes or through a standalone manual entry. The documentation workflow is dispatched on main after CLI artifacts are uploaded; its deployment result is tracked separately. CDN remains manual.
Migration: merge the main infrastructure PR first to disable the old publisher, then the release infrastructure PR. These PRs do not publish. Close the obsolete main version PR separately. After the actual release, synchronize versions, changelogs, lockfile and consumed changeset removals back to main, preserving later main work. Main synchronization does not publish npm or VS Code. Do not change Desktop's submodule pointer as part of this step.
## Manual Publishing (Not Recommended)
@ -143,9 +144,9 @@ The root-level `pnpm run publish` first runs typecheck, lint, sherif, test, buil
- Every PR that affects publishable-package behavior or public API should include a corresponding changeset.
- Changes under `plugins/` (the bundled official plugins such as `kimi-datasource`) do **not** need a changeset: each plugin carries its own version in `kimi.plugin.json` and `plugins/marketplace.json` and is distributed via the marketplace CDN, separately from the `@moonshot-ai/kimi-code` npm package.
- Changeset files must be committed to the repository — release PRs are only triggered after they're merged.
- Release PRs require human review and merge; they will not publish automatically.
- Release PRs require human review and approval. They do not publish before merge; merging the approved PR automatically publishes after its commit passes CI.
- Do not add release changesets for private internal packages; only select `@moonshot-ai/kimi-code` and `@moonshot-ai/kimi-code-sdk`.
- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/agent-core` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix.
- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/kosong` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix.
- `@moonshot-ai/kimi-code` is the official CLI package name; after a global install it provides the `kimi` command.
- Make sure each publishable package on npm has a Trusted Publisher configured.
Add `/provider refresh [providerId]` to re-fetch provider model lists at runtime; unknown `/provider` subcommands now show a usage hint instead of opening the manager.
Fix the conversation view freezing at an old point after restarting the app when a turn had been interrupted; interrupted turns are now marked instead of shown as completed.
@ -6,24 +6,40 @@ External PRs are accepted for approved bug fixes only: link an issue that a main
See https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md for more.
-->
## Related Issue
## Requirement or Bug
<!-- Link the issue this change came from. External PRs must link an issue approved by a maintainer (an `/approve` comment) — PRs without one may be closed. -->
<!-- If there's an issue, write Resolve #(issue_number).
If it's a requirement, describe it briefly in plain language (under 100 characters). -->
Resolve #(issue_number)
## Bug Reproduction Steps
## Problem
<!-- Required for bug PRs only; write N/A for feature PRs.
Prefer writing in the issue and linking here; if no issue exists, write directly. -->
<!-- What user need or limitation does this address? If the linked issue already covers this, write "See linked issue". -->
## Root Cause
## What changed
<!-- Required for bug PRs only. Explain the root cause.
State whether this is a fundamental fix or a workaround. -->
<!-- What did you implement, and why does this approach fit Kimi Code? -->
## Code Changes
<!-- Describe the code changes in plain, easy-to-understand language for the reviewer. -->
## Behavior Changes and Affected Users
<!-- List every observable behavior this PR changes, one row each, including behavior you
consider "unchanged" whose branch conditions moved, and prompt-text edits sentence by sentence.
| Behavior | Before | After | Who relies on the old behavior | Escape hatch |
Name affected users concretely (client, provider dialect, platform, config state, data written
by older versions, external scripts); see .agents/skills/review-pr/surfaces.md.
Write "None" with evidence if nothing observable changes.
Then list affected modules and the test coverage for each row. -->
## Checklist
- [ ] I have read the [CONTRIBUTING](https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md) document.
- [ ] I have linked a related issue (external PRs: the issue must have a maintainer's `/approve`).
- [ ] I have linked a related issue (external PRs: issue must have a maintainer's `/approve`).
- [ ] I have added tests that prove my feature works.
- [ ] The behavior-change table above is complete, and every removed behavior or flipped default is named in the changeset and either has an escape hatch or was explicitly approved by a maintainer in this PR.
- [ ] Ran `gen-changesets` skill, or this PR needs no changeset.
- [ ] Ran `gen-docs` skill, or this PR needs no doc update.
@ -14,19 +14,18 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
## Project Map
- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on engine packages. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- the browser web UI: **its source no longer lives in this repo.** It is developed in the code-app repo (`apps/web`) and shipped as the committed, prebuilt bundle `apps/kimi-code/dist-web` (gitignored, force-added), synced from code-app with `KIMI_CODE_REPO=<this checkout> pnpm run sync:web` — sync and commit the bundle in the same change whenever the web UI should ship differently. `apps/kimi-code/scripts/check-web-assets.mjs` guards packaging against a missing bundle. To hack on the web UI against this repo's server, run `pnpm dev:server` here and point code-app's `pnpm dev:web` at it via `KIMI_SERVER_URL`.
- `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays.
- `apps/kimi-inspect`: web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session transcript chat, per-scope Service panels, and the DI unit inspection view. See `apps/kimi-inspect/AGENTS.md`.
- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities. See `packages/agent-core/AGENTS.md`.
- `packages/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`app/scopes.ts`) — plus the L3 unit layer (`Service`/`Fiber` units, collection contribution points, the Feature seam in `src/features/`); there is no App-level session lifecycle facade — callers compose `ISessionIndex` → `IWorkspaceLifecycleService.handlerFor` → the handler. See `packages/agent-core-v2/AGENTS.md` and use the `agent-core-dev` skill (`.agents/skills/agent-core-dev/SKILL.md`) when developing here.
- `packages/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`app/scopes.ts`) — plus the L3 unit layer (`Service`/`Fiber` units, collection contribution points, the Feature seam in `src/features/`); there is no App-level session lifecycle facade — callers compose `ISessionIndex` → `IWorkspaceLifecycleService.handlerFor` → the handler.
- `packages/node-sdk`: the public TypeScript SDK and harness.
- `packages/kosong`: the LLM / provider abstraction layer.
- `packages/kaos`: the execution environment and file/process abstractions.
- `packages/oauth`: Kimi OAuth and managed auth utilities.
- `packages/transcript`: the isomorphic transcript rendering data layer — L1 agent-granular store, L2 idempotent operations, L3 `off/turn/block/delta` subscription granularity, L4 framework-free view registry, plus turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports); the sole owner of the transcript contract types (`src/contract/`) and the op-batch sequencing contract. See `packages/transcript/AGENTS.md`.
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth). See `packages/kap-server/AGENTS.md`.
- `packages/transcript`: the isomorphic transcript rendering data layer — L1 agent-granular store, L2 idempotent operations, L3 `off/turn/block/delta` subscription granularity, L4 framework-free view registry, plus turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports); the sole owner of the transcript contract types (`src/contract/`) and the op-batch sequencing contract.
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth).
- `packages/remote-control`: the Kimi Remote Control tunnel client — registers this machine with the relay and forwards HTTP/WebSocket traffic to the local server, with a machine-wide single-instance lock; consumed by kap-server (the `/api/v1/remote-control` toggle) and by the CLI (`kimi web --remote-control`).
- `packages/klient`: the client SDK — a contract-driven facade over agent-core-v2 (`global.*` / `session(id).*` / `agent(id).*`, zod-validated); transport via subpath entry (`@moonshot-ai/klient/ipc|memory`, both return the same `Klient`); also hosts the e2e suites. See `packages/klient/AGENTS.md`.
- `packages/tree-sitter-bash`: a pure-TypeScript bash parser (no runtime deps, no wasm); `parse(source, { timeoutMs, maxNodes })` runs under a deterministic budget and returns a discriminated `ParseResult` — callers must treat aborted/hasError trees as "cannot analyze" and degrade. Parser only, no safety judgments; see the package README's "Known differences" section.
- `packages/minidb`: the embedded JSON document store (`MiniDb`) behind kap-server's search index — snapshot + WAL persistence with an exclusive write lock, a larger-than-RAM full-text layer, and persistent index generations. See `packages/minidb/AGENTS.md`.
@ -56,6 +55,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- YES: `interface Options { user?: User }`
- NO: `interface Options { user?: User | undefined }`
- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity.
- Split functions only along abstraction levels: each function reads as one level of narrative (Step-down Rule), and a wrapper that adds no new abstraction level — especially one with a single call site — is inlined instead of extracted.
- Except for a package's `index.ts`, other `index.ts` files should prefer `export * from './module';`.
- Do not add too many new test files. Prefer adding tests to the existing test file of the corresponding component or module.
- When a test fails because of a user modification, default to fixing the test first; do not change the implementation to satisfy an old test unless the implementation truly has a bug.
@ -64,8 +64,7 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
## Experimental Features
- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Precedence is per-flag env > `[experimental]` config > master env > the flag's `default`. Release by flipping the entry's `default` to `true`.
- `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`.
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time (see `packages/agent-core-v2/docs/flag.md`), then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on).
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time, then check it with `IFlagService.enabled(id)`.
## Where to Update Instructions
@ -74,12 +73,17 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- Project-map entries stay at 1–2 sentences; deep package docs live in the package's own `AGENTS.md`.
- Keep instruction updates focused and supported by code facts.
## Release Workflow
- `main` accumulates changesets; `release/*` maintains `ci: release packages` PRs. Merging the matching version PR authorizes publication after its merge-commit CI succeeds. Ordinary pushes do not publish.
- npm publishing and native artifacts retain the existing Changesets flow. CDN upload and routing remain separately approved operations. See `.changeset/README.md` for migration and main synchronization.
## Workflow Requirements
- Prefer `rg` / `rg --files` when reading code.
- When designing changes, follow existing boundaries and local patterns first.
- In public text and test data, replace real internal identifiers with neutral placeholders such as `example.com`, `example.test`, and `YOUR_API_KEY`. Before opening a PR, ask a read-only agent to audit the diff for context-specific internal identifiers.
- When creating a PR, the PR title must follow Conventional Commit style, e.g. `chore: remove legacy format commands`.
- When creating a PR, use the `write-pr` skill (`.agents/skills/write-pr/SKILL.md`) to write the PR description. The PR title must follow Conventional Commit style, e.g. `chore: remove legacy format commands`.
- When an AI agent opens or updates a PR, fill in `.github/pull_request_template.md` — link the related issue or explain the problem, then describe what changed. Do not leave placeholder text or submit a generic summary of the diff.
- Do not submit vague AI-generated PR text. The human author must understand the change well enough to explain the code, edge cases, and why the approach fits this repository.
- After finishing a task and before submitting a PR, you must run the `gen-changesets` skill (see `.agents/skills/gen-changesets/SKILL.md`) and generate a changeset under `.changeset/` according to its rules.
@ -90,3 +94,39 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- Agent working notes or handoff/summary documents (e.g. `HANDOVER-*.md`, `HANDOFF-*.md`, `handoff.md`).
- Throwaway UI/UX prototypes or design mockups (e.g. `*-designs.html`, `*-mockup.html`, `*-demo(s).html`) at the repo root or under a `design/` folder. The only tracked `.html` files should be Vite `index.html` entrypoints.
Before committing or opening a PR, run `git status` and `git diff --staged --stat` and remove anything matching these patterns. Put scratch work under `.tmp/` (gitignored) instead of the repo root or the source tree.
## Code Review Rules
These rules apply to every pull request review, automated or human. The user populations and contract files they refer to are listed in `.agents/skills/review-pr/surfaces.md`.
### Review scope: the PR's diff, not the repo
- Review only the PR's diff against its base. A problem in code the PR does not touch (pre-existing on base) is reported as a non-blocking note, never as a blocking finding — the sole exception is pre-existing code that the PR's behavior directly triggers or amplifies.
- Report a class of issues in one pass: when a finding generalizes to several locations in the diff, list all of them at once instead of raising them across successive rounds.
- No pure style or personal-preference findings. Every finding carries a severity: P0 blocking / P1 should-fix / P2 suggestion.
- Re-reviews are incremental: review only what changed since the previous review; do not restate findings on unchanged code or re-raise resolved threads. Before treating a resolved thread as settled, verify the current code: suppress a finding only when the problem is actually fixed or the finding no longer holds.
- Domain-specific rules keep their own canonical homes — changesets (`.agents/skills/gen-changesets/SKILL.md`), comment-free zones (General Coding Rules), PR text (`write-pr` skill); review against those, not ad-hoc taste.
### Enumerate changed behavior, not just bugs
Any input that worked before the change — a config key, env var, CLI flag, provider response shape, session written by an older version, client request, or hook payload — must behave the same after it unless the PR declares the change. For every deleted or narrowed branch, condition, default, or prompt sentence, ask who reached it before and where they go now; for every new condition, ask which existing inputs now match it first. Flag a PR that calls a path "unchanged" when its branch condition moved.
### A flipped default or removed behavior needs a named loss and an escape hatch
Everyone on the old default is affected. Require the changeset to name the behavior users lose, not only the new default; a config, env, or flag escape hatch or a maintainer's explicit sign-off in the PR; and a test that pins the old behavior for the population that keeps it.
### Prompt text is behavior
Editing or deleting sentences under `packages/agent-core-v2/src/**/*.md` (system prompt, tool descriptions, reminders, overlays, built-in skills) changes agent behavior for every user who receives that prompt. "No test references the sentence" is not evidence of no impact. Require the PR to name the population that receives the text (every session, plan mode, a flag-gated feature such as Tower), what the sentence enforced, who relied on it, and what enforces it now.
### Contract files are tripwires
The manifests under `packages/agent-core-v2/docs/` (`config-manifest.toml`, `wire-manifest.d.ts`, `state-manifest.d.ts`), `packages/kap-server/test/__snapshots__/apiSurface.snapshot.test.ts.snap`, `packages/node-sdk/src/index.ts`, `packages/agent-core-v2/src/features/externalHooks/`, `packages/acp-server/`, and `apps/kimi-code/src/cli/` are consumed outside this repository: desktop and web in code-app, the VS Code extension, ACP clients such as Zed, SDK users, hook scripts, and headless-output parsers. When they change, require the PR to name the consumers and how data and clients from the previous release keep working.
### Ports and refactors carry the old path's feature inventory
When a change replaces or bypasses an existing path, require a list of what the old path did — env vars honored, fallbacks, accepted inputs — and where each item lives in the new path. A silently dropped item is a regression, not a cleanup.
### Silent failure outranks a crash
A change that makes the product silently ignore configuration, silently approve or skip an action, or silently drop data is the most severe finding: users get no signal to report.
- `reverse-rpc` converts SDK approval/question requests into the data shape a UI panel/dialog needs, and converts the user's choice back into an SDK response.
- `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly.
- `utils` holds utility functions with no UI-state dependency. Logic that needs `TUIState` or a component instance must not live under app-level `src/utils`.
- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import `@moonshot-ai/agent-core` directly in app code.
- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import engine packages directly in app code.
- [#4294](https://github.com/MoonshotAI/kimi-code/pull/4294) [`3f80896`](https://github.com/MoonshotAI/kimi-code/commit/3f80896a79c5b17867898bd829b6d7a78f569f47) Thanks [@liruifengv](https://github.com/liruifengv)! - Add a confirmation dialog when switching models in a session with conversation history: start a new session, compact and continue, continue directly, or cancel. Shown only when enabled by cloud configuration.
- [#4188](https://github.com/MoonshotAI/kimi-code/pull/4188) [`3f0857e`](https://github.com/MoonshotAI/kimi-code/commit/3f0857eb7d612c4890c45dfa6acc8381f1e761c2) Thanks [@oocz](https://github.com/oocz)! - List plugin skills under their qualified `<pluginId>:<skillName>` name so same-named skills from different plugins all stay visible and invocable; invoking a plugin skill by its bare name no longer works, use the qualified name instead.
- [#4214](https://github.com/MoonshotAI/kimi-code/pull/4214) [`ef727e5`](https://github.com/MoonshotAI/kimi-code/commit/ef727e517923cb312cb48d42825c49dfacd528e7) Thanks [@sailist](https://github.com/sailist)! - Support streaming large requests and responses through remote control, raising the transfer limit from 10 MiB to 100 MiB, with concurrent transfers bounded to protect memory.
- [#4288](https://github.com/MoonshotAI/kimi-code/pull/4288) [`df0ff3c`](https://github.com/MoonshotAI/kimi-code/commit/df0ff3c5492e8f896927c4af9b4ae00b2b488607) Thanks [@github-actions](https://github.com/apps/github-actions)! - When the same session is open in multiple clients, sending from a second one now shows an occupancy prompt with retry/cancel, or reloads updates made elsewhere first and resends your message.
- [#4200](https://github.com/MoonshotAI/kimi-code/pull/4200) [`5bab748`](https://github.com/MoonshotAI/kimi-code/commit/5bab748dddaae4b2ac15822d72b83446e5fd7bf4) Thanks [@7Sageer](https://github.com/7Sageer)! - Automatically generate a session title after the first turn and show it as the terminal window title.
### Patch Changes
- [#4250](https://github.com/MoonshotAI/kimi-code/pull/4250) [`6e7cc90`](https://github.com/MoonshotAI/kimi-code/commit/6e7cc909da9084a424f822dd5a9aae5b98070b73) Thanks [@sailist](https://github.com/sailist)! - Setting `[permission] dangerous_command_guard = false` now turns off every built-in confirmation guard — dangerous commands, sensitive-file access, and git control-path access — instead of only the dangerous-command guard.
- [#4281](https://github.com/MoonshotAI/kimi-code/pull/4281) [`5710c54`](https://github.com/MoonshotAI/kimi-code/commit/5710c5496a213742ca0687f69dd8eeb5bde52dbf) Thanks [@sailist](https://github.com/sailist)! - Fix session events stopping after a session is archived and restored.
- [#4201](https://github.com/MoonshotAI/kimi-code/pull/4201) [`83a39b4`](https://github.com/MoonshotAI/kimi-code/commit/83a39b49ed62627c7a604b39e26d8e9d17ade296) Thanks [@Grapedge](https://github.com/Grapedge)! - Stop blocking the terminal while the footer reads git status.
- [#4230](https://github.com/MoonshotAI/kimi-code/pull/4230) [`b260326`](https://github.com/MoonshotAI/kimi-code/commit/b2603264ec4687cee766cf5d5e6d4976e432961c) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Report a clear error when thinking effort is set to off for Gemini 4 models.
- [#4208](https://github.com/MoonshotAI/kimi-code/pull/4208) [`20b5991`](https://github.com/MoonshotAI/kimi-code/commit/20b59915bb11fc56ec389aa96720f1b7daac0862) Thanks [@SeleneXX](https://github.com/SeleneXX)! - Fix Gemini 4 requests always failing with a 400 error.
- [#4242](https://github.com/MoonshotAI/kimi-code/pull/4242) [`c1459a6`](https://github.com/MoonshotAI/kimi-code/commit/c1459a61845056832b85a0a220ee11a2bc5132f8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Run hooks such as `PreToolUse` and `Stop` in the session's project directory, as documented, instead of the directory the server was started from.
- [#4210](https://github.com/MoonshotAI/kimi-code/pull/4210) [`f28c51c`](https://github.com/MoonshotAI/kimi-code/commit/f28c51c01f025bba9f0d0f58f6309e7517a8cd02) Thanks [@sailist](https://github.com/sailist)! - Add the [mcp] default_deferred config key: when the experimental tool-select flag is on, set default_deferred = true in config.toml to load every MCP server's tools on demand by default; a server can opt out with deferred: false.
- [#4171](https://github.com/MoonshotAI/kimi-code/pull/4171) [`0327e8e`](https://github.com/MoonshotAI/kimi-code/commit/0327e8e9a875949fd40141ef79f902d756dfd3db) Thanks [@7Sageer](https://github.com/7Sageer)! - Support custom-registry model upgrade metadata in model configuration.
- [#4188](https://github.com/MoonshotAI/kimi-code/pull/4188) [`3f0857e`](https://github.com/MoonshotAI/kimi-code/commit/3f0857eb7d612c4890c45dfa6acc8381f1e761c2) Thanks [@oocz](https://github.com/oocz)! - Allow uppercase letters, dot-separated segments, and @ suffixes in plugin names when installing plugins.
- [#4294](https://github.com/MoonshotAI/kimi-code/pull/4294) [`3f80896`](https://github.com/MoonshotAI/kimi-code/commit/3f80896a79c5b17867898bd829b6d7a78f569f47) Thanks [@liruifengv](https://github.com/liruifengv)! - Queue /compact while the agent is busy: the command now joins the message queue and runs in order instead of failing with an error; it is skipped if the context was auto-compacted while it waited.
- [#4190](https://github.com/MoonshotAI/kimi-code/pull/4190) [`e4c99a3`](https://github.com/MoonshotAI/kimi-code/commit/e4c99a3bad19b00bd1939b1cf254f427b50061f0) Thanks [@starquakee](https://github.com/starquakee)! - Clarify unavailable tools, announce when no tools can be loaded, and discourage repeated failed tool selections.
- [#4272](https://github.com/MoonshotAI/kimi-code/pull/4272) [`ce0834a`](https://github.com/MoonshotAI/kimi-code/commit/ce0834a8f301b89cc4523499a3e074c36851de8f) Thanks [@Grapedge](https://github.com/Grapedge)! - Move the session rating opt-out from tui.toml to config.toml.
- [#4066](https://github.com/MoonshotAI/kimi-code/pull/4066) [`85077a4`](https://github.com/MoonshotAI/kimi-code/commit/85077a419001285e49ede3574924f001935aed62) Thanks [@7Sageer](https://github.com/7Sageer)! - Keep subagent tool descriptions aligned with currently available tools.
- [#3799](https://github.com/MoonshotAI/kimi-code/pull/3799) [`7e58f2d`](https://github.com/MoonshotAI/kimi-code/commit/7e58f2d2410d2b541ffd57f4501c0436bba4ccbd) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix the latest reply disappearing from the transcript after a background task completion notification arrives; the fold boundary is now anchored to the notification turn itself instead of the asynchronously mounted task card.
- [#4239](https://github.com/MoonshotAI/kimi-code/pull/4239) [`da5918c`](https://github.com/MoonshotAI/kimi-code/commit/da5918c0c861a1d670f4523a9f9dd968647fe4cb) Thanks [@RealKai42](https://github.com/RealKai42)! - Remind the agent to update its todo list after 5 steps without an update instead of 10, only while the list still has unfinished items, and ask it to bring the list up to date before its final reply.
- [#4248](https://github.com/MoonshotAI/kimi-code/pull/4248) [`8fcd332`](https://github.com/MoonshotAI/kimi-code/commit/8fcd3326e7488e5e0d4c595115ef74a10c340d5b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Show a single overflowing preview line in full instead of swapping it for a same-height expand hint.
- [#4304](https://github.com/MoonshotAI/kimi-code/pull/4304) [`d36071e`](https://github.com/MoonshotAI/kimi-code/commit/d36071ede6ae3cbdc08cbb8a0f7daf7bc6d06f2d) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Improved interactions and fixed known bugs.
## 2.2.0
### Minor Changes
- [#4159](https://github.com/MoonshotAI/kimi-code/pull/4159) [`0a3a2fb`](https://github.com/MoonshotAI/kimi-code/commit/0a3a2fbafab473df9e9f6609f825af78d2c1ce6d) Thanks [@RealKai42](https://github.com/RealKai42)! - Add an opt-in NotifyUser progress update feature (off by default): new sessions can show the agent's mid-turn progress with an optional one-line title, enabled in the TUI `/experiments` or the web/Desktop settings' Lab section; set `[experimental] notify_user = true` or `KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1` to turn it on.
- [#4145](https://github.com/MoonshotAI/kimi-code/pull/4145) [`242ac23`](https://github.com/MoonshotAI/kimi-code/commit/242ac230006447f8601e1678c49182bfbae89fe9) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add `kimi app [path]` to open a new desktop chat in the current or specified directory.
- [#4061](https://github.com/MoonshotAI/kimi-code/pull/4061) [`20a2cea`](https://github.com/MoonshotAI/kimi-code/commit/20a2cea72f5d2c4be3a3845e4ff24ce302a6630e) Thanks [@chengluyu](https://github.com/chengluyu)! - Cap the agent's in-turn wait for background tasks at 90 seconds (longer timeouts are no longer accepted), discourage repeated waits, and let a new message end the wait immediately.
- [#3998](https://github.com/MoonshotAI/kimi-code/pull/3998) [`09af3b4`](https://github.com/MoonshotAI/kimi-code/commit/09af3b483faac51a767aee50adb1bc1a8de4cc53) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: a message sent while the agent is running aborts the in-progress turn and runs as its own turn.
- [#4031](https://github.com/MoonshotAI/kimi-code/pull/4031) [`33e368a`](https://github.com/MoonshotAI/kimi-code/commit/33e368a1eff8d958856af8e2fb64f4db122b2247) Thanks [@Grapedge](https://github.com/Grapedge)! - Add sticky user message summaries in fullscreen mode; click a summary to jump to the original message.
- [#4074](https://github.com/MoonshotAI/kimi-code/pull/4074) [`c9f01f0`](https://github.com/MoonshotAI/kimi-code/commit/c9f01f07388abc2cdca40c9c5267f58c65086710) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: deterministic orchestration with TowerRebase and LLM-free status, goal/tower mutual exclusion, and workspace protection.
- [#4059](https://github.com/MoonshotAI/kimi-code/pull/4059) [`1f6f0b1`](https://github.com/MoonshotAI/kimi-code/commit/1f6f0b1fa2117cc9374c1be62c534b4a4d60e412) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the KIMI_CODE_TRUST_WORKSPACE environment variable to mark the workspace as trusted.
- [#4165](https://github.com/MoonshotAI/kimi-code/pull/4165) [`afd95f6`](https://github.com/MoonshotAI/kimi-code/commit/afd95f64a5444b649a4cc33504513bf88e4c54ba) Thanks [@Grapedge](https://github.com/Grapedge)! - Replace user skill cards with highlighted prompts and support Ctrl-S for skill prompts.
### Patch Changes
- [#4183](https://github.com/MoonshotAI/kimi-code/pull/4183) [`c15638b`](https://github.com/MoonshotAI/kimi-code/commit/c15638b0a1234e5aa97659fbc625d7dc4d60d93f) Thanks [@RealKai42](https://github.com/RealKai42)! - Cancel in-flight video uploads when a media read is interrupted.
- [#4081](https://github.com/MoonshotAI/kimi-code/pull/4081) [`06ebfc8`](https://github.com/MoonshotAI/kimi-code/commit/06ebfc821e304bd4ca3f828ae739954166463d65) Thanks [@sailist](https://github.com/sailist)! - Fix shifted turns and lost replies when a UserPromptSubmit hook injects text; hook JSON output without a "message" field is no longer added to the conversation.
- [#4154](https://github.com/MoonshotAI/kimi-code/pull/4154) [`419aced`](https://github.com/MoonshotAI/kimi-code/commit/419aced0e97fa04b75f8f71b089e1667f6de6d0a) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix media file reading on the first turn after resuming a session.
- [#4083](https://github.com/MoonshotAI/kimi-code/pull/4083) [`f409caa`](https://github.com/MoonshotAI/kimi-code/commit/f409caa21e71ce7beb158d29ffca1fed76216a64) Thanks [@sailist](https://github.com/sailist)! - Cron tasks from the source session no longer fire inside a forked session.
- [#4152](https://github.com/MoonshotAI/kimi-code/pull/4152) [`f0bc988`](https://github.com/MoonshotAI/kimi-code/commit/f0bc988bbc7db08d650f37ae0eaa69bcc6e90696) Thanks [@7Sageer](https://github.com/7Sageer)! - Forked subagents now inherit the parent's available agent types, including custom agents.
- [#4057](https://github.com/MoonshotAI/kimi-code/pull/4057) [`a940f2f`](https://github.com/MoonshotAI/kimi-code/commit/a940f2ff049ed57abce68c59526de7a6628a4112) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix the permission mode switch in the goal-start approval prompt not taking effect on the v2 engine.
- [#4081](https://github.com/MoonshotAI/kimi-code/pull/4081) [`06ebfc8`](https://github.com/MoonshotAI/kimi-code/commit/06ebfc821e304bd4ca3f828ae739954166463d65) Thanks [@sailist](https://github.com/sailist)! - Fix messages rendering out of place in sessions with a UserPromptSubmit hook.
- [#4153](https://github.com/MoonshotAI/kimi-code/pull/4153) [`7091138`](https://github.com/MoonshotAI/kimi-code/commit/7091138f5e66289d04b9b7a5cd77f94af9308be3) Thanks [@RealKai42](https://github.com/RealKai42)! - Let the agent write one- or two-sentence progress notes between tool calls instead of bare status lines; in the experimental Updates panel (`KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1`), stop asking the agent to repost its mid-turn text as an update and remind it to post updates less often.
- [#4054](https://github.com/MoonshotAI/kimi-code/pull/4054) [`4fbe065`](https://github.com/MoonshotAI/kimi-code/commit/4fbe065442179435c43d3c3dc8d11bb408b3fd30) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop prompting the agent to send NotifyUser updates when a session is opened in a host that does not display them.
- [#4091](https://github.com/MoonshotAI/kimi-code/pull/4091) [`21406fb`](https://github.com/MoonshotAI/kimi-code/commit/21406fb4c805cc8c715e6d1f16ad3fb5f25f4fe3) Thanks [@sailist](https://github.com/sailist)! - Omit max_tokens for models without an explicit output limit, fixing repeated 400 errors on strict serving stacks (e.g. bare vLLM) when the model's max output size is unknown.
- [#4184](https://github.com/MoonshotAI/kimi-code/pull/4184) [`cbd7540`](https://github.com/MoonshotAI/kimi-code/commit/cbd754049a114ee36d1b27675c82819e4963c692) Thanks [@7Sageer](https://github.com/7Sageer)! - Remove the never-implemented `executor = "kaos"` option from MCP stdio server configuration; configs using it now fail validation at load time.
- [#2919](https://github.com/MoonshotAI/kimi-code/pull/2919) [`0f052fe`](https://github.com/MoonshotAI/kimi-code/commit/0f052fee1399f34086bb6343a56867d7df9b70b5) Thanks [@duyphan154](https://github.com/duyphan154)! - Sanitize foreground Bash tool output so captured terminal escape sequences no longer change the host terminal state.
- [#4164](https://github.com/MoonshotAI/kimi-code/pull/4164) [`3395f5b`](https://github.com/MoonshotAI/kimi-code/commit/3395f5bea6c77feb6a835d1ca3f4935970f96c6b) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Show a "Shutting down" status hint while shutdown hooks run during exit, and let a Ctrl+C during that window force an immediate exit instead of waiting for the hook timeout.
- [#4033](https://github.com/MoonshotAI/kimi-code/pull/4033) [`eec9bbd`](https://github.com/MoonshotAI/kimi-code/commit/eec9bbd94958248034f5dbf109a639970308f05e) Thanks [@Grapedge](https://github.com/Grapedge)! - Feedback surveys now appear less often.
- [#4160](https://github.com/MoonshotAI/kimi-code/pull/4160) [`72fc1d0`](https://github.com/MoonshotAI/kimi-code/commit/72fc1d0a3375b2a87dff602a1d91726a99f360e0) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Show "Working…" in the status spinner while thinking.
- [#4056](https://github.com/MoonshotAI/kimi-code/pull/4056) [`395d537`](https://github.com/MoonshotAI/kimi-code/commit/395d537237d737de0239159b60d1cc160bdf04e6) Thanks [@7Sageer](https://github.com/7Sageer)! - Replace inline MCP commands in the workspace trust prompt with activation summaries and configuration paths for manual inspection.
- [#4076](https://github.com/MoonshotAI/kimi-code/pull/4076) [`e3bf50c`](https://github.com/MoonshotAI/kimi-code/commit/e3bf50c0831d91ceb3656acf1b7a3f480eaada56) Thanks [@7Sageer](https://github.com/7Sageer)! - Remove interruption reminders from the conversation when their interrupted turns are undone.
- [#4031](https://github.com/MoonshotAI/kimi-code/pull/4031) [`33e368a`](https://github.com/MoonshotAI/kimi-code/commit/33e368a1eff8d958856af8e2fb64f4db122b2247) Thanks [@Grapedge](https://github.com/Grapedge)! - Replace the user message bullet ✨ with ❯.
- [#4221](https://github.com/MoonshotAI/kimi-code/pull/4221) [`2e6821d`](https://github.com/MoonshotAI/kimi-code/commit/2e6821dcbd8034f128b2326a73d4704201b7c108) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - web: Improved interactions and fixed known bugs.
## 2.1.1
### Patch Changes
- [#4013](https://github.com/MoonshotAI/kimi-code/pull/4013) [`929403b`](https://github.com/MoonshotAI/kimi-code/commit/929403b6db219eae6099dd23eae2f0efa2bb2823) Thanks [@7Sageer](https://github.com/7Sageer)! - Roll back some of the overly defensive changes in 2.1.0
- [#4015](https://github.com/MoonshotAI/kimi-code/pull/4015) [`c7dd841`](https://github.com/MoonshotAI/kimi-code/commit/c7dd84124a00d2dc1a68fbbc3e54b1095ee9ac23) Thanks [@sailist](https://github.com/sailist)! - Revert filesystem watchers for config and workspace files to on by default. Set `[watch] enabled` to `false` or `KIMI_CODE_WATCH=0` to keep them off.
## 2.1.0
### Minor Changes
- [#3990](https://github.com/MoonshotAI/kimi-code/pull/3990) [`32000d0`](https://github.com/MoonshotAI/kimi-code/commit/32000d08afcd019016fa60db0d68eaf21f3e35a6) Thanks [@Grapedge](https://github.com/Grapedge)! - In fullscreen, click a folded block to open or close it.
- [#4004](https://github.com/MoonshotAI/kimi-code/pull/4004) [`e17ff67`](https://github.com/MoonshotAI/kimi-code/commit/e17ff679281e340fd328bc2ebaba1237e79a9ad3) Thanks [@Grapedge](https://github.com/Grapedge)! - Add a TUI mode setting to /settings with an experimental fullscreen layout; switching modes takes effect after restarting Kimi Code. You can also set `tui_mode` in `~/.kimi-code/tui.toml` directly: `"regular"` renders into the terminal scrollback, while `"fullscreen"` makes parts of the UI mouse-interactive for a smoother experience.
### Patch Changes
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Block file tools from accessing files outside the working directory through symlinks.
- [#3975](https://github.com/MoonshotAI/kimi-code/pull/3975) [`bb96d80`](https://github.com/MoonshotAI/kimi-code/commit/bb96d80748921618182369a40a6610f38fa16acb) Thanks [@RealKai42](https://github.com/RealKai42)! - Reduce the CLI's startup time and memory usage.
- [#3957](https://github.com/MoonshotAI/kimi-code/pull/3957) [`6a214b8`](https://github.com/MoonshotAI/kimi-code/commit/6a214b85e53e58a9ef6480f27bcb7b0103c0e34e) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix a "Maximum call stack size exceeded" error when opening large sessions.
- [#3974](https://github.com/MoonshotAI/kimi-code/pull/3974) [`994287a`](https://github.com/MoonshotAI/kimi-code/commit/994287acb3a7428b580c09cabd5aba1b5a818aaa) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix forked sessions getting their titles overwritten by auto-generated titles.
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Apply project-local configuration only after the workspace is trusted.
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Block repository git configuration from executing commands during background git operations.
- [#3969](https://github.com/MoonshotAI/kimi-code/pull/3969) [`b3212fd`](https://github.com/MoonshotAI/kimi-code/commit/b3212fd9abc1f4670fb47df16f0137957aed6f2d) Thanks [@sailist](https://github.com/sailist)! - Skip cloud telemetry on the global login until its collector is live, and keep shutdown from aborting TUI exit.
- [#3979](https://github.com/MoonshotAI/kimi-code/pull/3979) [`e796bb5`](https://github.com/MoonshotAI/kimi-code/commit/e796bb5d482ecb7976c25dad0563b74cc9ec2c68) Thanks [@sailist](https://github.com/sailist)! - Fix MCP OAuth not requesting the offline_access scope, which caused hourly browser re-authorization with providers like Vercel.
- [#3969](https://github.com/MoonshotAI/kimi-code/pull/3969) [`b3212fd`](https://github.com/MoonshotAI/kimi-code/commit/b3212fd9abc1f4670fb47df16f0137957aed6f2d) Thanks [@sailist](https://github.com/sailist)! - Fix Remote Control failing to start after `kimi login --region global`.
- [#3964](https://github.com/MoonshotAI/kimi-code/pull/3964) [`6451f1e`](https://github.com/MoonshotAI/kimi-code/commit/6451f1e056e90037bbf832f3578955cf8e55db64) Thanks [@7Sageer](https://github.com/7Sageer)! - Reject additional directories that resolve to the home directory or filesystem root.
- [#3995](https://github.com/MoonshotAI/kimi-code/pull/3995) [`a54e6f6`](https://github.com/MoonshotAI/kimi-code/commit/a54e6f6a9b36bc1346e41da576d96ff54aa557b6) Thanks [@7Sageer](https://github.com/7Sageer)! - Turn off repeat reminders and forced stops with `KIMI_CODE_REPEAT_BREAKER=0`.
- [#3970](https://github.com/MoonshotAI/kimi-code/pull/3970) [`895e9d9`](https://github.com/MoonshotAI/kimi-code/commit/895e9d9b868cf9899e444784130fa1946c6cef6c) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix missing Swarm members after reopening an interrupted conversation.
- [#3989](https://github.com/MoonshotAI/kimi-code/pull/3989) [`a268ad7`](https://github.com/MoonshotAI/kimi-code/commit/a268ad775d9451d008d3c73de053655f6ab0772a) Thanks [@Grapedge](https://github.com/Grapedge)! - Show a Thinking… spinner and keep the status row height steady during streaming.
- [#3989](https://github.com/MoonshotAI/kimi-code/pull/3989) [`a268ad7`](https://github.com/MoonshotAI/kimi-code/commit/a268ad775d9451d008d3c73de053655f6ab0772a) Thanks [@Grapedge](https://github.com/Grapedge)! - Add a clickable "Jump to bottom" indicator to the fullscreen TUI.
- [#3976](https://github.com/MoonshotAI/kimi-code/pull/3976) [`f7012aa`](https://github.com/MoonshotAI/kimi-code/commit/f7012aa23b38d90d1a643166e2e78f706a606a34) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: smaller missions, stricter completion and review checks, and review history in rework briefings.
- [#3847](https://github.com/MoonshotAI/kimi-code/pull/3847) [`65ae3e3`](https://github.com/MoonshotAI/kimi-code/commit/65ae3e368c7cfa096242cacc12eeeb661e682977) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode: mission titles must be ASCII, and task cards show the mission id.
- [#3931](https://github.com/MoonshotAI/kimi-code/pull/3931) [`f17a22e`](https://github.com/MoonshotAI/kimi-code/commit/f17a22ebf4e21b8196bfaaa9a2490707676bf519) Thanks [@sailist](https://github.com/sailist)! - Turn off filesystem watchers for config and workspace files by default. Set `[watch] enabled` to `true` or `KIMI_CODE_WATCH=1` to turn them back on.
- [#4005](https://github.com/MoonshotAI/kimi-code/pull/4005) [`ba41d07`](https://github.com/MoonshotAI/kimi-code/commit/ba41d07810cae37a9e529cad6b0f8e8abc8d7aaf) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improved interactions and fixed known bugs.
## 2.0.2
### Patch Changes
- [#3922](https://github.com/MoonshotAI/kimi-code/pull/3922) [`9df7a9c`](https://github.com/MoonshotAI/kimi-code/commit/9df7a9ccfb0d7d0713b51ae1ac165d0634fc40d2) Thanks [@sailist](https://github.com/sailist)! - Fix new messages occasionally landing at an old position in the conversation after resuming a session.
- [#3911](https://github.com/MoonshotAI/kimi-code/pull/3911) [`88a7d93`](https://github.com/MoonshotAI/kimi-code/commit/88a7d932f1cda95c78bf5488d34ff974a160fb08) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix compaction failing after switching to a model with a smaller context window.
- [#3929](https://github.com/MoonshotAI/kimi-code/pull/3929) [`6ffdf0d`](https://github.com/MoonshotAI/kimi-code/commit/6ffdf0d579fa1a9f30c2d00c437c6c8de0c03997) Thanks [@7Hanrui](https://github.com/7Hanrui)! - The agent no longer assumes the current working directory is the project root.
- [#3933](https://github.com/MoonshotAI/kimi-code/pull/3933) [`9721259`](https://github.com/MoonshotAI/kimi-code/commit/97212596f7d694b9683046ec7aad4d1247d29468) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix a message sent while the agent was running sometimes appearing twice in the chat.
- [#3934](https://github.com/MoonshotAI/kimi-code/pull/3934) [`2e605b1`](https://github.com/MoonshotAI/kimi-code/commit/2e605b10be906fb627c1f3aeaaa066b14ddbdbba) Thanks [@liruifengv](https://github.com/liruifengv)! - web: Improved interactions and fixed known bugs.
## 2.0.1
### Patch Changes
- [#3878](https://github.com/MoonshotAI/kimi-code/pull/3878) [`f233f9d`](https://github.com/MoonshotAI/kimi-code/commit/f233f9de04d71c42d27df870b18e1bc1a6605f16) Thanks [@7Sageer](https://github.com/7Sageer)! - Stop listing unavailable media tools in subagent menus and clarify Read errors for images and videos.
- [#3762](https://github.com/MoonshotAI/kimi-code/pull/3762) [`c5ad17f`](https://github.com/MoonshotAI/kimi-code/commit/c5ad17f06aa12702d2b30dff63acba9ed08f688d) Thanks [@7Sageer](https://github.com/7Sageer)! - Providers can read their API key from a named environment variable (`api_key_env` in `config.toml` or the provider API).
- [#3896](https://github.com/MoonshotAI/kimi-code/pull/3896) [`73ebe9a`](https://github.com/MoonshotAI/kimi-code/commit/73ebe9ab2fbf9bf0ffc8fe5a5375060ce8e984b5) Thanks [@sailist](https://github.com/sailist)! - Keep follow-up messages sent during a turn from appearing twice after a session reload.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Compact the session index on startup when many entries point to deleted sessions.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Start the CLI faster by removing a duplicate skills directory scan.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Open the session list and resume sessions faster on workspaces with many sessions.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Resume long sessions faster.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Index sessions for search in capped, incremental background passes, tunable via the [database] searchSyncSessionCap and searchSyncDebounceMs settings.
- [#3864](https://github.com/MoonshotAI/kimi-code/pull/3864) [`25dd4ce`](https://github.com/MoonshotAI/kimi-code/commit/25dd4ce97345c7ebfd9c036898e5eef955c45ea8) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Rename the kimi install-app subcommand to kimi install-desktop; the old name keeps working as a hidden alias.
- [#3910](https://github.com/MoonshotAI/kimi-code/pull/3910) [`7dc253c`](https://github.com/MoonshotAI/kimi-code/commit/7dc253c5cefac7c277e5fc7071acf3e855fd503e) Thanks [@sailist](https://github.com/sailist)! - Restore thinking display for OpenAI-compatible providers that send a reasoning string next to an unrecognized reasoning_details array.
- [#3875](https://github.com/MoonshotAI/kimi-code/pull/3875) [`c1047a6`](https://github.com/MoonshotAI/kimi-code/commit/c1047a6bbe0e37d097e5da54d92dd3fb1b7a28da) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix `kimi -p` exiting early and cancelling the active turn when a cron task fires.
- [#3894](https://github.com/MoonshotAI/kimi-code/pull/3894) [`910aba2`](https://github.com/MoonshotAI/kimi-code/commit/910aba273e2bee3a8815666a3a9c4fff609f2366) Thanks [@7Sageer](https://github.com/7Sageer)! - Drop literal decision clauses from the system prompt and the auto permission mode reminder.
- [#3889](https://github.com/MoonshotAI/kimi-code/pull/3889) [`5108cad`](https://github.com/MoonshotAI/kimi-code/commit/5108cad9b65845ae992520dcf732cbbccc6a34b9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Restore sessions with large file-edit histories faster.
- [#3885](https://github.com/MoonshotAI/kimi-code/pull/3885) [`5acc863`](https://github.com/MoonshotAI/kimi-code/commit/5acc863ae60c092457bd9ecdde55266b5fad7040) Thanks [@starquakee](https://github.com/starquakee)! - select_tools now points out already-available tools to call directly and suggests announced names for near-miss selections.
- [#3887](https://github.com/MoonshotAI/kimi-code/pull/3887) [`e3f48a2`](https://github.com/MoonshotAI/kimi-code/commit/e3f48a225bbedbe167b30510e67d66ee67fcb0ec) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix deleting or archiving a session sometimes never finishing.
- [#3891](https://github.com/MoonshotAI/kimi-code/pull/3891) [`53e5e3f`](https://github.com/MoonshotAI/kimi-code/commit/53e5e3fca6bf377662bc4d61d220dde4ded8e7b7) Thanks [@sailist](https://github.com/sailist)! - Keep follow-up messages sent during a turn attached to that turn in the conversation history.
- [#3906](https://github.com/MoonshotAI/kimi-code/pull/3906) [`60f2a63`](https://github.com/MoonshotAI/kimi-code/commit/60f2a63278f28b77d9358cf589b2da6193bd2614) Thanks [@sailist](https://github.com/sailist)! - Keep a follow-up sent during a turn as one message, and undo the host turn instead of the follow-up alone.
- [#3879](https://github.com/MoonshotAI/kimi-code/pull/3879) [`1fddc16`](https://github.com/MoonshotAI/kimi-code/commit/1fddc16e3ea2de4c26a18acd764380adf9e2ed64) Thanks [@7Sageer](https://github.com/7Sageer)! - Remove the system-prompt rule that forbade all file access outside the working directory.
- [#3892](https://github.com/MoonshotAI/kimi-code/pull/3892) [`a80fe31`](https://github.com/MoonshotAI/kimi-code/commit/a80fe31cff0ebae686e60d7d5d0559ad2d2e843d) Thanks [@sailist](https://github.com/sailist)! - Stop workspace file watchers from scanning an unbounded project root, and add `[watch] enabled` / `KIMI_CODE_WATCH` to disable watching entirely.
- [#3869](https://github.com/MoonshotAI/kimi-code/pull/3869) [`86e0820`](https://github.com/MoonshotAI/kimi-code/commit/86e08208e3445171f6500c948234d7e3a0684324) Thanks [@sailist](https://github.com/sailist)! - Stop asking for approval of bash commands that cannot be statically analyzed in Ask When Needed permission mode.
## 2.0.0
### Major Changes
- [#3849](https://github.com/MoonshotAI/kimi-code/pull/3849) [`34ec5d2`](https://github.com/MoonshotAI/kimi-code/commit/34ec5d2a1700e540c39d2a1b6c905c9fa96af2db) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Add the /desktop slash command (alias /install-desktop) and the kimi install-app subcommand to open the Kimi Code desktop app page in the browser.
### Minor Changes
- [#3851](https://github.com/MoonshotAI/kimi-code/pull/3851) [`faec32c`](https://github.com/MoonshotAI/kimi-code/commit/faec32c7e7c1df5d27b7060f75a2a665efb59268) Thanks [@Grapedge](https://github.com/Grapedge)! - Render mermaid code blocks as diagrams in the terminal; turn it off under /settings → Mermaid diagrams, or set `mermaid = "off"` in the `[markdown]` section of tui.toml.
### Patch Changes
- [#3838](https://github.com/MoonshotAI/kimi-code/pull/3838) [`f4e5822`](https://github.com/MoonshotAI/kimi-code/commit/f4e5822164800ec138f77b0f323e2374963bfe19) Thanks [@sailist](https://github.com/sailist)! - Fix an occasional crash when a running turn is canceled.
- [#3844](https://github.com/MoonshotAI/kimi-code/pull/3844) [`0cf413f`](https://github.com/MoonshotAI/kimi-code/commit/0cf413f7759c314766ba30250a9875ac1af1185e) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix a steered message appearing twice after interrupting the turn.
- [#3848](https://github.com/MoonshotAI/kimi-code/pull/3848) [`b4d0b8a`](https://github.com/MoonshotAI/kimi-code/commit/b4d0b8a3889c1a6f0115c527341ccd2066bd180a) Thanks [@Grapedge](https://github.com/Grapedge)! - Fix @ file mentions duplicating or dropping the path when accepting a suggestion while still typing.
- [#3828](https://github.com/MoonshotAI/kimi-code/pull/3828) [`2bb6e12`](https://github.com/MoonshotAI/kimi-code/commit/2bb6e12c7e3ecc19521d21fcffc832075ed13200) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix duplicate user messages when steering an ongoing conversation.
- [#3832](https://github.com/MoonshotAI/kimi-code/pull/3832) [`1c7e996`](https://github.com/MoonshotAI/kimi-code/commit/1c7e996aa8bdd7a6f32447f1d89e43c0646b41bc) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix files attached while steering an ongoing conversation appearing only after a reload.
- [#3853](https://github.com/MoonshotAI/kimi-code/pull/3853) [`bd06178`](https://github.com/MoonshotAI/kimi-code/commit/bd06178913d2cc4ebd229ecee6714081105e5ae1) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Keep the original prompt visible when steering a running turn, instead of replacing it with the steered text.
- [#3832](https://github.com/MoonshotAI/kimi-code/pull/3832) [`1c7e996`](https://github.com/MoonshotAI/kimi-code/commit/1c7e996aa8bdd7a6f32447f1d89e43c0646b41bc) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix slash commands sent while steering an ongoing conversation missing from the transcript.
- [#3837](https://github.com/MoonshotAI/kimi-code/pull/3837) [`cafd9b5`](https://github.com/MoonshotAI/kimi-code/commit/cafd9b5a65509fe57332cbc0eb1fb19f0443fb0e) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Fix missing and reappearing chat messages after undoing a steered message.
- [#3818](https://github.com/MoonshotAI/kimi-code/pull/3818) [`19ce4b3`](https://github.com/MoonshotAI/kimi-code/commit/19ce4b3f0600c846e2f28828d8169829630fc666) Thanks [@liruifengv](https://github.com/liruifengv)! - Fix /usage showing a wrong error message when no session has been created yet.
- [#3784](https://github.com/MoonshotAI/kimi-code/pull/3784) [`5653c73`](https://github.com/MoonshotAI/kimi-code/commit/5653c739b9c0dbafd1e4b3d36edeb0fe44ec71f4) Thanks [@7Sageer](https://github.com/7Sageer)! - Images sent to Kimi models are uploaded as file references instead of inline data, and a warning is shown when media are dropped from a retried request.
- [#3778](https://github.com/MoonshotAI/kimi-code/pull/3778) [`7d174ac`](https://github.com/MoonshotAI/kimi-code/commit/7d174ac93352ccfc835d186ac7b8185d0194e6b5) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Report background subagents that time out or are stopped as cancelled instead of failed.
- [#3846](https://github.com/MoonshotAI/kimi-code/pull/3846) [`31f1b68`](https://github.com/MoonshotAI/kimi-code/commit/31f1b6824d1900aeea71b69c824c6354a0dd9019) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix OAuth login never triggering for MCP servers that allow anonymous tool discovery but reject tool calls with 401.
- [#3784](https://github.com/MoonshotAI/kimi-code/pull/3784) [`5653c73`](https://github.com/MoonshotAI/kimi-code/commit/5653c739b9c0dbafd1e4b3d36edeb0fe44ec71f4) Thanks [@7Sageer](https://github.com/7Sageer)! - When accumulated images and videos exceed the request size budget, the oldest media are omitted from requests with a warning instead of failing.
- [#3778](https://github.com/MoonshotAI/kimi-code/pull/3778) [`7d174ac`](https://github.com/MoonshotAI/kimi-code/commit/7d174ac93352ccfc835d186ac7b8185d0194e6b5) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Report tools and subagents interrupted by the user as cancelled instead of aborted or failed.
- [#3803](https://github.com/MoonshotAI/kimi-code/pull/3803) [`39a7455`](https://github.com/MoonshotAI/kimi-code/commit/39a74556a662a96293e9838663e91b31dbdc7a0b) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - The built-in browser plugin now appears as "Kimi Browser Extension" in the plugins panel, marketplace catalog, and docs, matching the product rename.
- [#3858](https://github.com/MoonshotAI/kimi-code/pull/3858) [`95d4a9d`](https://github.com/MoonshotAI/kimi-code/commit/95d4a9d0411b18f468345717c01c40f5ac996cdc) Thanks [@liruifengv](https://github.com/liruifengv)! - Sign the Windows CLI executable with a trusted publisher certificate.
- [#3795](https://github.com/MoonshotAI/kimi-code/pull/3795) [`a7bdabb`](https://github.com/MoonshotAI/kimi-code/commit/a7bdabbe82a089cd8631f6d613c252d96cbab639) Thanks [@Grapedge](https://github.com/Grapedge)! - Feedback surveys now appear at better times in long conversations.
- [#3840](https://github.com/MoonshotAI/kimi-code/pull/3840) [`9c5e9b4`](https://github.com/MoonshotAI/kimi-code/commit/9c5e9b48634be1dfae921fb48f9afc3a23ffd1ad) Thanks [@chengluyu](https://github.com/chengluyu)! - Fix a Windows crash when a project or config folder is opened through a short 8.3 path.
## 0.43.1
### Patch Changes
- [#3780](https://github.com/MoonshotAI/kimi-code/pull/3780) [`486dcd2`](https://github.com/MoonshotAI/kimi-code/commit/486dcd26c76f2854fc351515a45d8f0fc2d31ed6) Thanks [@Grapedge](https://github.com/Grapedge)! - Update the terminal UI engine, fixing link colors in wrapped markdown tables and `@` file-completion ordering, and adding native clipboard support on Linux X11.
- [#3776](https://github.com/MoonshotAI/kimi-code/pull/3776) [`48fad6e`](https://github.com/MoonshotAI/kimi-code/commit/48fad6e7d9cd00511c6118e84ae6d22dafb96be9) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix memory not being released when subagent scopes are disposed.
- [#3777](https://github.com/MoonshotAI/kimi-code/pull/3777) [`dc76b0c`](https://github.com/MoonshotAI/kimi-code/commit/dc76b0caaa6e0aa406bf87957e6b5f8aca722221) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix progressively slower rendering on each round of large agent swarm runs.
- [#3631](https://github.com/MoonshotAI/kimi-code/pull/3631) [`1336be3`](https://github.com/MoonshotAI/kimi-code/commit/1336be38777959cc558fed2b51dc53caa07da5db) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Stop returning deleted sessions from global search before the search index catches up.
- [#3775](https://github.com/MoonshotAI/kimi-code/pull/3775) [`f248624`](https://github.com/MoonshotAI/kimi-code/commit/f2486241fb5e268d7a43cb589b5c2d59af8db571) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Reduce event-loop stalls and GC churn in sessions with many concurrent subagents.
- [#3779](https://github.com/MoonshotAI/kimi-code/pull/3779) [`82ec469`](https://github.com/MoonshotAI/kimi-code/commit/82ec469e47eed52feb664fd361e3d8dcb774596e) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix pressing Ctrl+C while subagents are running exiting the whole CLI instead of just interrupting the subagents.
- [#3749](https://github.com/MoonshotAI/kimi-code/pull/3749) [`6126472`](https://github.com/MoonshotAI/kimi-code/commit/6126472c7af421070f09c470c5750455dd277657) Thanks [@liruifengv](https://github.com/liruifengv)! - web: AI session titles are now always on — a title is generated after the first turn and can be regenerated from the rename field, with no experimental flag required.
- [#3670](https://github.com/MoonshotAI/kimi-code/pull/3670) [`a9efbe0`](https://github.com/MoonshotAI/kimi-code/commit/a9efbe053f966104d4b51090118da5135c084e22) Thanks [@Grapedge](https://github.com/Grapedge)! - Delete sessions from the session picker: press Ctrl+X on a session, then y to confirm.
### Patch Changes
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: add user agreement and privacy policy entries to the Settings → About page.
- [#3750](https://github.com/MoonshotAI/kimi-code/pull/3750) [`775a6c3`](https://github.com/MoonshotAI/kimi-code/commit/775a6c35de40e6dd8cc399cc848f95818733ee7c) Thanks [@7Sageer](https://github.com/7Sageer)! - Add the `loop_control.compaction_max_attempts` config option to set the maximum total attempts for a failing compaction request (default 5).
- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Add the dynamically_loaded_tools capability to official Kimi Code models when the service declares support for message-level tool declarations.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix sessions containing many brackets or backslashes getting stuck while loading.
- [#3734](https://github.com/MoonshotAI/kimi-code/pull/3734) [`ee2cac1`](https://github.com/MoonshotAI/kimi-code/commit/ee2cac102b835fcd7adb3d4b9bc3d62b0b71cdfd) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Fix a crash that killed the process when a retried LLM request had streamed a partial tool call before disconnecting.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix persistent UI stuttering while streaming in long sessions.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix certain formulas, links, tool results, and log contents causing the UI to stutter or become unresponsive.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the send button staying disabled when starting a new session.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix previous sessions being wrongly marked as unread and triggering "turn complete" notifications after starting a new conversation.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: render Markdown frontmatter metadata as key-value cards and tag lists.
- [#3688](https://github.com/MoonshotAI/kimi-code/pull/3688) [`9f7e68e`](https://github.com/MoonshotAI/kimi-code/commit/9f7e68e8bdb70d3e5cde5a924740e357ede2fc40) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve MCP attachments that cannot be delivered directly to the model.
- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Add a per-server `deferred` field to MCP server configuration: when the model supports dynamic tool loading (experimental `tool-select` flag), set `deferred: true` to keep a server's tools out of the top-level tool list and load them on demand via `select_tools`; servers are exposed inline by default.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: drop the Changes entry from the panel new-tab menu while a diff tab is open.
- [#3657](https://github.com/MoonshotAI/kimi-code/pull/3657) [`5b3b5b6`](https://github.com/MoonshotAI/kimi-code/commit/5b3b5b6f7cfa4e9cca2a394632d52f8748dc8190) Thanks [@chengluyu](https://github.com/chengluyu)! - Exclude time spent with the session closed from goal time budgets.
- [#3728](https://github.com/MoonshotAI/kimi-code/pull/3728) [`b180725`](https://github.com/MoonshotAI/kimi-code/commit/b1807253c34e12b0ecf60c9b4da3890d0c80ce72) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Add the `KIMI_CODE_PERMISSION_MODE_REMINDER` environment variable: set it to `0` to stop injecting the auto permission-mode reminders into the model context.
- [#3696](https://github.com/MoonshotAI/kimi-code/pull/3696) [`d3dc594`](https://github.com/MoonshotAI/kimi-code/commit/d3dc5945548d3353a5e6cfc457a3a8cf84d7da71) Thanks [@sailist](https://github.com/sailist)! - Include the server token in the Remote Control Local UI link so it opens already signed in.
- [#3709](https://github.com/MoonshotAI/kimi-code/pull/3709) [`306f6f6`](https://github.com/MoonshotAI/kimi-code/commit/306f6f69251d65fda434463433c0761758eacb16) Thanks [@sailist](https://github.com/sailist)! - Fix Remote Control uploads larger than ~3.5MB always failing with a 400 error.
- [#3718](https://github.com/MoonshotAI/kimi-code/pull/3718) [`ab9e688`](https://github.com/MoonshotAI/kimi-code/commit/ab9e688cd0c753bd3cea3406a9e9194c73ab2239) Thanks [@sailist](https://github.com/sailist)! - Reuse unchanged Remote Control assets across page loads instead of retransferring them.
- [#3657](https://github.com/MoonshotAI/kimi-code/pull/3657) [`5b3b5b6`](https://github.com/MoonshotAI/kimi-code/commit/5b3b5b6f7cfa4e9cca2a394632d52f8748dc8190) Thanks [@chengluyu](https://github.com/chengluyu)! - Remove the 24-hour limit on goal time budgets.
- [#3714](https://github.com/MoonshotAI/kimi-code/pull/3714) [`565093f`](https://github.com/MoonshotAI/kimi-code/commit/565093f727eee07e2aa05f43859fa12b9e977989) Thanks [@sailist](https://github.com/sailist)! - Skip the confirmation prompt for rm -rf commands that target only /tmp or /temp paths.
- [#3667](https://github.com/MoonshotAI/kimi-code/pull/3667) [`9296e68`](https://github.com/MoonshotAI/kimi-code/commit/9296e6877032328fcd9cf1d145f9286bec140897) Thanks [@sailist](https://github.com/sailist)! - Fix the select_tools tool never being registered because agent profiles do not list it in their tool allowlists.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: restructure the Settings pages — Account moves right after General, Agent is renamed to Agents & Sessions and now includes message folding, Advanced is renamed to About, and data & privacy settings move into General.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the selected segment background flashing and shifting while the Settings segmented control loads.
- [#3697](https://github.com/MoonshotAI/kimi-code/pull/3697) [`5e452fc`](https://github.com/MoonshotAI/kimi-code/commit/5e452fc2ddf5a0d9feedfbf3c7cdde4246fc44b6) Thanks [@RealKai42](https://github.com/RealKai42)! - Allow steering messages to interrupt waits for background tasks.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the switch thumb deforming at both ends when stretched on hover.
- [#3763](https://github.com/MoonshotAI/kimi-code/pull/3763) [`dd6a411`](https://github.com/MoonshotAI/kimi-code/commit/dd6a4116ddfcc56cb3d37b0c1d526d64cb36086b) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix tooltips popping up even when the mouse has not moved.
- [#3648](https://github.com/MoonshotAI/kimi-code/pull/3648) [`2da4aa2`](https://github.com/MoonshotAI/kimi-code/commit/2da4aa23b0d484312cc068b2d4b2a62e694a2bfe) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode reliability fixes across messaging, worktrees, and the review-to-merge gate.
- [#3707](https://github.com/MoonshotAI/kimi-code/pull/3707) [`e409bc8`](https://github.com/MoonshotAI/kimi-code/commit/e409bc8ba582c9f390146eca08812d22790c81f8) Thanks [@sailist](https://github.com/sailist)! - Compress Remote Control tunnel responses with gzip.
- [#3702](https://github.com/MoonshotAI/kimi-code/pull/3702) [`42998cf`](https://github.com/MoonshotAI/kimi-code/commit/42998cfc13cb03914256587f0b702ec95e2d156c) Thanks [@sailist](https://github.com/sailist)! - Add `-y, --yes` to `kimi upgrade` (alias `kimi update`) to skip the confirmation prompt and install the update directly.
- [#3681](https://github.com/MoonshotAI/kimi-code/pull/3681) [`1306a9a`](https://github.com/MoonshotAI/kimi-code/commit/1306a9a8dbd5acd9532182c36edcabbc7e8de807) Thanks [@7Sageer](https://github.com/7Sageer)! - Warn at startup when a [models] entry in config.toml is missing the model field and cannot be used.
## 0.42.0
### Minor Changes
- [#3613](https://github.com/MoonshotAI/kimi-code/pull/3613) [`d4d20d2`](https://github.com/MoonshotAI/kimi-code/commit/d4d20d21d733a7942056c15ffcc2c74cce51f7d5) Thanks [@liukx0205](https://github.com/liukx0205)! - Add read-only tools to the /btw side agent.
- [#3524](https://github.com/MoonshotAI/kimi-code/pull/3524) [`f6a9c39`](https://github.com/MoonshotAI/kimi-code/commit/f6a9c39e22a63b2a231684ed8426bfe4ec406cf7) Thanks [@RealKai42](https://github.com/RealKai42)! - Add an experimental Updates panel with paginated progress messages from the main agent and subagents; enable it with `KIMI_CODE_EXPERIMENTAL_NOTIFY_USER=1`.
- [#3634](https://github.com/MoonshotAI/kimi-code/pull/3634) [`e831fd1`](https://github.com/MoonshotAI/kimi-code/commit/e831fd1ea9488ad5192bcc9d96579470cf0c4442) Thanks [@7Sageer](https://github.com/7Sageer)! - The subagent model pool (`[secondary_model]`) is now always on; the experimental secondary-model flag and the `KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL` opt-out have been removed.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: support permanently deleting sessions from the session row context menu, with a confirmation prompt.
- [#3526](https://github.com/MoonshotAI/kimi-code/pull/3526) [`55685c5`](https://github.com/MoonshotAI/kimi-code/commit/55685c58b5dbff692e16c66efa65fa6d32222f61) Thanks [@RealKai42](https://github.com/RealKai42)! - Stop reminding the model of its context budget before automatic compaction.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: preview images and videos in a reorderable media rail in the composer, mention them in the text on demand, and keep the previews after queueing and sending.
### Patch Changes
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix the conversation scrollbar being too thin to click and drag easily.
- [#3539](https://github.com/MoonshotAI/kimi-code/pull/3539) [`34ad513`](https://github.com/MoonshotAI/kimi-code/commit/34ad5137ce36f6eb03c4a42145e9bdcc8d607d87) Thanks [@RealKai42](https://github.com/RealKai42)! - Collapse finished tool calls in the transcript to a header plus one marked outcome row: short output is shown whole, hidden output is counted (`N more lines`, `+N more`) and revealed by `Ctrl+O`, which the footer advertises while it is available.
- [#3537](https://github.com/MoonshotAI/kimi-code/pull/3537) [`f12d59e`](https://github.com/MoonshotAI/kimi-code/commit/f12d59e089e2531a33fbca30b26ffeabd5862b45) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix the agent resuming the wrong request after automatic context compaction in long sessions.
- [#3552](https://github.com/MoonshotAI/kimi-code/pull/3552) [`f0434f2`](https://github.com/MoonshotAI/kimi-code/commit/f0434f2d25ead11d44f5616930157a59dfd524a5) Thanks [@sailist](https://github.com/sailist)! - The minidb session-index read model and global search worker are now always on; the experimental flags have been replaced by the `[database]` config section and the `KIMI_CODE_PERSISTENCE_MINIDB_READMODEL` / `KIMI_CODE_SEARCH_WORKER` env vars.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix background task notifications lingering at the bottom of the session during subsequent tool calls.
- [#3658](https://github.com/MoonshotAI/kimi-code/pull/3658) [`ff7371b`](https://github.com/MoonshotAI/kimi-code/commit/ff7371b70a7ae80ec70712b610f73e6d6f29edc0) Thanks [@RealKai42](https://github.com/RealKai42)! - Allow file searches to retrieve matches beyond the first 100 results.
- [#3652](https://github.com/MoonshotAI/kimi-code/pull/3652) [`7f5debf`](https://github.com/MoonshotAI/kimi-code/commit/7f5debfa71ac9e4a23b5dab1a511aa3672677381) Thanks [@RealKai42](https://github.com/RealKai42)! - Accept HEIC, HEIF, and BMP images on a session's first prompt when the configured default model is served by Kimi and no model has been selected yet.
- [#3649](https://github.com/MoonshotAI/kimi-code/pull/3649) [`80480c0`](https://github.com/MoonshotAI/kimi-code/commit/80480c01d27a921d4660ad043e78c8b61167f82b) Thanks [@RealKai42](https://github.com/RealKai42)! - Accept HEIC, HEIF, and BMP images in ReadMediaFile and prompt attachments when the model is served by Kimi.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix page jank caused by hundreds of simultaneous requests when reloading a session with many background tasks.
- [#3654](https://github.com/MoonshotAI/kimi-code/pull/3654) [`6de0cec`](https://github.com/MoonshotAI/kimi-code/commit/6de0cec174ae3af44fc9f9f74b2aa8599b928fdf) Thanks [@RealKai42](https://github.com/RealKai42)! - Preserve distinct structured data in MCP tool results.
- [#3669](https://github.com/MoonshotAI/kimi-code/pull/3669) [`f8c606e`](https://github.com/MoonshotAI/kimi-code/commit/f8c606e7d7b33a43190721d47ec6c8a2076eaec6) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Support compressed downloads from updated native release manifests.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: reduce jank when opening and scrolling back through long conversations, while preserving message and tool expansion state.
- [#3548](https://github.com/MoonshotAI/kimi-code/pull/3548) [`baf17a8`](https://github.com/MoonshotAI/kimi-code/commit/baf17a8fcc289f20fa6c8d85dd8f93eeb3ff0cbc) Thanks [@chengluyu](https://github.com/chengluyu)! - Preserve image and video filenames in session history.
- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Add configurable character limits and resumable long-line file reads without repeated output truncation.
- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Read malformed UTF-16 files with an explicit lossy-decoding warning.
- [#3645](https://github.com/MoonshotAI/kimi-code/pull/3645) [`5000f98`](https://github.com/MoonshotAI/kimi-code/commit/5000f981c3ef8b59560f26fd01a20fbfe030d81e) Thanks [@RealKai42](https://github.com/RealKai42)! - Avoid repeated scanning for common tail reads and report file changes detected during tail rereads.
- [#3616](https://github.com/MoonshotAI/kimi-code/pull/3616) [`260ac3f`](https://github.com/MoonshotAI/kimi-code/commit/260ac3faad1cf0ca84dca26173ddf66a09220841) Thanks [@Grapedge](https://github.com/Grapedge)! - Upgrade the default thinking effort to the recommended level for eligible users.
- [#3552](https://github.com/MoonshotAI/kimi-code/pull/3552) [`f0434f2`](https://github.com/MoonshotAI/kimi-code/commit/f0434f2d25ead11d44f5616930157a59dfd524a5) Thanks [@sailist](https://github.com/sailist)! - Remote Control is now always on; the experimental `KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL` flag has been removed.
- [#3618](https://github.com/MoonshotAI/kimi-code/pull/3618) [`75682b0`](https://github.com/MoonshotAI/kimi-code/commit/75682b0ef130e0340a9d16421312e2766c327be1) Thanks [@sailist](https://github.com/sailist)! - Fix recent sessions missing from the session list when the sessions directory contains stray files.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix skills created mid-session not appearing in the slash list until the app restarts.
- [#3671](https://github.com/MoonshotAI/kimi-code/pull/3671) [`e6bc8b8`](https://github.com/MoonshotAI/kimi-code/commit/e6bc8b8ad90def151f17e8e1d4b03b408d9f69ea) Thanks [@liruifengv](https://github.com/liruifengv)! - web: fix trailing backticks briefly flashing at the end of code blocks while they stream.
- [#3596](https://github.com/MoonshotAI/kimi-code/pull/3596) [`0d7833e`](https://github.com/MoonshotAI/kimi-code/commit/0d7833ee8019d39161f8462977a3a2713bbfd39e) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - The /tasks panel now shows each background agent's model under its task row.
- [#3607](https://github.com/MoonshotAI/kimi-code/pull/3607) [`5eea890`](https://github.com/MoonshotAI/kimi-code/commit/5eea89016511d113480755907619defafaf025cf) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix the streaming debug timing attributing client-side busy time to the server.
- [#3596](https://github.com/MoonshotAI/kimi-code/pull/3596) [`0d7833e`](https://github.com/MoonshotAI/kimi-code/commit/0d7833ee8019d39161f8462977a3a2713bbfd39e) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower worker and reviewer briefings now carry the full mission context, and tower agent timeouts follow the subagent timeout setting (`[subagent] timeout_ms` or `KIMI_SUBAGENT_TIMEOUT_MS`), defaulting to 2 hours. Fix the /tasks list not showing the model for tower-spawned agents.
- [#3593](https://github.com/MoonshotAI/kimi-code/pull/3593) [`00cfbb0`](https://github.com/MoonshotAI/kimi-code/commit/00cfbb0547cee6ec2650fd3fc52e15d2072c41aa) Thanks [@7Sageer](https://github.com/7Sageer)! - Print a warning in `kimi -p` when project-level MCP servers are skipped because the folder is not trusted.
- [#3608](https://github.com/MoonshotAI/kimi-code/pull/3608) [`fb0353a`](https://github.com/MoonshotAI/kimi-code/commit/fb0353a8ba5ceb7e8ae4e27f3260b3c8c8d80784) Thanks [@liukx0205](https://github.com/liukx0205)! - Watch the user-level skill roots (`~/.kimi-code/skills` and `~/.agents/skills`) so the workspace skill catalog refreshes automatically when skills are created, modified, or deleted while the daemon is running — no restart or manual reload needed.
- [#3560](https://github.com/MoonshotAI/kimi-code/pull/3560) [`af81bb9`](https://github.com/MoonshotAI/kimi-code/commit/af81bb92215dca2f933579ce0119f7add452bc96) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Open the browser on localhost instead of the wildcard bind address for `kimi web --host 0.0.0.0`.
- [#3605](https://github.com/MoonshotAI/kimi-code/pull/3605) [`f1e9152`](https://github.com/MoonshotAI/kimi-code/commit/f1e915277655c68213cf91fe5010413bfde3cb65) Thanks [@huangzheng2016](https://github.com/huangzheng2016)! - Fix slow response streaming and rendering after resuming sessions with many scheduled cron turns.
## 0.41.0
### Minor Changes
- [#3423](https://github.com/MoonshotAI/kimi-code/pull/3423) [`b199e33`](https://github.com/MoonshotAI/kimi-code/commit/b199e3326de28d5094ca5f18816a74ab336c51e0) Thanks [@RealKai42](https://github.com/RealKai42)! - Remind the model of its context budget before automatic compaction, and after compaction point it at the session's event log for exact details.
- [#3525](https://github.com/MoonshotAI/kimi-code/pull/3525) [`eba23ed`](https://github.com/MoonshotAI/kimi-code/commit/eba23edb93ec00aa11ffa288e2b89ebbf3d77a70) Thanks [@wbxl2000](https://github.com/wbxl2000)! - Turn-level file history is now always on; the experimental file-history flag has been removed.
- [#3516](https://github.com/MoonshotAI/kimi-code/pull/3516) [`6013658`](https://github.com/MoonshotAI/kimi-code/commit/60136588a4218bc2423b93467c4fae8c6554219c) Thanks [@Grapedge](https://github.com/Grapedge)! - Add an occasional session rating prompt above the input box.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: add tower multi-agent collaboration mode (experimental), enabled via the /tower command or the composer plus menu; /tower supports specifying a base branch (e.g. /tower add-new-feature).
- [#3529](https://github.com/MoonshotAI/kimi-code/pull/3529) [`b184b31`](https://github.com/MoonshotAI/kimi-code/commit/b184b31497949ac019ac43d5a3dfe2ab87dbebc4) Thanks [@sailist](https://github.com/sailist)! - Stop blocking dangerous commands and commands that cannot be statically analyzed in auto permission mode.
- [#3522](https://github.com/MoonshotAI/kimi-code/pull/3522) [`523d35b`](https://github.com/MoonshotAI/kimi-code/commit/523d35b54b25a0b4589388a2b6c8c4261f1ef7db) Thanks [@RealKai42](https://github.com/RealKai42)! - Deliver background question answers to the agent directly instead of via a saved output file.
- [#3522](https://github.com/MoonshotAI/kimi-code/pull/3522) [`523d35b`](https://github.com/MoonshotAI/kimi-code/commit/523d35b54b25a0b4589388a2b6c8c4261f1ef7db) Thanks [@RealKai42](https://github.com/RealKai42)! - Fix background questions being cancelled as soon as the agent finishes its turn.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: polish the style and interaction of Bash commands in the right-side panel.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: support selection comments and quote-to-chat in the diff and per-turn changes panels.
- [#3459](https://github.com/MoonshotAI/kimi-code/pull/3459) [`b6b9b37`](https://github.com/MoonshotAI/kimi-code/commit/b6b9b374dd5a3cda4257081b194760d7998e73f0) Thanks [@RealKai42](https://github.com/RealKai42)! - Subagent final messages are no longer bounced back for expansion when they are under 200 characters.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix file change previews showing added/removed lines that never existed when the same file is edited multiple times in one turn.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: pressing Esc no longer closes the right detail panel.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix the default thinking effort in settings not being settable to the highest level (Max).
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: rename the three permission modes to Always Ask / Ask When Needed / Never Ask and update their Chinese and English descriptions.
- [#3473](https://github.com/MoonshotAI/kimi-code/pull/3473) [`d567a6a`](https://github.com/MoonshotAI/kimi-code/commit/d567a6a4fdaeb052deae9d9b313df0d922105799) Thanks [@7Sageer](https://github.com/7Sageer)! - Show a warning after switching to Ask When Needed or Never Ask mode.
- [#3531](https://github.com/MoonshotAI/kimi-code/pull/3531) [`51bd52a`](https://github.com/MoonshotAI/kimi-code/commit/51bd52a589089f99a941e0d3285b0cf638e5b1a0) Thanks [@7Sageer](https://github.com/7Sageer)! - Fix print mode (`kimi -p`) losing session records when the run exits on an error or a termination signal.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: restyle selection quote pills from blue to the same neutral ink as other mentions, with a vertical bar separating the quote and comment.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: shorten selection quote pill excerpts to at most 12 characters.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: restore selection quoting — after selecting text in a message or file preview, you can add a comment or quote it into the chat.
- [#3478](https://github.com/MoonshotAI/kimi-code/pull/3478) [`052e98e`](https://github.com/MoonshotAI/kimi-code/commit/052e98ec17ac8d931873ef892fb0e1912aa401e0) Thanks [@RealKai42](https://github.com/RealKai42)! - Resuming a subagent by its agent id now works after the session is reopened in a new process; the resumed subagent follows the current permission mode and is matched by its own profile in permission rules.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: selection comment bubbles near the bottom of the page now pop above the selection and grow upward, eliminating double scrollbars.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix selection bubbles displayed over the panel header being unclickable and lacking a hover state.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix the comment bubble popping up before mouse release when selecting text with a slow drag.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: rework selection comment bubble actions into right-aligned Cancel / Add to chat below the input, with an Enter hint on the confirm button.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: revamp the selection comment bubble — the comment box supports multi-line input and auto-grows, and the confirm button now matches the chat composer send button.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: support selection quoting in the terminal — select text to add a comment or quote it into the chat.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: lengthen the hover delay of quote and mention preview cards so merely passing the mouse over them no longer triggers them.
- [#3461](https://github.com/MoonshotAI/kimi-code/pull/3461) [`8057d30`](https://github.com/MoonshotAI/kimi-code/commit/8057d30afbd2942f7c2e647f33a9dc7401af2b43) Thanks [@tpoisonooo](https://github.com/tpoisonooo)! - Tower mode (experimental, `KIMI_CODE_EXPERIMENTAL_TOWER=1`): fix tower mode never starting when enabled through `[experimental] tower = true` in `config.toml` instead of the environment variable. When tower mode cannot be enabled, the error now names the actual blocker — the disabled experiment, a required restart, or the owning session. When another live session owns the workspace tower, the message also names the owning session's title alongside its id. /tower now also works in a directory that is not a git repository — it runs git init and commits what is there (an empty initial commit for empty directories).
- [#3521](https://github.com/MoonshotAI/kimi-code/pull/3521) [`744b718`](https://github.com/MoonshotAI/kimi-code/commit/744b718b672199f407786f3b5f789fb3caf4b2a7) Thanks [@kimi-agent-bot](https://github.com/kimi-agent-bot)! - Remove the /dance Easter egg hint from the TUI tips rotation.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: fix inaccurate added/removed line counts in per-turn file change summaries after the same file is edited or overwritten multiple times.
- [#3549](https://github.com/MoonshotAI/kimi-code/pull/3549) [`29e1875`](https://github.com/MoonshotAI/kimi-code/commit/29e1875919a6b2a734d6ae1c0d8694a98dd80933) Thanks [@wbxl2000](https://github.com/wbxl2000)! - web: per-turn file change cards now show only exact line statistics, and the card no longer appears when statistics are unavailable.
import{casO,wasI,aasJ,fasP,basE,sasA}from"./chunk-RYQCIY6F-DRHEFBXE.js";import{_asw,amasv,anasD,aoasH,apasY,l,cas_,aqasW,aras$,agasj,asasq,ahasR,afasF,atasz,auasK,avasG}from"./mermaid.core-CqiFQExc.js";import{GasQ}from"./graph-DOmOIIwC.js";import{lasU}from"./layout-D-LzfAck.js";import"./map-DxJ2ADlA.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";varC=w((s,t,g)=>Math.max(t,Math.min(g,s)),"clamp"),B=w((s="TB")=>{switch(s){case"BT":return"bottom";case"LR":return"right";case"RL":return"left";case"TB":default:return"top"}},"getDefaultSelfLoopSide"),V=w(s=>s==="flowchart"||s==="flowchart-v2"||s==="stateDiagram","shouldMergeSelfLoopSegments"),Z=w((s,t,g,m,c)=>{consto=[],r=newSet;if(g.forEach(({start:i,end:n})=>{i!==m&&r.add(i),n!==m&&r.add(n)}),r.forEach(i=>{constn=s.node(i);typeofn?.x=="number"&&typeofn?.y=="number"&&o.push(n)}),o.length===0&&g.forEach(({edge:i})=>{(i.points??[]).forEach(n=>{typeofn?.x=="number"&&typeofn?.y=="number"&&o.push(n)})}),o.length===0)returnB(c);constf=o.reduce((i,n)=>({x:i.x+n.x/o.length,y:i.y+n.y/o.length}),{x:0,y:0}),h=f.x-t.x,a=f.y-t.y;returnMath.abs(h)>Math.abs(a)?h>0?"right":"left":Math.abs(a)>0?a>0?"bottom":"top":B(c)},"getSelfLoopSide"),ee=w((s,t="top",g=0,m=0)=>{constc=s.x,o=s.y-g,r=s.width/2,f=s.height/2,h=Math.max(36,Math.min(100,s.width*.8)),a=C(Math.max(m,s.width*.35),36,h),i=C(Math.min(s.width,s.height)*.45,24,48);switch(t){case"bottom":{constn=o+f;return[{x:c-a/2,y:n},{x:c-a/2,y:n+i},{x:c+a/2,y:n+i},{x:c+a/2,y:n}]}case"right":{constn=c+r;return[{x:n,y:o-a/2},{x:n+i,y:o-a/2},{x:n+i,y:o+a/2},{x:n,y:o+a/2}]}case"left":{constn=c-r;return[{x:n,y:o-a/2},{x:n-i,y:o-a/2},{x:n-i,y:o+a/2},{x:n,y:o+a/2}]}case"top":default:{constn=o-f;return[{x:c-a/2,y:n},{x:c-a/2,y:n-i},{x:c+a/2,y:n-i},{x:c+a/2,y:n}]}}},"getSelfLoopPoints"),te=w((s,t,g="top",m=0,c={})=>{constr=s.x,f=s.y-m,h=c.width??0,a=c.height??0;switch(g){case"bottom":return{x:r,y:Math.max(...t.map(i=>i.y))+a/2+4};case"right":return{x:Math.max(...t.map(i=>i.x))+h/2+4,y:f};case"left":return{x:Math.min(...t.map(i=>i.x))-h/2-4,y:f};case"top":default:return{x:r,y:Math.min(...t.map(i=>i.y))-a/2-4}}},"getSelfLoopLabelPosition"),ne=w((s,t=0,{mergeSelfLoops:g=!0}={})=>{constm=newMap,c=[],o=s.graph()?.rankdir;returns.edges().forEach(r=>{constf=s.edge(r);if(g&&f.selfLoop){consth=f.selfLoop.id;m.has(h)||m.set(h,[]),m.get(h).push({edge:f,start:r.v,end:r.w})}elsec.push({edge:f,start:r.v,end:r.w})}),m.forEach(r=>{if(r.length!==3){r.forEach(L=>c.push(L));return}r.sort((L,d)=>L.edge.selfLoop.order-d.edge.selfLoop.order);const[f,h,a]=r,i=f.edge.originalEdge??h.edge.originalEdge??a.edge.originalEdge??h.edge,n=s.node(i.start);if(!n){r.forEach(L=>c.push(L));return}constp={width:h.edge.width,height:h.edge.height},y=Z(s,n,r,i.start,o),X=ee(n,y,t,p.width??0),S=te(n,X,y,t,p),b={...h.edge,...i,id:i.id,points:X,start:i.start,end:i.end,x:S.x,y:S.y,width:p.width,height:p.height,labelStyle:h.edge.labelStyle,fromCluster:f.edge.fromCluster??h.edge.fromCluster??a.edge.fromCluster,toCluster:f.edge.toCluster??h.edge.toCluster??a.edge.toCluster};deleteb.selfLoop,deleteb.originalEdge,c.push({edge:b,start:b.start,end:b.end})}),c},"getEdgesToRender"),T=w(async(s,t,g,m,c,o)=>{l.warn("Graph in recursive render:XAX",I(t),c);constr=t.graph().rankdir;l.trace("Dir in recursive render - dir:",r);constf=s.insert("g").attr("class","root");t.nodes()?l.info("Recursive render XXX",t.nodes()):l.info("No nodes found for",t),t.edges().length>0&&l.info("Recursive edges",t.edge(t.edges()[0]));consth=f.insert("g").attr("class","clusters"),a=f.insert("g").attr("class","edgePaths"),i=f.insert("g").attr("class","edgeLabels"),n=f.insert("g").attr("class","nodes"),p=V(g);awaitPromise.all(t.nodes().map(asyncfunction(d){conste=t.node(d);if(c!==void0){constu=JSON.parse(JSON.stringify(c.clusterData));l.trace(`Setting data for parent cluster XXX
Node.id=`,d,`
data=`,u.height,`
Parentcluster`,c.height),t.setNode(c.id,u),t.parent(d)||(l.trace("Setting parent",d,c.id),t.setParent(d,c.id,u))}if(l.info("(Insert) Node XXX"+d+": "+JSON.stringify(t.node(d))),e?.clusterNode){l.info("Cluster identified XBX",d,e.width,t.node(d));const{ranksep:u,nodesep:x}=t.graph();e.graph.setGraph({...e.graph.graph(),ranksep:u+25,nodesep:x});const N=await T(n,e.graph,g,m,t.node(d),o),M=N.elem;W(e,M),e.diff=N.diff||0,l.info("New compound node after recursive render XAX",d,"width",e.width,"height",e.height),$(M,e)}else t.children(d).length>0?(l.trace("Cluster - the non recursive path XBX",d,e.id,e,e.width,"Graph:",t),l.trace(P(e.id,t)),E.set(e.id,{id:P(e.id,t),node:e})):(l.trace("Node - the non recursive path XAX",d,n,t.node(d),r),await j(n,t.node(d),{config:o,dir:r}))})),await w(async()=>{const d=t.edges().map(async function(e){const u=t.edge(e.v,e.w,e.name);if(l.info("Edge "+e.v+" -> "+e.w+": "+JSON.stringify(e)),l.info("Edge "+e.v+" -> "+e.w+": ",e," ",JSON.stringify(t.edge(e))),l.info("Fix",E,"ids:",e.v,e.w,"Translating: ",E.get(e.v),E.get(e.w)),p&&u.selfLoop){if(u.selfLoop.order!==1)return;const x=u.id;u.id=u.selfLoop.id,await G(i,u),u.id=x;return}await G(i,u)});await Promise.all(d)},"processEdges")(),l.info("Graph before layout:",JSON.stringify(I(t))),l.info("############################################# XXX"),l.info("### Layout ### XXX"),l.info("############################################# XXX"),U(t),l.info("Graph after layout:",JSON.stringify(I(t)));let X=0,{subGraphTitleTotalMargin:S}=q(o);await Promise.all(A(t).map(async function(d){const e=t.node(d);if(l.info("Position XBX => "+d+": ("+e.x,","+e.y,") width: ",e.width," height: ",e.height),e?.clusterNode)e.y+=S,l.info("A tainted cluster node XBX1",d,e.id,e.width,e.height,e.x,e.y,t.parent(d)),E.get(e.id).node=e,R(e);else if(t.children(d).length>0){l.info("A pure cluster node XBX1",d,e.id,e.x,e.y,e.width,e.height,t.parent(d)),e.height+=S,t.node(e.parentId);const u=e?.padding/2||0,x=e?.labelBBox?.height||0,N=x-u||0;l.debug("OffsetY",N,"labelHeight",x,"halfPadding",u),await F(h,e),E.get(e.id).node=e}else{const u=t.node(e.parentId);e.y+=S/2,l.info("A regular node XBX1 - using the padding",e.id,"parent",e.parentId,e.width,e.height,e.x,e.y,"offsetY",e.offsetY,"parent",u,u?.offsetY,e),R(e)}}));const b=S/2;return ne(t,b,{mergeSelfLoops:p}).forEach(function({edge:d,start:e,end:u}){l.info("Edge "+e+" -> "+u+": "+JSON.stringify(d),d),d.points.forEach(k=>k.y+=b);const x=t.node(e),N=t.node(u),M=z(a,d,E,g,x,N,m);K(d,M)}),t.nodes().forEach(function(d){const e=t.node(d);l.info(d,e.type,e.diff),e.isGroup&&(X=e.diff)}),l.warn("Returning from recursive render XAX",f,X),{elem:f,diff:X}},"recursiveRender"),le=w(async(s,t)=>{const g=new Q({multigraph:!0,compound:!0}).setGraph({rankdir:s.direction,nodesep:s.config?.nodeSpacing||s.config?.flowchart?.nodeSpacing||s.nodeSpacing,ranksep:s.config?.rankSpacing||s.config?.flowchart?.rankSpacing||s.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}}),m=t.select("g");v(m,s.markers,s.type,s.diagramId),D(),H(),Y(),O(),s.nodes.forEach(o=>{g.setNode(o.id,{...o}),o.parentId&&g.setParent(o.id,o.parentId)}),l.debug("Edges:",s.edges),s.edges.forEach(o=>{if(o.start===o.end){const r=o.start,f=r+"---"+r+"---1",h=r+"---"+r+"---2",a=g.node(r);g.setNode(f,{domId:f,id:f,parentId:a.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),g.setParent(f,a.parentId),g.setNode(h,{domId:h,id:h,parentId:a.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),g.setParent(h,a.parentId);const i=structuredClone(o),n=structuredClone(o),p=structuredClone(o),y=structuredClone(o);n.originalEdge=i,n.selfLoop={id:i.id,order:0},p.originalEdge=i,p.selfLoop={id:i.id,order:1},y.originalEdge=i,y.selfLoop={id:i.id,order:2},n.label="",n.arrowTypeEnd="none",n.endLabelLeft="",n.endLabelRight="",n.startLabelLeft="",n.id=r+"-cyclic-special-1",p.startLabelRight="",p.startLabelLeft="",p.endLabelLeft="",p.endLabelRight="",p.arrowTypeStart="none",p.arrowTypeEnd="none",p.id=r+"-cyclic-special-mid",y.label="",y.startLabelRight="",y.startLabelLeft="",y.arrowTypeStart="none",a.isGroup&&(n.fromCluster=r,y.toCluster=r),y.id=r+"-cyclic-special-2",y.arrowTypeStart="none",g.setEdge(r,f,n,r+"-cyclic-special-0"),g.setEdge(f,h,p,r+"-cyclic-special-1"),g.setEdge(h,r,y,r+"-cyclic-special-2")}else g.setEdge(o.start,o.end,{...o},o.id)}),l.warn("Graph at first:",JSON.stringify(I(g))),J(g),l.warn("Graph after XAX:",JSON.stringify(I(g)));const c=_();await T(m,g,s.type,s.diagramId,void 0,c)},"render");export{ne as getEdgesToRender,le as render};
import{casO,wasI,aasJ,fasP,basE,sasA}from"./chunk-RYQCIY6F-BrAj5IFL.js";import{_asw,amasv,anasD,aoasH,apasY,l,cas_,aqasW,aras$,agasj,asasq,ahasR,afasF,atasz,auasK,avasG}from"./mermaid.core-B_UcP3X_.js";import{GasQ}from"./graph-DOmOIIwC.js";import{lasU}from"./layout-D-LzfAck.js";import"./map-DxJ2ADlA.js";import"./index-BOtIzGLC.js";varC=w((s,t,g)=>Math.max(t,Math.min(g,s)),"clamp"),B=w((s="TB")=>{switch(s){case"BT":return"bottom";case"LR":return"right";case"RL":return"left";case"TB":default:return"top"}},"getDefaultSelfLoopSide"),V=w(s=>s==="flowchart"||s==="flowchart-v2"||s==="stateDiagram","shouldMergeSelfLoopSegments"),Z=w((s,t,g,y,c)=>{consto=[],r=newSet;if(g.forEach(({start:i,end:n})=>{i!==y&&r.add(i),n!==y&&r.add(n)}),r.forEach(i=>{constn=s.node(i);typeofn?.x=="number"&&typeofn?.y=="number"&&o.push(n)}),o.length===0&&g.forEach(({edge:i})=>{(i.points??[]).forEach(n=>{typeofn?.x=="number"&&typeofn?.y=="number"&&o.push(n)})}),o.length===0)returnB(c);constf=o.reduce((i,n)=>({x:i.x+n.x/o.length,y:i.y+n.y/o.length}),{x:0,y:0}),h=f.x-t.x,a=f.y-t.y;returnMath.abs(h)>Math.abs(a)?h>0?"right":"left":Math.abs(a)>0?a>0?"bottom":"top":B(c)},"getSelfLoopSide"),ee=w((s,t="top",g=0,y=0)=>{constc=s.x,o=s.y-g,r=s.width/2,f=s.height/2,h=Math.max(36,Math.min(100,s.width*.8)),a=C(Math.max(y,s.width*.35),36,h),i=C(Math.min(s.width,s.height)*.45,24,48);switch(t){case"bottom":{constn=o+f;return[{x:c-a/2,y:n},{x:c-a/2,y:n+i},{x:c+a/2,y:n+i},{x:c+a/2,y:n}]}case"right":{constn=c+r;return[{x:n,y:o-a/2},{x:n+i,y:o-a/2},{x:n+i,y:o+a/2},{x:n,y:o+a/2}]}case"left":{constn=c-r;return[{x:n,y:o-a/2},{x:n-i,y:o-a/2},{x:n-i,y:o+a/2},{x:n,y:o+a/2}]}case"top":default:{constn=o-f;return[{x:c-a/2,y:n},{x:c-a/2,y:n-i},{x:c+a/2,y:n-i},{x:c+a/2,y:n}]}}},"getSelfLoopPoints"),te=w((s,t,g="top",y=0,c={})=>{constr=s.x,f=s.y-y,h=c.width??0,a=c.height??0;switch(g){case"bottom":return{x:r,y:Math.max(...t.map(i=>i.y))+a/2+4};case"right":return{x:Math.max(...t.map(i=>i.x))+h/2+4,y:f};case"left":return{x:Math.min(...t.map(i=>i.x))-h/2-4,y:f};case"top":default:return{x:r,y:Math.min(...t.map(i=>i.y))-a/2-4}}},"getSelfLoopLabelPosition"),ne=w((s,t=0,{mergeSelfLoops:g=!0}={})=>{consty=newMap,c=[],o=s.graph()?.rankdir;returns.edges().forEach(r=>{constf=s.edge(r);if(g&&f.selfLoop){consth=f.selfLoop.id;y.has(h)||y.set(h,[]),y.get(h).push({edge:f,start:r.v,end:r.w})}elsec.push({edge:f,start:r.v,end:r.w})}),y.forEach(r=>{if(r.length!==3){r.forEach(L=>c.push(L));return}r.sort((L,d)=>L.edge.selfLoop.order-d.edge.selfLoop.order);const[f,h,a]=r,i=f.edge.originalEdge??h.edge.originalEdge??a.edge.originalEdge??h.edge,n=s.node(i.start);if(!n){r.forEach(L=>c.push(L));return}constp={width:h.edge.width,height:h.edge.height},m=Z(s,n,r,i.start,o),X=ee(n,m,t,p.width??0),S=te(n,X,m,t,p),b={...h.edge,...i,id:i.id,points:X,start:i.start,end:i.end,x:S.x,y:S.y,width:p.width,height:p.height,labelStyle:h.edge.labelStyle,fromCluster:f.edge.fromCluster??h.edge.fromCluster??a.edge.fromCluster,toCluster:f.edge.toCluster??h.edge.toCluster??a.edge.toCluster};deleteb.selfLoop,deleteb.originalEdge,c.push({edge:b,start:b.start,end:b.end})}),c},"getEdgesToRender"),T=w(async(s,t,g,y,c,o)=>{l.warn("Graph in recursive render:XAX",I(t),c);constr=t.graph().rankdir;l.trace("Dir in recursive render - dir:",r);constf=s.insert("g").attr("class","root");t.nodes()?l.info("Recursive render XXX",t.nodes()):l.info("No nodes found for",t),t.edges().length>0&&l.info("Recursive edges",t.edge(t.edges()[0]));consth=f.insert("g").attr("class","clusters"),a=f.insert("g").attr("class","edgePaths"),i=f.insert("g").attr("class","edgeLabels"),n=f.insert("g").attr("class","nodes"),p=V(g);awaitPromise.all(t.nodes().map(asyncfunction(d){conste=t.node(d);if(c!==void0){constu=JSON.parse(JSON.stringify(c.clusterData));l.trace(`Setting data for parent cluster XXX
Node.id=`,d,`
data=`,u.height,`
Parentcluster`,c.height),t.setNode(c.id,u),t.parent(d)||(l.trace("Setting parent",d,c.id),t.setParent(d,c.id,u))}if(l.info("(Insert) Node XXX"+d+": "+JSON.stringify(t.node(d))),e?.clusterNode){l.info("Cluster identified XBX",d,e.width,t.node(d));const{ranksep:u,nodesep:x}=t.graph();e.graph.setGraph({...e.graph.graph(),ranksep:u+25,nodesep:x});const N=await T(n,e.graph,g,y,t.node(d),o),M=N.elem;W(e,M),e.diff=N.diff||0,l.info("New compound node after recursive render XAX",d,"width",e.width,"height",e.height),$(M,e)}else t.children(d).length>0?(l.trace("Cluster - the non recursive path XBX",d,e.id,e,e.width,"Graph:",t),l.trace(P(e.id,t)),E.set(e.id,{id:P(e.id,t),node:e})):(l.trace("Node - the non recursive path XAX",d,n,t.node(d),r),await j(n,t.node(d),{config:o,dir:r}))})),await w(async()=>{const d=t.edges().map(async function(e){const u=t.edge(e.v,e.w,e.name);if(l.info("Edge "+e.v+" -> "+e.w+": "+JSON.stringify(e)),l.info("Edge "+e.v+" -> "+e.w+": ",e," ",JSON.stringify(t.edge(e))),l.info("Fix",E,"ids:",e.v,e.w,"Translating: ",E.get(e.v),E.get(e.w)),p&&u.selfLoop){if(u.selfLoop.order!==1)return;const x=u.id;u.id=u.selfLoop.id,await G(i,u),u.id=x;return}await G(i,u)});await Promise.all(d)},"processEdges")(),l.info("Graph before layout:",JSON.stringify(I(t))),l.info("############################################# XXX"),l.info("### Layout ### XXX"),l.info("############################################# XXX"),U(t),l.info("Graph after layout:",JSON.stringify(I(t)));let X=0,{subGraphTitleTotalMargin:S}=q(o);await Promise.all(A(t).map(async function(d){const e=t.node(d);if(l.info("Position XBX => "+d+": ("+e.x,","+e.y,") width: ",e.width," height: ",e.height),e?.clusterNode)e.y+=S,l.info("A tainted cluster node XBX1",d,e.id,e.width,e.height,e.x,e.y,t.parent(d)),E.get(e.id).node=e,R(e);else if(t.children(d).length>0){l.info("A pure cluster node XBX1",d,e.id,e.x,e.y,e.width,e.height,t.parent(d)),e.height+=S,t.node(e.parentId);const u=e?.padding/2||0,x=e?.labelBBox?.height||0,N=x-u||0;l.debug("OffsetY",N,"labelHeight",x,"halfPadding",u),await F(h,e),E.get(e.id).node=e}else{const u=t.node(e.parentId);e.y+=S/2,l.info("A regular node XBX1 - using the padding",e.id,"parent",e.parentId,e.width,e.height,e.x,e.y,"offsetY",e.offsetY,"parent",u,u?.offsetY,e),R(e)}}));const b=S/2;return ne(t,b,{mergeSelfLoops:p}).forEach(function({edge:d,start:e,end:u}){l.info("Edge "+e+" -> "+u+": "+JSON.stringify(d),d),d.points.forEach(k=>k.y+=b);const x=t.node(e),N=t.node(u),M=z(a,d,E,g,x,N,y);K(d,M)}),t.nodes().forEach(function(d){const e=t.node(d);l.info(d,e.type,e.diff),e.isGroup&&(X=e.diff)}),l.warn("Returning from recursive render XAX",f,X),{elem:f,diff:X}},"recursiveRender"),ce=w(async(s,t)=>{const g=new Q({multigraph:!0,compound:!0}).setGraph({rankdir:s.direction,nodesep:s.config?.nodeSpacing||s.config?.flowchart?.nodeSpacing||s.nodeSpacing,ranksep:s.config?.rankSpacing||s.config?.flowchart?.rankSpacing||s.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}}),y=t.select("g");v(y,s.markers,s.type,s.diagramId),D(),H(),Y(),O(),s.nodes.forEach(o=>{g.setNode(o.id,{...o}),o.parentId&&g.setParent(o.id,o.parentId)}),l.debug("Edges:",s.edges),s.edges.forEach(o=>{if(o.start===o.end){const r=o.start,f=r+"---"+r+"---1",h=r+"---"+r+"---2",a=g.node(r);g.setNode(f,{domId:f,id:f,parentId:a.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),g.setParent(f,a.parentId),g.setNode(h,{domId:h,id:h,parentId:a.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),g.setParent(h,a.parentId);const i=structuredClone(o),n=structuredClone(o),p=structuredClone(o),m=structuredClone(o);n.originalEdge=i,n.selfLoop={id:i.id,order:0},p.originalEdge=i,p.selfLoop={id:i.id,order:1},m.originalEdge=i,m.selfLoop={id:i.id,order:2},n.label="",n.arrowTypeEnd="none",n.endLabelLeft="",n.endLabelRight="",n.startLabelLeft="",n.id=r+"-cyclic-special-1",p.startLabelRight="",p.startLabelLeft="",p.endLabelLeft="",p.endLabelRight="",p.arrowTypeStart="none",p.arrowTypeEnd="none",p.id=r+"-cyclic-special-mid",m.label="",m.startLabelRight="",m.startLabelLeft="",m.arrowTypeStart="none",a.isGroup&&(n.fromCluster=r,m.toCluster=r),m.id=r+"-cyclic-special-2",m.arrowTypeStart="none",g.setEdge(r,f,n,r+"-cyclic-special-0"),g.setEdge(f,h,p,r+"-cyclic-special-1"),g.setEdge(h,r,m,r+"-cyclic-special-2")}else g.setEdge(o.start,o.end,{...o},o.id)}),l.warn("Graph at first:",JSON.stringify(I(g))),J(g),l.warn("Graph after XAX:",JSON.stringify(I(g)));const c=_();await T(y,g,s.type,s.diagramId,void 0,c)},"render");export{ne as getEdgesToRender,ce as render};
import{pasB}from"./chunk-JWPE2WC7-C7S97jLg.js";import{_asb,Aasu,Das$,easC,lasm,basS,aasD,nasT,oasP,gasz,sasA,yasE,BasF,pasW}from"./mermaid.core-CqiFQExc.js";import{pas_}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";varN=F.packet,w=class{constructor(){this.packet=[],this.setAccTitle=S,this.getAccTitle=D,this.setDiagramTitle=T,this.getDiagramTitle=P,this.getAccDescription=z,this.setAccDescription=A}static{b(this,"PacketDB")}getConfig(){constt=u({...N,...E().packet});returnt.showBits&&(t.paddingY+=10),t}getPacket(){returnthis.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){W(),this.packet=[]}},L=1e4,M=b((t,e)=>{B(t,e);letr=-1,o=[],n=1;const{bitsPerRow:l}=e.getConfig();for(let{start:a,end:i,bits:d,label:c}oft.blocks){if(a!==void0&&i!==void0&&i<a)thrownewError(`Packet block ${a} - ${i} is invalid. End must be greater than start.`);if(a??=r+1,a!==r+1)thrownewError(`Packet block ${a} - ${i??a} is not contiguous. It should start from ${r+1}.`);if(d===0)thrownewError(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(i??=a+(d??1)-1,d??=i-a+1,r=i,m.debug(`Packet block ${a} - ${r} with label ${c}`);o.length<=l+1&&e.getPacket().length<L;){const[p,s]=Y({start:a,end:i,bits:d,label:c},n,l);if(o.push(p),p.end+1===n*l&&(e.pushWord(o),o=[],n++),!s)break;({start:a,end:i,bits:d,label:c}=s)}}e.pushWord(o)},"populate"),Y=b((t,e,r)=>{if(t.start===void0)thrownewError("start should have been set during first phase");if(t.end===void0)thrownewError("end should have been set during first phase");if(t.start>t.end)thrownewError(`Block start ${t.start} is greater than block end ${t.end}.`);if(t.end+1<=e*r)return[t,void0];consto=e*r-1,n=e*r;return[{start:t.start,end:o,label:t.label,bits:o-t.start},{start:n,end:t.end,label:t.label,bits:t.end-n}]},"getNextFittingBlock"),v={parser:{yy:void0},parse:b(asynct=>{conste=await_("packet",t),r=v.parser?.yy;if(!(rinstanceofw))thrownewError("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");m.debug(e),M(e,r)},"parse")},I=b((t,e,r,o)=>{constn=o.db,l=n.getConfig(),{rowHeight:a,paddingY:i,bitWidth:d,bitsPerRow:c}=l,p=n.getPacket(),s=n.getDiagramTitle(),h=a+i,g=h*(p.length+1)-(s?0:a),k=d*c+2,f=$(e);f.attr("viewBox",`0 0 ${k}${g}`),C(f,g,k,l.useMaxWidth);for(const[x,y]ofp.entries())O(f,y,x,l);f.append("text").text(s).attr("x",k/2).attr("y",g-h/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),O=b((t,e,r,{rowHeight:o,paddingX:n,paddingY:l,bitWidth:a,bitsPerRow:i,showBits:d})=>{constc=t.append("g"),p=r*(o+l)+l;for(constsofe){consth=s.start%i*a+1,g=(s.end-s.start+1)*a-n;if(c.append("rect").attr("x",h).attr("y",p).attr("width",g).attr("height",o).attr("class","packetBlock"),c.append("text").attr("x",h+g/2).attr("y",p+o/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(s.label),!d)continue;constk=s.end===s.start,f=p-2;c.append("text").attr("x",h+(k?g/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(s.start),k||c.append("text").attr("x",h+g).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(s.end)}},"drawWord"),j={draw:I},G={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},H=b(({packet:t}={})=>{conste=u(G,t);return`
.packetByte{
font-size:${e.byteFontSize};
}
.packetByte.start{
fill:${e.startByteColor};
}
.packetByte.end{
fill:${e.endByteColor};
}
.packetLabel{
fill:${e.labelColor};
font-size:${e.labelFontSize};
}
.packetTitle{
fill:${e.titleColor};
font-size:${e.titleFontSize};
}
.packetBlock{
stroke:${e.blockStrokeColor};
stroke-width:${e.blockStrokeWidth};
fill:${e.blockFillColor};
}
`},"styles"),J={parser:v,get db(){return new w},renderer:j,styles:H};export{J as diagram};
import{pasB}from"./chunk-JWPE2WC7-TZtJmqSP.js";import{_asb,Basu,Fas$,easC,lasm,basS,aasD,oasT,pasz,gasF,sasP,zasE,DasA,qasW}from"./mermaid.core-B_UcP3X_.js";import{pas_}from"./cynefin-VYW2F7L2-BnQdr27u.js";import"./index-BOtIzGLC.js";varN=A.packet,w=class{constructor(){this.packet=[],this.setAccTitle=S,this.getAccTitle=D,this.setDiagramTitle=T,this.getDiagramTitle=z,this.getAccDescription=F,this.setAccDescription=P}static{b(this,"PacketDB")}getConfig(){constt=u({...N,...E().packet});returnt.showBits&&(t.paddingY+=10),t}getPacket(){returnthis.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){W(),this.packet=[]}},L=1e4,M=b((t,e)=>{B(t,e);letr=-1,s=[],n=1;const{bitsPerRow:l}=e.getConfig();for(let{start:a,end:i,bits:d,label:c}oft.blocks){if(a!==void0&&i!==void0&&i<a)thrownewError(`Packet block ${a} - ${i} is invalid. End must be greater than start.`);if(a??=r+1,a!==r+1)thrownewError(`Packet block ${a} - ${i??a} is not contiguous. It should start from ${r+1}.`);if(d===0)thrownewError(`Packet block ${a} is invalid. Cannot have a zero bit field.`);for(i??=a+(d??1)-1,d??=i-a+1,r=i,m.debug(`Packet block ${a} - ${r} with label ${c}`);s.length<=l+1&&e.getPacket().length<L;){const[p,o]=Y({start:a,end:i,bits:d,label:c},n,l);if(s.push(p),p.end+1===n*l&&(e.pushWord(s),s=[],n++),!o)break;({start:a,end:i,bits:d,label:c}=o)}}e.pushWord(s)},"populate"),Y=b((t,e,r)=>{if(t.start===void0)thrownewError("start should have been set during first phase");if(t.end===void0)thrownewError("end should have been set during first phase");if(t.start>t.end)thrownewError(`Block start ${t.start} is greater than block end ${t.end}.`);if(t.end+1<=e*r)return[t,void0];consts=e*r-1,n=e*r;return[{start:t.start,end:s,label:t.label,bits:s-t.start},{start:n,end:t.end,label:t.label,bits:t.end-n}]},"getNextFittingBlock"),v={parser:{yy:void0},parse:b(asynct=>{conste=await_("packet",t),r=v.parser?.yy;if(!(rinstanceofw))thrownewError("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");m.debug(e),M(e,r)},"parse")},I=b((t,e,r,s)=>{constn=s.db,l=n.getConfig(),{rowHeight:a,paddingY:i,bitWidth:d,bitsPerRow:c}=l,p=n.getPacket(),o=n.getDiagramTitle(),h=a+i,g=h*(p.length+1)-(o?0:a),k=d*c+2,f=$(e);f.attr("viewBox",`0 0 ${k}${g}`),C(f,g,k,l.useMaxWidth);for(const[x,y]ofp.entries())O(f,y,x,l);f.append("text").text(o).attr("x",k/2).attr("y",g-h/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),O=b((t,e,r,{rowHeight:s,paddingX:n,paddingY:l,bitWidth:a,bitsPerRow:i,showBits:d})=>{constc=t.append("g"),p=r*(s+l)+l;for(constoofe){consth=o.start%i*a+1,g=(o.end-o.start+1)*a-n;if(c.append("rect").attr("x",h).attr("y",p).attr("width",g).attr("height",s).attr("class","packetBlock"),c.append("text").attr("x",h+g/2).attr("y",p+s/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(o.label),!d)continue;constk=o.end===o.start,f=p-2;c.append("text").attr("x",h+(k?g/2:0)).attr("y",f).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",k?"middle":"start").text(o.start),k||c.append("text").attr("x",h+g).attr("y",f).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(o.end)}},"drawWord"),j={draw:I},q={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},G=b(({packet:t}={})=>{conste=u(q,t);return`
.packetByte{
font-size:${e.byteFontSize};
}
.packetByte.start{
fill:${e.startByteColor};
}
.packetByte.end{
fill:${e.endByteColor};
}
.packetLabel{
fill:${e.labelColor};
font-size:${e.labelFontSize};
}
.packetTitle{
fill:${e.titleColor};
font-size:${e.titleFontSize};
}
.packetBlock{
stroke:${e.blockStrokeColor};
stroke-width:${e.blockStrokeWidth};
fill:${e.blockFillColor};
}
`},"styles"),X={parser:v,get db(){return new w},renderer:j,styles:G};export{X as diagram};
import{pask}from"./chunk-JWPE2WC7-TZtJmqSP.js";import{sasR,gasF,pasI,oas_,aasD,basE,_asc,Fasz,qasP,Basy,zasC,DasG,lasB,W,easV}from"./mermaid.core-B_UcP3X_.js";import{pasH}from"./cynefin-VYW2F7L2-BnQdr27u.js";import"./index-BOtIzGLC.js";varm={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:m},x=structuredClone(w),j=G.radar,q=c(()=>y({...j,...C().radar}),"getConfig"),b=c(()=>x.axes,"getAxes"),N=c(()=>x.curves,"getCurves"),U=c(()=>x.options,"getOptions"),X=c(a=>{x.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Y=c(a=>{x.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:Z(t.entries)}))},"setCurves"),Z=c(a=>{if(a[0].axis==null)returna.map(e=>e.value);constt=b();if(t.length===0)thrownewError("Axes must be populated before curves for reference entries");returnt.map(e=>{constr=a.find(s=>s.axis?.$refText===e.name);if(r===void0)thrownewError("Missing entry for axis "+e.label);returnr.value})},"computeCurveEntries"),J=c(a=>{constt=a.reduce((e,r)=>(e[r.name]=r,e),{});x.options={showLegend:t.showLegend?.value??m.showLegend,ticks:t.ticks?.value??m.ticks,max:t.max?.value??m.max,min:t.min?.value??m.min,graticule:t.graticule?.value??m.graticule}},"setOptions"),K=c(()=>{P(),x=structuredClone(w)},"clear"),$={getAxes:b,getCurves:N,getOptions:U,setAxes:X,setCurves:Y,setOptions:J,getConfig:q,clear:K,setAccTitle:E,getAccTitle:D,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:F,setAccDescription:R},Q=c(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),tt={parse:c(asynca=>{constt=awaitH("radar",a);B.debug(t),Q(t)},"parse")},et=c((a,t,e,r)=>{consts=r.db,i=s.getAxes(),l=s.getCurves(),n=s.getOptions(),o=s.getConfig(),d=s.getDiagramTitle(),p=z(t),u=at(p,o),g=n.max??Math.max(...l.map(f=>Math.max(...f.entries))),h=n.min,v=Math.min(o.width,o.height)/2;rt(u,i,v,n.ticks,n.graticule),st(u,i,v,o),A(u,i,l,h,g,n.graticule,o),T(u,l,n.showLegend,o),u.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-o.height/2-o.marginTop)},"draw"),at=c((a,t)=>{conste=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};returnV(a,r,e,t.useMaxWidth??!0),a.attr("viewBox",`0 0 ${e}${r}`).attr("overflow","visible"),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),rt=c((a,t,e,r,s)=>{if(s==="circle")for(leti=0;i<r;i++){constl=e*(i+1)/r;a.append("circle").attr("r",l).attr("class","radarGraticule")}elseif(s==="polygon"){consti=t.length;for(letl=0;l<r;l++){constn=e*(l+1)/r,o=t.map((d,p)=>{constu=2*p*Math.PI/i-Math.PI/2,g=n*Math.cos(u),h=n*Math.sin(u);return`${g},${h}`}).join(" ");a.append("polygon").attr("points",o).attr("class","radarGraticule")}}},"drawGraticule"),st=c((a,t,e,r)=>{consts=t.length;for(leti=0;i<s;i++){constl=t[i].label,n=2*i*Math.PI/s-Math.PI/2,o=Math.cos(n),d=Math.sin(n);a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*o).attr("y2",e*r.axisScaleFactor*d).attr("class","radarAxisLine");constp=o>.01?"start":o<-.01?"end":"middle",u=d>.01?"hanging":d<-.01?"auto":"central",g=4;a.append("text").text(l).attr("x",e*r.axisLabelFactor*o+g*o).attr("y",e*r.axisLabelFactor*d+g*d).attr("text-anchor",p).attr("dominant-baseline",u).attr("class","radarAxisLabel")}},"drawAxes");functionA(a,t,e,r,s,i,l){constn=t.length,o=Math.min(l.width,l.height)/2;e.forEach((d,p)=>{if(d.entries.length!==n)return;constu=d.entries.map((g,h)=>{constv=2*Math.PI*h/n-Math.PI/2,f=M(g,r,s,o),S=f*Math.cos(v),O=f*Math.sin(v);return{x:S,y:O}});i==="circle"?a.append("path").attr("d",L(u,l.curveTension)).attr("class",`radarCurve-${p}`):i==="polygon"&&a.append("polygon").attr("points",u.map(g=>`${g.x},${g.y}`).join(" ")).attr("class",`radarCurve-${p}`)})}c(A,"drawCurves");functionM(a,t,e,r){consts=Math.min(Math.max(a,t),e);returnr*(s-t)/(e-t)}c(M,"relativeRadius");functionL(a,t){conste=a.length;letr=`M${a[0].x},${a[0].y}`;for(lets=0;s<e;s++){consti=a[(s-1+e)%e],l=a[s],n=a[(s+1)%e],o=a[(s+2)%e],d={x:l.x+(n.x-i.x)*t,y:l.y+(n.y-i.y)*t},p={x:n.x-(o.x-l.x)*t,y:n.y-(o.y-l.y)*t};r+=` C${d.x},${d.y}${p.x},${p.y}${n.x},${n.y}`}return`${r} Z`}c(L,"closedRoundCurve");functionT(a,t,e,r){if(!e)return;consts=(r.width/2+r.marginRight)*3/4,i=-(r.height/2+r.marginTop)*3/4,l=20;t.forEach((n,o)=>{constd=a.append("g").attr("transform",`translate(${s}, ${i+o*l})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${o}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}c(T,"drawLegend");varnt={draw:et},ot=c((a,t)=>{lete="";for(letr=0;r<a.THEME_COLOR_LIMIT;r++){consts=a[`cScale${r}`];e+=`
import{pask}from"./chunk-JWPE2WC7-C7S97jLg.js";import{sasR,gasE,oasI,nas_,aasD,basF,_asc,DasP,pasz,Aasy,yasC,BasG,lasB,EasW,easV}from"./mermaid.core-CqiFQExc.js";import{pasH}from"./cynefin-VYW2F7L2-DEnbetzx.js";import"./index--0t1wzw_.js";import"./_commonjsHelpers-CqkleIqs.js";varm={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},w={axes:[],curves:[],options:m},x=structuredClone(w),j=G.radar,N=c(()=>y({...j,...C().radar}),"getConfig"),b=c(()=>x.axes,"getAxes"),U=c(()=>x.curves,"getCurves"),X=c(()=>x.options,"getOptions"),Y=c(a=>{x.axes=a.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),Z=c(a=>{x.curves=a.map(t=>({name:t.name,label:t.label??t.name,entries:q(t.entries)}))},"setCurves"),q=c(a=>{if(a[0].axis==null)returna.map(e=>e.value);constt=b();if(t.length===0)thrownewError("Axes must be populated before curves for reference entries");returnt.map(e=>{constr=a.find(s=>s.axis?.$refText===e.name);if(r===void0)thrownewError("Missing entry for axis "+e.label);returnr.value})},"computeCurveEntries"),J=c(a=>{constt=a.reduce((e,r)=>(e[r.name]=r,e),{});x.options={showLegend:t.showLegend?.value??m.showLegend,ticks:t.ticks?.value??m.ticks,max:t.max?.value??m.max,min:t.min?.value??m.min,graticule:t.graticule?.value??m.graticule}},"setOptions"),K=c(()=>{z(),x=structuredClone(w)},"clear"),$={getAxes:b,getCurves:U,getOptions:X,setAxes:Y,setCurves:Z,setOptions:J,getConfig:N,clear:K,setAccTitle:F,getAccTitle:D,setDiagramTitle:_,getDiagramTitle:I,getAccDescription:E,setAccDescription:R},Q=c(a=>{k(a,$);const{axes:t,curves:e,options:r}=a;$.setAxes(t),$.setCurves(e),$.setOptions(r)},"populate"),tt={parse:c(asynca=>{constt=awaitH("radar",a);B.debug(t),Q(t)},"parse")},et=c((a,t,e,r)=>{consts=r.db,i=s.getAxes(),l=s.getCurves(),n=s.getOptions(),o=s.getConfig(),d=s.getDiagramTitle(),p=P(t),u=at(p,o),g=n.max??Math.max(...l.map(f=>Math.max(...f.entries))),h=n.min,v=Math.min(o.width,o.height)/2;rt(u,i,v,n.ticks,n.graticule),st(u,i,v,o),A(u,i,l,h,g,n.graticule,o),T(u,l,n.showLegend,o),u.append("text").attr("class","radarTitle").text(d).attr("x",0).attr("y",-o.height/2-o.marginTop)},"draw"),at=c((a,t)=>{conste=t.width+t.marginLeft+t.marginRight,r=t.height+t.marginTop+t.marginBottom,s={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};returnV(a,r,e,t.useMaxWidth??!0),a.attr("viewBox",`0 0 ${e}${r}`).attr("overflow","visible"),a.append("g").attr("transform",`translate(${s.x}, ${s.y})`)},"drawFrame"),rt=c((a,t,e,r,s)=>{if(s==="circle")for(leti=0;i<r;i++){constl=e*(i+1)/r;a.append("circle").attr("r",l).attr("class","radarGraticule")}elseif(s==="polygon"){consti=t.length;for(letl=0;l<r;l++){constn=e*(l+1)/r,o=t.map((d,p)=>{constu=2*p*Math.PI/i-Math.PI/2,g=n*Math.cos(u),h=n*Math.sin(u);return`${g},${h}`}).join(" ");a.append("polygon").attr("points",o).attr("class","radarGraticule")}}},"drawGraticule"),st=c((a,t,e,r)=>{consts=t.length;for(leti=0;i<s;i++){constl=t[i].label,n=2*i*Math.PI/s-Math.PI/2,o=Math.cos(n),d=Math.sin(n);a.append("line").attr("x1",0).attr("y1",0).attr("x2",e*r.axisScaleFactor*o).attr("y2",e*r.axisScaleFactor*d).attr("class","radarAxisLine");constp=o>.01?"start":o<-.01?"end":"middle",u=d>.01?"hanging":d<-.01?"auto":"central",g=4;a.append("text").text(l).attr("x",e*r.axisLabelFactor*o+g*o).attr("y",e*r.axisLabelFactor*d+g*d).attr("text-anchor",p).attr("dominant-baseline",u).attr("class","radarAxisLabel")}},"drawAxes");functionA(a,t,e,r,s,i,l){constn=t.length,o=Math.min(l.width,l.height)/2;e.forEach((d,p)=>{if(d.entries.length!==n)return;constu=d.entries.map((g,h)=>{constv=2*Math.PI*h/n-Math.PI/2,f=M(g,r,s,o),S=f*Math.cos(v),O=f*Math.sin(v);return{x:S,y:O}});i==="circle"?a.append("path").attr("d",L(u,l.curveTension)).attr("class",`radarCurve-${p}`):i==="polygon"&&a.append("polygon").attr("points",u.map(g=>`${g.x},${g.y}`).join(" ")).attr("class",`radarCurve-${p}`)})}c(A,"drawCurves");functionM(a,t,e,r){consts=Math.min(Math.max(a,t),e);returnr*(s-t)/(e-t)}c(M,"relativeRadius");functionL(a,t){conste=a.length;letr=`M${a[0].x},${a[0].y}`;for(lets=0;s<e;s++){consti=a[(s-1+e)%e],l=a[s],n=a[(s+1)%e],o=a[(s+2)%e],d={x:l.x+(n.x-i.x)*t,y:l.y+(n.y-i.y)*t},p={x:n.x-(o.x-l.x)*t,y:n.y-(o.y-l.y)*t};r+=` C${d.x},${d.y}${p.x},${p.y}${n.x},${n.y}`}return`${r} Z`}c(L,"closedRoundCurve");functionT(a,t,e,r){if(!e)return;consts=(r.width/2+r.marginRight)*3/4,i=-(r.height/2+r.marginTop)*3/4,l=20;t.forEach((n,o)=>{constd=a.append("g").attr("transform",`translate(${s}, ${i+o*l})`);d.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${o}`),d.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(n.label)})}c(T,"drawLegend");varnt={draw:et},ot=c((a,t)=>{lete="";for(letr=0;r<a.THEME_COLOR_LIMIT;r++){consts=a[`cScale${r}`];e+=`