* fix(kap-server): decouple workspace trust from runtime initialization
* fix: preserve workspace trust errors
* fix: emit workspace trust recovery changes
* fix: complete workspace trust recovery
* fix(agent-core-v2): reclaim session locks from zombie processes (#4320)
(cherry picked from commit 0a90e2e8a2)
* feat: let users take over a session lock left by an exited process (#4328)
* feat: let users take over a session lock left by an exited process
Session locks judged alive by mistake (a different hostname, pid reuse, or a failed process-state probe) blocked the session with no way out. Add a takeover path: the lock service evicts another process's lock, the SDK exposes Session.takeOverLock(), kap-server adds POST /api/v1/sessions/{id}:takeover, and the TUI offers Take over once a retry is still occupied. KIMI_CODE_SESSION_LOCK=0 turns the lock off for a process.
* fix(agent-core-v2): evict the lock directly on takeover
Takeover went through the reclaim marker, so a stale marker left by a crashed reclaimer on another host made takeover a silent no-op while the API reported success. Unlink the lock file directly instead.
* fix(agent-core-v2): surface takeover failures when the lock cannot be removed
Takeover swallowed unlink errors, so a lock file another user owns in a shared sticky directory was reported as taken over while the next retry stayed occupied. Ignore only ENOENT and let other errors reach the caller. Also fix the run gate test bootstrap stub typing.
* fix(tui): simplify the session takeover copy
* fix(tui): tie the takeover warning to the takeover action
* fix(tui): drop the holder line from the takeover dialog
* refactor(tui): move the session occupancy dialogs into a controller
(cherry picked from commit e7f1a99fc5)
* chore: sync web dist from code-app
code-app: 76483646b7c46a61bff868f1473535e0a32b473d
---------
Co-authored-by: 7Sageer <sag77r@hotmail.com>
* feat(kap-server): add remote control device deletion (#4192) (#4195)
* feat(kap-server): add remote control device deletion
* fix(kap-server): coordinate with the local tunnel before deleting this machine's device
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
* ci: enable recoverable publishing for the October release (#4218)
* ci: publish core releases from isolated release branches
* ci: simplify release recovery to the CLI package
* ci: publish VS Code from approved release commits
* test: await release test registration for type-aware lint
* test: avoid assuming order of independent tower events
* ci: limit release migration to existing publishing flow
* chore: sync web dist from code-app (#4221)
* chore: sync web dist from code-app
code-app: f828bac439ab5b9266ff8387c2afb96ecfcb59ae
* ci: retrigger (flaky rpc-events teardown ENOTEMPTY)
---------
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
* ci: release packages (#4220)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(tui): queue /compact behind the running turn (#4276)
* feat(tui): queue /compact behind the running turn
* docs: note /compact queueing while the agent is busy
* fix(tui): harden queued /compact edge cases
- keep /compact blocked while a compaction holds the turn (busy reason normalized to compacting)
- treat the first queued /compact as a FIFO barrier for Ctrl-S manual steer
- gate the failed-fire retry timer on the session actually going idle
* fix(tui): cover remaining /compact queue review edges
- advertise Ctrl-S in the queue pane only for items ahead of the compact barrier
- queue /compact when the awaited lazy session creation lands on a running turn
- document the auto-compaction skip for queued /compact in docs and the changeset
* fix(tui): guard queued /compact against concurrent compactions
- requeue without firing when another compaction already started (the SDK's begin() swallows this as a silent no-op; declared residual window needs the contract to expose the result)
- restore the input when a duplicate /compact is refused after lazy session creation
* fix(tui): render requeued /compact and mark it always available in docs
- request a render after restoring a compact item to the queue so it does not appear vanished during backoff
- the reference table's availability column now matches the queueing behavior (/compact is accepted during streaming and runs in order)
* fix(tui): steer messages ahead of a queued /compact automatically
The automatic (tower/WaitFor) steer batch now slices at the first queued /compact just like the manual path, so pre-barrier prompts still end a WaitFor promptly instead of being blocked by an unrelated compact item. Batches containing bash or skill items ahead of the barrier keep their existing all-or-nothing semantics.
* test(tui): drive the tower steer barrier case through the WaitFor event path
* chore: downgrade the /compact queue changeset to patch (#4279)
* feat: add cloud-gated model switch confirmation (#4286)
* feat(cli): add cloud-gated model switch confirmation dialog
* feat(server): expose named client_configs endpoint for cloud-controlled clients
* fix(server): keep broadcasting compaction events to transcript subscribers
* Revert "fix(server): keep broadcasting compaction events to transcript subscribers"
This reverts commit e489915bd8bf753033f982ce1b0845cea64c0784.
* fix(cli): address review findings on the model switch confirmation
* test(cli): fix the auth-flow gate test typing
* fix: close the login-path holes in the model switch gate
- install the refreshed model catalog before matching the cloud rule,
so an alias introduced by /login no longer bypasses the confirmation
- return 'gated' from refreshConfigAfterLogin and forward the picked
effort to the dialog; setDefaultModel skips its follow-up activation
while the gate owns the switch
- ignore Escape once a dialog action was chosen so the input lock
survives the new-session creation
* fix: keep displaced panels alive when the new-session switch settles
The async finalizer of a settled dialog action now only restores the
editor while the model-switch dialog still owns the editor replacement,
so an approval or question panel mounted over it is never wiped out
with its reverse-RPC request still pending.
* feat: support id lists and always-fresh model switch confirmation configs (#4292)
* feat: accept id lists in model switch confirmation rules
* feat: fetch client configs fresh on every request
The /client_configs endpoint now proxies the cloud per request instead
of serving a copy cached at server startup, so config changes take
effect without a server restart. The startup warm-up and the in-memory
store are removed; region is resolved per request.
---------
Co-authored-by: qer <wbxl2000@outlook.com>
Co-authored-by: Kimi Agent <zhangkaiyi@moonshot.ai>
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>
* 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: 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(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: 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
* 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: 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
- 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>
* 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>
* 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
* 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>
* 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.
* 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
* 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.
* 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>
* 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.
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.