Commit graph

131 commits

Author SHA1 Message Date
Luyu Cheng
d6021fa036
feat(kap-server): accept bundled skill activations on the prompt submission route (#2982)
* feat(kap-server): accept bundled skill activations on the prompt submission route

The bundled-submission capability was only reachable through the
in-process klient transports; the App talks to kap-server over /api/v1.
The submit-prompt route now accepts an optional non-empty skills field
and delegates to IAgentSkillService.promptWithSkills — same validation,
events, and single bundled user message as the TUI path — skipping its
own prompt-metadata update (the engine owns it there) and mapping
skill.not_found / skill.type_unsupported onto the skills route's codes.
To return the submission's queue identity, the engine's promptWithSkills
now resolves with prompt_id / user_message_id / created_at / state (plus
turn_id once launched), mirrored through the klient contract.

* refactor(agent-core-v2): slim the promptWithSkills result contract

Drop the user_message_id field (it is always the same identity as
prompt_id — the route duplicates it) and narrow state to the
running/queued/blocked vocabulary, mapped at the engine edge instead of
exposing the internal seven-state PromptState on the wire.

* fix(kap-server): harden bundled skill submissions against review findings

- Validate bundled skill names and types before any media materialization
  or control override, so a rejected bundle leaves session state untouched
  (the engine still re-validates authoritatively).
- Declare the 40415/40912 outcomes on the submit route so the generated
  API documentation includes them.
- The klient output schema no longer tolerates a missing promptWithSkills
  result (a transport-level absence now raises instead of resolving
  undefined), and a failed launch surfaces as an error rather than a
  successful running result.
- Add the changeset for the new public API field.

* fix(kap-server): preflight bundled skills before agent materialization and stabilize listed content

- Skill preflight now runs on the session's catalog before the main agent
  is resolved, so a rejected bundle cannot mutate session metadata by
  registering main (regression test on a cold session without an agent).
- The prompts list projection strips the stored skill blocks from a
  bundled prompt, so GET /prompts returns the same caller-only content as
  the submit response.

* fix(kap-server): reject bundled prompt_id combos at preflight and clean queued staging

- The skills + prompt_id incompatibility rejection now runs at the initial
  bundled preflight, before the main agent is materialized or any
  override binds (previously a yolo override could bind before the 40001).
- Queued bundles no longer skip staging cleanup forever: the discard is
  deferred to the bundle's prompt.completed / prompt.aborted lifecycle
  event, mirroring the plain path's launch-raced cleanup.

* fix(kap-server): clean queued bundle staging on the steer path too

A queued bundle steered into the active turn is consumed at steer time,
but the engine publishes prompt.completed/aborted only for the parent —
the deferred cleanup never fired and its subscription leaked. The
prompt.steered event (matching promptIds) now counts as the child's
intake-completion signal.

* fix(agent-core-v2): materialize daemon-ref media on the steer and inject paths

startNext materializes daemon file references into the session media
store before a prompt's turn, but steer() and inject() enqueued the same
references without that intake, leaving the staging upload as the only
copy — any staging cleanup at steer time would delete the media the
turn is about to consume. Both paths now run the same intake before the
SteerStepRequest is created, so prompt.steered is a truthful
intake-complete signal.

* fix(kap-server): defer staging cleanup to turn settlement, never to steer time

Prompt-intake materialization is best-effort: when it degrades, the
daemon upload is the request-time resolver's fallback source. Discarding
staging at prompt.steered could therefore delete the only readable copy
before the parent's request ran. Cleanup is now uniformly event-driven —
the bundle's own prompt.completed/aborted, or the steer parent's — so
the upload always outlives the request it feeds.

* fix(kap-server): install settlement tracking before bundled enqueue

A hook-blocked bundle completes synchronously inside the submission
call, and an exceptionally fast launch can settle just as early — a
post-call subscription misses the only settlement event and leaks both
the staging blob and the listener. The tracker now subscribes before
enqueueing, buffers lifecycle events, and settles against the returned
prompt id (or its steer parent's).

* fix(kap-server): scope settlement tracking to the owning agent and dispose on rejection

- The tracker now subscribes through the agent-scoped IEventBus instead of
  the App-scoped IEventService: prompt lifecycle events from other
  sessions never reach it, so a colliding client-chosen prompt id cannot
  trigger a foreign settlement (and the steer re-target only follows this
  agent's parent).
- A bundled submission that rejects after the tracker was installed now
  disposes it on the error path instead of leaking a permanent listener.

* fix(agent-core-v2): keep steered prompts queued until their media intake finishes

Materializing a steered prompt's daemon-ref media awaits a file copy
during which the active turn may finish. Records are now spliced out of
the queue only after that copy completes, and when the turn is gone by
enqueue time they are restored to pending so startNext can launch them
as fresh prompts — their handles always launch or settle.

* fix(agent-core-v2): revalidate the queue and active turn after steer media intake

The daemon-ref copy yields, so settle/abort can consume selected records
and the active turn can rotate meanwhile. Only records still pending are
steered, and only into the turn that was active at entry; records that
vanish from the queue are left to their own launch path, and a missing
turn restores them to pending instead of splicing an unrelated tail
prompt. The intake/queue-preservation contract is documented in the
module header.

* fix(agent-core-v2): steer only the surviving records and keep their media truthful

- The steered content is rebuilt from the records that are still pending
  after the media intake, so an aborted or concurrently consumed record's
  text is never injected (or injected twice) alongside the surviving
  handles.
- The enqueue is wrapped so an activeTurnOnly rejection restores the
  records to pending (the loop throws instead of resolving a missing
  turn, which made the previous rollback unreachable).
- The merged origin now carries the union of every record's bundled
  skillActivations, and prompt.steered publishes the caller-only content,
  so the skill instructions reach the model with their metadata intact
  while the event projection stops leaking internal skill markdown.

* fix(agent-core-v2): harden steer rollback and register bundled prompt ids

* fix(agent-core-v2): strip bundled blocks from prompt.queued and reject partial steers

* fix(kap-server): update session metadata for bundled prompts routed to subagents

* fix(agent-core-v2): restart queue after raced steer rollback and prefix skill blocks in merged steer

* fix(agent-core-v2): block queue advancement during steer admission

* chore: drop the changeset for server-only protocol plumbing
2026-08-18 14:57:13 +08:00
liruifengv
eaa3969dd3
feat(kap-server): add page mode, updated_before, and batch archive/restore to v2 sessions (#2983)
* feat(kap-server): add page-number mode and total to GET /api/v2/sessions

The v2 session list gains a stateless 1-based `page` parameter beside the
opaque page_token cursor for admin-style lists that jump arbitrarily:
each request stays a full independent snapshot, no token is minted, and
`page` + `page_token` together fail 40001. Every response now carries
`total` (the filtered/sorted set size) in both pagination modes.

* feat(kap-server): add meta.updated_before filter to GET /api/v2/sessions

Symmetric with meta.updated_after (inclusive boundary, Unix ms), applied
at the edge over the drained set and bound into the page_token query
fingerprint like every other condition.

* feat(kap-server): add POST /api/v2/sessions:archive and :restore batch endpoints

Batch archive/restore for session-management views: { ids } (non-empty,
≤5000 unique after dedup) answers per-item results in input order with
succeeded/failed counts — only a body validation failure fails the whole
request, and an unknown id folds into its own item as 40401.

The live/cold split keeps the batch cheap: a session with a live handle
goes through the full ISessionLifecycleService chain (agents drain,
scope teardown, mirror drain), while a cold session is never
materialized — the new setColdSessionArchived helper in agent-core-v2
patches the persisted state.json (archived/archivedAt, updatedAt
preserved, mirroring setArchived's touchUpdatedAt: false semantics),
mirrors the flipped summary into the read-model queue, and republishes
the same event.session.archived bus event the live lifecycle emits
(:restore publishes nothing, matching the live restore). Hot items run
with bounded concurrency and the batch ends with one shared
ISessionIndexMirror.drain().

* docs(server-api): document v2 sessions page mode, total, updated_before, and batch archive/restore

* fix(kap-server): deep-import workspace lifecycle symbols in the v2 sessions route

CI's tsgo/rolldown (Linux) fail to bind liveHandlerForSession and
IWorkspaceLifecycleService through the agent-core-v2 package-root
barrel even though it re-exports them; the same files use the
established deep-import pattern already used for the git domain.

* fix(kap-server): inline the live-handler lookup in the batch route

The previous deep imports still fail to resolve on CI's Linux toolchain
(tsgo TS2307, rolldown MISSING_EXPORT) while every other module path
from the same package binds fine. Keep the route self-contained: the
hot-path lookup is a five-line loop over IWorkspaceLifecycleService's
handlers (mirrors agent-core-v2's liveHandlerForSession), and the tests
assert non-materialization behaviorally via the live map instead of
importing the same two symbols for spies.

* fix(kap-server): drive the batch hot path through getLiveSessionById

The phantom only hits the workspaceLifecycle-group symbols in these two
files on CI's Linux toolchain; getLiveSessionById is observed to bind
fine there. It returns the session's live scope directly (no resume),
which is exactly what the batch hot path needs.

* refactor(kap-server): move the batch live/cold split into agent-core-v2

setSessionArchivedBatch owns the split next to the cold patch: live
sessions go through the full lifecycle chain via the workspace handler
accessor (the v1-proven resolution path), cold sessions through the
direct write. The route becomes a thin wire-code adapter, and the batch
tests assert the live chain behaviorally (disposal, events, index)
instead of spying through scope accessors.

* fix(agent-core-v2): import sessionLookup relatively from coldSessionArchive

The '#/app/workspaceLifecycle/*' specifier resolves from src/ and
src/app/* files on CI's Linux toolchain but not from
src/workspace/sessionLifecycle/ (tsgo TS2307, rolldown follows); a
relative import bypasses the package-imports mapping.

* fix(agent-core-v2): migrate the batch hot path to ISessionManager

Main's workspace/session DI refactor removed the workspaceLifecycle
lookup modules; the live branch now goes through the App-level
ISessionManager (the same entry the v1 action route uses post-refactor)
with getLiveSessionById from the new sessionManager lookup.

* feat(kap-server): add the id,archived item projection to GET /api/v2/sessions

fields=id,archived trims each item to { id, archived } for
select-all-matching flows (the session admin page's Gmail-style
select-all). Only that projection gets the relaxed page_size ceiling
(10000); unknown fields, non-pair subsets, and include=git combinations
are 40001, and the projection binds into the page_token fingerprint so
shapes never flip mid-pagination.

* fix(agent-core-v2): serialize the batch cold write against in-flight resumes

Codex review on #2983: while a resume is in flight the live registry
hides the handle, so the batch route could classify the session as cold
and its direct write would race the materializing metadata service (its
stale in-memory document wins the next write, silently un-archiving the
session after the endpoint reported success).

The batch now settles the resume first: SessionManager registers the
whole resume promise synchronously at the App level (controllerForSession
is async, so the controller's own resuming map learns about it a few
microtasks late) and whenResumeSettled awaits it before classification —
a settled resume lands the item on the live chain, a failed one falls
back to the cold path. Also folds the module header down to the
package's external-role comment convention.

* fix(agent-core-v2): publish SessionArchived as an Event2 class in cold archive

* fix(agent-core-v2): serialize batch archive/restore with session lifecycle transitions

* fix(agent-core-v2): serialize session delete with the lifecycle chain

* fix(agent-core-v2): mirror the persisted metadata on cold archive, not the index summary

* docs(agent-core-v2): bring sessionManager comments and new tests to package conventions

* fix(agent-core-v2): normalize legacy session metadata before the cold archive write

* fix(kap-server): serialize the v1 single-session archive with the lifecycle chain

* chore: drop changesets for internal-only protocol work

* fix(agent-core-v2): encode cold-archived metadata for v1 readers

* fix(agent-core-v2): serialize fork and createChild with the source session's chain

* refactor(agent-core-v2): chain every session lifecycle method and hand batch sections unguarded ops

* fix(agent-core-v2): propagate failed resumes to the next settle

* fix(agent-core-v2): roll back the unannounced handle when a resume fails mid-materialization

* fix(agent-core-v2): read and migrate the legacy session-meta location on cold archive

* fix(agent-core-v2): serialize explicit-id session creation with the lifecycle chain

create() with a caller-supplied sessionId bypassed the per-session chain,
so a concurrent batch archive could classify the half-created session as
cold and write archived state that the live metadata service later
overwrites. Creation now queues on the target id's chain whenever an
explicit id is present.

Also type the resume-failure maps as Error and normalize at the catch
site, satisfying only-throw-error.

* style(kap-server): strip comments from the session routes per the no-comments convention

* fix(agent-core-v2): serialize explicit fork and child target ids on the lifecycle chain

fork() and createChild() with a newSessionId locked only the source id, so
a batch archive of the target could slip into the creation window: the
index already knows the half-created session, the batch writes archived
state to its document, and the fork's in-memory metadata later overwrites
it. Both operations now acquire the deduped, sorted key set so multi-key
sections always take locks in one deterministic order.
2026-08-18 13:57:37 +08:00
Haozhe
5ae82cd5bc
feat(agent-core-v2): disable the tower feature entirely (#3023) 2026-08-18 13:38:13 +08:00
Haozhe
8267bb8fce
feat(kap-server): add workspace fs:suggest file completion endpoint (#3019) 2026-08-18 11:53:48 +08:00
Haozhe
3ded08084a
fix(protocol): expose turn ended event time (#3011)
* fix(protocol): expose turn ended event time

* fix(protocol): expose turn ended event time

* chore(changeset): remove patch release entry

* test(node-sdk): align background task parity expectations
2026-08-18 10:16:12 +08:00
Haozhe
1ab19190e9
refactor(agent-core-v2): strip comments from agent-core-v2, kap-server, and transcript (#3010)
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-08-18 00:30:49 +08:00
bj456736
09976b0914
feat(cli): add --web-title and expose it via /meta (#2989)
* feat(cli): add --web-title and expose it via /meta

* refactor(kap-server): pass optional web_title directly in /meta

Per the repo rule for optional object properties, pass undefined directly
instead of a conditional spread; serialization omits the unset value.

* fix(cli): sync web bundle with instance tab title support

The committed dist-web bundle predates the document title feature, so a
released `kimi web --web-title` served a client that never read web_title.
Rebuilt from code-app (feat/web-document-title) via sync:web; the bundle
now titles tabs from web_title or the active workspace directory.

* ci: retrigger checks after flaky harness cleanup failure

---------

Co-authored-by: wbxl2000 <wbxl2000@outlook.com>
Co-authored-by: bj456736 <bj456736@users.noreply.github.com>
2026-08-17 20:19:15 +08:00
Haozhe
2265305e81
refactor(agent-core-v2): replace defineOp/Model with Event2 dispatch and replayable states (#2909)
* refactor(agent-core-v2): replace defineOp/Model with Event2 dispatch and replayable states

- replace defineOp/Op/OpDescriptor/toEvent and defineModel/defineCheckpointedModel with Event2 subclasses: durable classes declare static durable + schema, serialize() keeps the wire record shape byte-frozen, transient classes stay off the journal
- define states via defineState(...).replayable(...).on(Event2, fold): immer produceWithPatches folds with atomic prepare/commit, .undoable() trait drives prompt-submit checkpoints and context.undo, ephemeral kv keys keep imperative set
- degrade IWireService to a journal adapter; the agent event dispatcher owns the pipeline (fold -> set -> appendRecord -> publish) and silent restore
- align downstream event surfaces: kap-server WS envelope timestamp from event.time, klient event schemas gain time, node-sdk/acp-server/print wiring updated
- rewrite gen-wire-manifest/gen-state-manifest for the unified registry and replace the op-uniqueness lint with event-uniqueness

* fix(ci): repair Event2 prompt and media projections

- restore prompt admission and session media materialization
- align transcript, WS, SDK, and replayable media state projections
- update affected tests and generated state manifest

* fix(ci): update prompt event and projection expectations

- update snapshots for the durable prompt.accepted event
- normalize prompt.steered media in transcript projections
2026-08-17 17:38:50 +08:00
7Sageer
d833a1a893
feat: engine-native image references via kimi-file:// media resolver (#2593)
* feat: engine-native image references via kimi-file:// media resolver

* fix(agent-core-v2): regenerate state manifest for media resolver rename

* feat(agent-core-v2): add audio MediaKind and tag/ref fold helpers to media ref contract

* fix(agent-core-v2): synthesize image path tag when degrading bare file references

* fix(agent-core-v2): scrub dangling alias re-exports in contract type generator

* feat(transcript): project paired media tag+ref as single attachments in read models

* fix(kimi-code): fall back to inline image when cache write fails after upload

* fix(agent-core-v2): pair media path tags with refs by adjacency and path, keep unpaired tags

* fix(kap-server): fold media tag+ref pairs out of prompt snapshot projection

* fix(kap-server): list attachment-only prompts as empty user messages

* fix(kap-server): keep live attachment ids across transcript overlay and heal

* fix(kap-server): keep promptAttachments off the legacy session event wire

* fix(kap-server): inherit the backfilled turn header on mid-turn terminal projection

A projector that attached after turn.started built the terminal turn.upsert
with an empty header, and the whole-header replace downstream wiped the
backfilled origin / prompt / attachmentIds — only the debounced best-effort
heal could restore them. Fall back to the producer store's seeded header
(via a new optional ProjectorLookups.turn) when currentTurn misses, and
cover the mid-turn attach path with a service-level regression test.

* refactor(agent-core-v2): move media ref contract out of kosong into agent/media

The kimi-file:// daemon reference grammar, media path tags, and the tag/ref
fold are engine-internal conventions, not provider-wire contract; keep
src/kosong untouched. Root exports and SDK re-exports are unchanged.

* feat(agent-core-v2): materialize prompt media into the session media dir

Pasted and uploaded media now materialize under the session's own media/
dir instead of the shared cache, so the copies follow the session's
lifecycle: fork carries them along, session deletion cleans them up.

A new Session-scope ISessionMediaStore owns the dir: atomic tmp+rename
materialization with a unified extension policy, and canonical-vs-hint
display-path resolution. The persisted ?path= is a write-time snapshot —
readers prefer the session-canonical location, so fork and home relocation
never hand the model a dead path. Prompt intake normalizes every daemon
reference through the single enqueue funnel (REST edge, SDK prompt/steer,
gateway), serialized in arrival order to keep the FIFO across the async
file I/O. The kap-server edge materializes through the same store with a
shared-cache fallback, and the request-time resolver refreshes stale
persisted and memoized path tags; a claimed video reference degrades to
its tag alone instead of duplicating it.

* fix(agent-core-v2): take prompt media intake off the enqueue critical path

The record now joins the FIFO synchronously and its daemon-ref intake runs
as a per-record promise, awaited by the launch and steer paths before the
message is consumed — queue order, list/abort visibility, and prompt
submission latency no longer wait on file I/O, and a slow intake no longer
head-of-line blocks later prompts. The launching record is tracked so abort
and clear stay reachable inside the launch window; startNext re-checks
cancellation after every await (intake race, hook, turn admission), a
cancelled record is never re-queued, and a compaction requeue waits for
onDidFinishCompaction instead of busy-looping the scheduler.

* fix(agent-core-v2): record the claiming ref in the media path-tag pairing

pairMediaPathTagRefs now exposes claimingRefByTagIndex, and claimingRefIndex
reads it instead of recovering the claimer by path equality — which
mis-attributed a tag when two different fileIds carried the same path in an
interleaved sequence, breaking the pair and leaking the tag as user text.
Also covers the memoized-video-tag claimed-drop branch.

* fix(transcript): fold upload pairs in user-slash turns and pin pairing parity

The cold rebuild's user-slash branch now folds the turn-opening input like
any user turn (claimed tag out of the prompt text, one attachment entity),
matching the live projection. The ref extraction is consolidated into the
contract module (daemonFileRefFromPairingPart, the mirror of the engine's
daemonFileRefFromPart) and the mirror carries the new claimingRefByTagIndex
map. A new kap-server parity test imports both implementations and asserts
identical pairings over shared fixtures, so the engine/mirror pair can no
longer drift silently.

* fix(kap-server): fold upload media tags out of the search index

The global search indexer concatenated every text part of a persisted user
message, so the upload pair's <image path> tag made pure-image prompts
searchable and wrote the materialization path into the index — breaking the
module's documented pure-image invariant and diverging from the live route.
textOfContent now folds the pair like every other read model (with a
fold-safe coercion for malformed wire parts). Also pins the prompt-media
cache-dir fallback with a read-only session media dir test (skipped as root).

* feat(node-sdk): re-export the media fold helpers and cover the v1 uploadFile rejection

foldMediaPathTagRefs and matchSingleMediaPathTag join the daemon
file-reference helper re-exports so hosts can fold the upload tag+ref pair
without importing agent-core-v2; the v1 harness's uploadFile not_implemented
rejection is pinned by a test.

* fix(kimi-code): fold upload pairs in replay/export and keep media tags atomic in steer input

Resumed-session replay rendered the upload pair raw — the <image path> tag
as user text and the kimi-file:// url as an XML-ish reference — and the
markdown export leaked the tag into both the turn body and the overview
topic. contentPartsToText and the exporter now fold the pair, and daemon
references render as a bare [image]/[video] placeholder. combineSteerInput
moves to tui/utils/steer-input and no longer merges a standalone media tag
into adjacent text, which would have broken the engine-side pairing for
steered image messages.

* fix(kimi-code): drop the steer separator before a leading media tag

A queued pure-image message opens with a standalone `<media path>` tag,
which combineSteerInput keeps atomic. With the previous item ending in a
media part, the '\n\n' separator landed as a stranded whitespace-only text
part between the media part and the tag, normalizePromptInput rejected the
steer, and the already-cleared queue lost the messages. Treat a leading
standalone tag as media so the separator is dropped there.

* fix: clean staged media lifecycle

* refactor(agent-core-v2): narrow the mediaRef root exports and drop a deprecated alias

* fix: keep staged media alive through turn

* fix(agent-core-v2): reject non-upload ids at the session media store

A daemon reference's fileId becomes a storage key in the session media
store, but only the file domain validated the id shape — a crafted
kimi-file://<id> reaching the request-time resolver's canonical-read
fallback could traverse out of the session media dir. Share the file
domain's id regex and guard every store entry point: reads miss,
materialize declines, and the display path falls back to the hint.

* fix(kap-server): project steered prompt content without leaking daemon refs

prompt.steered published the raw engine content parts — kimi-file://
refs carrying the absolute materialization path plus the paired
<media path> tag — to both the legacy session_event wire (whose schema
declares the protocol content shape) and the transcript prompt entity.
Route both through one shared prompt-content projection: the upload
pair folds into a single {kind:'file'} part, matching the REST prompt
list and the no-path-leak rule every sibling surface already follows.

* refactor: align daemon-ref naming and drop a duplicate re-export

The deprecated videoResolverService alias also re-exported
mediaResolvedKey, which made the package root's star exports ambiguous
and silently dropped the name. The new transcript contract mirror now
uses the canonical daemon-ref vocabulary instead of the deprecated
kimi-file spelling.

* test(agent-core-v2): pin image abort rethrow, video canonical read-through, release-once

Mirror the video abort contract on the new image path (an aborted read
cancels the request instead of degrading to a tag), cover the video
fallback that uploads the session-canonical bytes after the transient
upload is released, and assert the staged-upload release fires exactly
once on the intake success path.

* fix(kimi-code): bind goal-steer staging leases to the running turn

sendMessageInternal read the turn context only after beginSessionRequest
had cleared it, so a steer buffered into a running goal turn never got
its staging lease bound — the staged daemon upload and cache copies
lived until session close instead of being released at the consuming
turn's end. Capture the live turn id before the reset (only while a
turn is actually streaming; the id outlives its turn otherwise).

Also move the staging-lease state machine off the KimiTUI coordinator
into a self-contained StagingLeaseTracker with injected effects, drop
the duplicate media-tag builder in image-placeholder in favor of the
SDK helper, and fix the paste-in-flight comment to match the gate's
real granularity.

* fix(kap-server): project prompt.queued content without leaking daemon refs

The broadcaster projected prompt.steered and stripped turn.started
attachments but forwarded prompt.queued raw, leaking kimi-file:// URLs
and absolute materialization paths to every subscribed WS connection
and the journal. Fold the tag+ref pair into a {kind:'file'} part, same
as steered.

* fix: keep compressed uploads retrievable and close the steer abort window

Two review fixes around prompt media intake:

- The compressed re-save was released right after intake (and carried a
  1h expiry) while every client read model projects its file id,
  leaving historical compressed images unfetchable. Keep the re-save as
  an ordinary upload; roll it back only when preparation or submission
  fails before the engine takes the prompt. The engine's
  PromptInput.release hook loses its only producer and is removed.
- A prompt aborted while its steer awaited the loop's step assignment
  was flipped back to 'steered' and its content could still
  materialize into a later turn. Re-check the reservations after the
  assignment await and abort the undispatched request when the check
  fails.

* perf(agent-core-v2): memoize inlined image parts across request steps

A successful image inline depends only on the immutable upload bytes, so
it is memoized per file id (size-bounded) in media.resolved and reused
across steps, retries, and media-recovery reprojections instead of
re-reading and re-encoding on every request. Degrade forms are never
memoized since they depend on the message's tag pairing. Also make the
never-empty message placeholder kind-aware (video vs image).

* refactor: author media tag+ref pairs in the engine prompt intake

Edges (TUI, kap-server REST) now submit bare kimi-file references and the
engine intake materializes the bytes, synthesizes the paired media path
tag, and falls back to the shared cache dir when the session store is
unavailable, replacing per-edge pair construction and duplicate
materialization copies.

Thread the prompt id from submission through to turn.started (REST
prompt_id, WS event, SDK prompt option) so the TUI binds staged-media
leases to turns exactly; the origin heuristic stays as fallback and
ambiguous claims now surface a staging_lease_invariant telemetry warning.

Also lands the pending resendable-extraction fix for cache-hint resubmits
after a session switch.

* fix: decouple media persistence from prompt intake

* refactor(agent-core-v2): project the turn prompt in a single fold pass

* test: slim redundant media-ref coverage across layers

Fold duplicate pinning of the same media tag+ref rules into shared
helpers and it.each tables, and drop assertions that restate behavior
already covered at another layer:

- drop the kimiFileUrl alias describe (mediaRef.test.ts covers the
  aliased functions with more cases)
- drop pairMediaPathTagRefs describe in favor of the parity fixtures
- merge the identical prompt.steered/prompt.queued broadcast tests
- parameterize the resolver degradation matrix and prompt intake
  fixtures (enqueueMedia/gatedImage/expectMediaPair helpers)
- drop REST-level context-memory pairing assertions (engine-level
  intake tests pin the same shapes); keep the caption->system-reminder
  assertion, the only cover of extractCompressionCaptions
- drop the turn-finish-during-intake steer-cancel vector and the
  switch-session release driver test (unit-level lease tests remain)

Net -762 lines; 645 tests green across agent-core-v2, kap-server,
transcript, node-sdk, klient, and the TUI.

* chore: fix oxlint warnings introduced by image-file-ref changes

* fix: harden image file reference lifecycle

* fix: close image reference lifecycle gaps

* fix: preserve session media paths on replay

* chore: streamline image-file-ref changesets

* refactor: make daemon media references self-contained, dropping tag+ref pairing

A daemon-ref media part now carries everything a read model needs — the
kind from the part type and the materialization path from the reference's
`?path=` — so prompt intake no longer authors a paired `<media path>`
tag, and the pairing/fold machinery (pairMediaPathTagRefs /
foldMediaPathTagRefs and their mirror copy) is deleted across the engine,
transcript, kap-server, node-sdk, and the TUI. The request-time resolver
synthesizes the degrade tag from the reference path whenever bytes cannot
reach the provider. Standalone tags stay user-visible text, and never
reach the search index or prompt metadata.

* fix: reconcile image file references with main after rebase

Main removed the agent RPC aggregation layer (agent/rpc) and moved
LifecycleScope to app/scopes. Fold the branch's RPC-side behavior into
the new structure: PromptPayload carries promptId/disabledTools, and
AgentPromptService.submit admits the client-chosen id through the
reservation (duplicate rejects before any session state changes) and
applies the denylist through toolPolicy. Regenerate the wire/state
manifests.

* fix(kimi-code): run paste ingestion in the background, wait bounded at submit

The paste callback awaited compression + original persistence + the
daemon upload while CustomEditor queued every keystroke, so a slow
ingestion stalled all typing. Settle the callback once the placeholder
lands and track the rest as ImageAttachment.pending; the send path gives
a referenced pending ingestion a bounded wait (2s) so paste-then-Enter
still submits the compressed/daemon-ref form, and falls back to the
inline form when ingestion has not finished. Media-free submits stay
fully synchronous.

* fix(protocol): mirror prompt_id in the shared prompt submission schema

kap-server's local REST schema accepts a client-chosen prompt_id, but
the shared promptSubmissionSchema stripped it as an unknown key, so
clients validating through @moonshot-ai/protocol lost the id and the
turn.started promptId correlation never matched.

* fix(klient): normalize file-store errors to public RPC errors on both transports

The fileService save/get wire adaptation ran outside the dispatcher's
error normalization, so a stale or expired upload id surfaced as the
engine's raw Error2 on the memory transport and as a generic 50001 on
ipc. Map file.not_found to the public NOT_FOUND RPCError in the shared
dispatcher so both transports reject identically, and pin the parity in
the conformance suite.

* fix(agent-core-v2): keep launching media prompts visible in the queue snapshot

startNext shifts the launching record out of pending before its media
intake settles, so list()/GET /prompts reported neither an active nor a
queued prompt during the intake window even though the submission was
accepted and abortable. Report the launching record as still queued,
matching the prompt.queued event already published for it.

* fix(node-sdk): strip internal promptAttachments from SDK turn.started events

The in-process v2 event mapper forwarded the whole domain event, so SDK
session.onEvent consumers saw the transcript-projection-only
promptAttachments field that kap-server explicitly strips from the WS
wire event. Drop it in the mapper so both consumers share the same
turn.started field set.

* fix(kimi-code): align staging lease id multiplicity with retain count

A lease's flat id list conflated two cases: one submission referencing
the same image twice (one retain) and a batched steer merging two queued
messages sharing the image (two retains). Occurrence-wise release
over-consumed in the first case and batch-wise release would
under-consume in the second. Dedupe each extraction's ids at the lease
creation sites so list multiplicity always equals the retain count, and
release one retain per occurrence.

* fix(agent-core-v2): check video_in before honoring memoized video uploads

The video memo hit path returned a cached ms:// part before the current
model's capability check, so switching to a same-provider model with
video_in:false sent a video part the model cannot accept instead of
degrading to the path tag. Gate on capability first, mirroring the image
strategy.

* fix(kimi-code): keep recalled queued media staged instead of releasing it

Recalling a queued media prompt into the editor is not a discard, but
the recall path released the staged files: image attachments lost their
daemon upload (resubmit silently downgraded to inline), and a recalled
video's cache copy was deleted even though re-materialization needs a
source that may already be gone. Recall now consumes only the retain
(the next submit re-retains), retires the cache copy to session
lifetime, and rebases the video attachment onto that copy.

* fix(agent-core-v2): count launching media prompts in prompt.queued queueLength

startNext shifts the record into launchingItem before publishQueued
computes the count, so a media prompt's prompt.queued reported
queueLength 0 even though the prompt is accepted, abortable, and listed
as queued. Compute the count from the same snapshot list() exposes.

* refactor(agent-core-v2): drop the session media shared-cache fallback

Intake keeps the upload-backed reference when the canonical write fails
instead of double-writing into an unowned global cache scope; the session
media store's reads collapse to the canonical scope, and non-filesystem
deployments no longer write every media blob twice.

* refactor(agent-core-v2): stop persisting materialization paths in daemon file references

The kimi-file:// reference persisted in context memory bundled a durable
identity (fileId) with a perishable machine-local absolute path (?path=),
which forked sessions and home relocations would stale. The reference now
carries only the file id; the display path is derived from the session
media store by file id at read time. Parsers tolerate and strip the legacy
?path= query so old records keep resolving.

* fix(agent-core-v2): skip atomic-write temp siblings in session media by-id resolution

The fs backend stages atomic writes at <key>.tmp.<pid>.<hex> next to the
target key, and the media store's prefix-listing predicate matched them, so
a lookup racing an unfinished materialize could return the partial copy as
the canonical file.

* fix(kimi-code): close the staging-lease gap between extraction and dispatch

Create the staging lease right after extraction so every pre-dispatch exit
releases through the tracker: validation/session failures release it,
queueing defers it to the queue item's raw ids/paths, and the cache-hint
stash takes over ownership. A forgotten exit now degrades to an unclaimed
lease swept at session close instead of a permanently retained upload.

The cache-hint restore exits (dismiss, chained restore, session switch
during fetch, failed compact/new-session) previously returned only the
text to the editor, leaking the extraction's retains and staged cache
copies. They now go through queue-recall semantics: retains are consumed,
staged copies retire, and recalled videos rebase onto them.

* fix(agent-core-v2): bound the inline image memo with a private byte-budgeted LRU

A memoized inline image part pins a multi-MB base64 string, and the agent
state registry's snapshot/inspect path serializes every registered state
in full — so the memo no longer lives in agentState. It is now a private
per-file-id LRU with the existing 8MB per-entry cap plus a 64MB total
budget; eviction simply re-reads the bytes on the next request. The video
memo stays in agentState.

* fix(kap-server): fall back to the staged upload on the session media route

Prompt intake materializes bytes into the session media store
asynchronously and best-effort, but a session_media ref is projected to
clients as soon as the prompt is queued — so the download route could 404
during the intake window, and forever after an intake failure. The route
now reads the canonical session store first and falls back to the App-scope
staged upload, adapting it to the same served shape; only a double miss is
a 404. The header note also records that resolving the store resumes cold
sessions, an accepted short-term semantic with a TODO for a cold-read
channel.
2026-08-17 13:11:28 +08:00
Haozhe
04d23e2dab
fix(agent-core-v2): unify text/binary classification for UTF-8 multibyte files (#2972) 2026-08-17 11:05:33 +08:00
Luyu Cheng
61591bce09
feat(agent-core-v2): bundle multiple skill activations into one prompt submission (#2934)
* feat(agent-core-v2): support grouped multi-skill prompt submissions

Add IAgentSkillService.promptWithSkills: one or more skill activations
are validated up front (an unknown or empty submission rejects with no
side effects), recorded with a shared submissionId, and enqueued ahead
of the prompt through the prompt queue's messagesBefore support, so the
whole group materializes atomically as a single turn. Undo cuts, the
transcript projection, and the undo precheck treat the group as one
unit (stopping at the next anchor even when submission ids collide);
hook-result messages are skipped like injections during those walks.
Submit hooks run against every message of the group, and user-slash
skill activations count as user-submitted content for the UserPromptSubmit
hook's origin filter.

Surface it through the contract layers: protocol gains submissionId on
the user / skill_activation origins and on the skill.activated event
(kap-server zod mirrored), klient exposes agentSkillContract.promptWithSkills
with parity assertions, and the SDK grows session.promptWithSkills —
implemented on the v2 engine and rejecting loudly on the deprecated v1
engine, which is otherwise untouched.

* fix(agent-core-v2): reject empty skill lists in grouped prompt submissions

- Validate that promptWithSkills receives at least one skill, enforced in
  the engine and as a non-empty constraint in the klient wire schema.
- Restore the released versions and changelog sections for agent-core-v2,
  klient, and node-sdk that the branch cut had reverted.
- Move statement-level narration into the owning file headers per the
  package comment conventions.
- Align the hook-result undo tests with the reachable record ordering
  (hook results are recorded before the group materializes).

* refactor(agent-core-v2): bundle grouped skill activations into the prompt message

Replace the submissionId-correlated message group with a single bundled
user message: the rendered skill blocks precede the caller's parts in the
content, and every activation's metadata rides the prompt origin's new
skillActivations field. The bundle is one anchor by construction, so undo
needs no group-cutting logic and the messagesBefore prompt seam disappears;
the submit hook fires once per submission. skill.activated still fires per
skill (transient ops, live-only); resume rebuilds the per-skill view from
the prompt origin. Contract chain (protocol, kap-server, klient, node-sdk)
drops submissionId accordingly.

* fix(agent-core-v2): keep bundled skill blocks out of prompt-facing projections

- The transcript cold rebuild expands a bundled prompt's origin
  skillActivations back into per-skill markers (the live path already
  projects them from skill.activated events).
- turn.started.prompt, the session title excerpt source, and the fork
  lastPrompt now derive from the caller's own parts, excluding the
  rendered skill blocks the engine prepends to the bundled content.
- Drop the redundant undefined unions from the new origin fields.
- Move the activateSkill test narration into the file header.
2026-08-17 00:09:18 +08:00
Haozhe
84da6629b1
refactor(agent-core-v2): decouple workspace from session DI via runtime binding (#2961)
* refactor(agent-core-v2): decouple workspace from session DI via runtime binding

* fix(agent-core-v2): unblock session external hooks and scope workspaceMcp seeds

- externalHooksService: inject App-level ISessionManager instead of the
  unregistered ISessionLifecycleService so SessionStart/SessionEnd hooks
  actually activate in production; keep sessionId matching and tolerate
  absent lifecycle events
- workspaceMcpService: ignore onWillCreateSession events whose session
  belongs to another workspace, preventing cross-workspace
  ISessionMcpHandle seed overrides
- update externalHooks integration tests, agent harness, and workspaceMcp
  tests; add reloadSources coverage in skillCatalog tests

* fix(agent-core-v2): honor the bound runtime in prompt context, swarm spawn, and ACP sessions

- map system-prompt cwd, directory listing, and additional dirs through
  RuntimeWorkspaceView, and skip the listing when the bound runtime has
  no fs capability
- pass the caller agent's runtime binding to AgentSwarm child creation
  and prompt-prefix execution instead of hardcoding local
- expose the ACP client filesystem through the ACP session runtime and
  build its shell/path environment from the probed host instead of
  hardcoded Linux
- dispatch klient facade createChild to sessionManager.createChild so
  child sessions keep their parent markers

* fix(agent-core-v2): resolve routed fs and tool paths with runtime path semantics

- WorkspaceFsService resolves via the bound runtime's RuntimePath (extended with basename/dirname) instead of node:path, so mapped roots such as C:\\repo stay runtime-local.
- Read/Write/Glob/Grep pass skill roots through mapRoots via RuntimeWorkspaceView input, matching Edit.
- acp-server unbinds session runtimes on session/close, not only on delete.
- apps/kimi-code drops the /runtime slash command; SDK runtime methods stay.

* fix(agent-core-v2): retire idle session controllers, untrack disposed runtime resources, and rebuild fs watches on generation replace

* fix(agent-core-v2): resolve oxlint errors in runtime lifecycle fixes

* fix(kap-server): untrack download stream from runtime generation on completion

* fix(kap-server): drop meaningless void operator on tracked dispose
2026-08-16 20:24:12 +08:00
tpoisonooo
f492cd7c9e
feat(agent-core): add tower command to orchestrate multi-agents (#2633)
* feat(packages): implement cowork

feat(packages): update throttle control

feat(agent-core): rename to /tower

feat(agent-core-v2): support tower mode

fix(packages): keep tower teardown from stranding submodule worktrees

A plain `git worktree remove` refuses worktrees containing initialized
submodules even when they are clean, so tower teardown silently left
behind exactly the worktrees whose workers had run builds (the failure
only reached the tool report, never the activity log).

The dirty check is the data-loss gate; once it passes, removal always
passes --force (harmless on a clean worktree, and precisely what
bypasses git's submodule refusal). Kept and failed removals now also
land in the activity log as worktree.keep / worktree.remove.failed.

feat(packages): allow the tower to AskUserQuestion, workers still cannot

The tower-mode AskUserQuestion deny only ever fired on the tower itself:
workers never enter tower mode, and their tower-worker profile simply
does not list the tool. Drop the deny so the tower can clarify
requirements with the human up front; workers and reviewers stay
ask-less and escalate via TowerSend. Auto permission mode still
disables AskUserQuestion for everyone.

fix(agent-core-v2): import LifecycleScope from #/app/scopes

main moved the enum out of #/_base/di/scope; follow the new location in
the two tower services.

test(agent-core-v2): refresh fullCompaction token expectations

main's #2699 counts compaction tokens on the full-request basis, so the
tower tool schemas (default registry) and the /tower skill catalog entry
(system prompt) shift the pinned numbers: +2789 with the default tool
set, +173 with the explicit harness tool list. The 20k-window test keeps
its shape with a 22k window so the post-compaction floor still fits.

feat(agent-core-v2): tower command support secondary model

fix(tower): disable todo-list tool

feat(tower): reviewer keep primary model

fix(tower): tower worker call for authroization

update

* refactor(tower): drop agent-core-v1 version

* feat(tower): remove builtin.ts

* fix(agent-core-v2): verify the recorded base branch before tower merges

* fix(agent-core-v2): activate tower missions only after a successful spawn

* chore(kap-server): correct the search-service activation comment

* fix(tower): allowActivationWhileBusy for all skill

* update

* update

---------

Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
2026-08-16 15:13:42 +08:00
github-actions[bot]
13d86f8b7b
ci: release packages (#2881)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-14 20:51:17 +08:00
qer
741708f948
feat(kap-server): add plugin marketplace and capability REST routes (#2868)
* feat(agent-core-v2): surface a machine-key note from capability installs

CapabilityEntry.install now resolves an optional note exposed through
CapabilityInstallProgress.note (wire-visible). The webbridge entry
returns 'user-skill-migrated' when it migrates a pre-existing
standalone skill copy onto the plugin-managed one — clients can
localize the migration instead of the skill silently disappearing
from the user's directory.

* feat(kap-server): add plugin management and capability REST routes

Expose the App-scope plugin and capability services over the wire so
non-CLI hosts (desktop, web) can manage plugins and built-in
capabilities end to end:

- GET  /api/v1/plugins, POST /api/v1/plugins {source},
  POST /api/v1/plugins/{id}:{enable,disable,remove}
- GET  /api/v1/plugins/marketplace — catalog (pluginMarketplaceUrl
  server option / KIMI_CODE_PLUGIN_MARKETPLACE_URL env / production
  default) merged on demand with live install state; updateAvailable
  only on strict semver catalog > installed (no semver dependency)
- GET  /api/v1/capabilities, GET /api/v1/capabilities/{id},
  POST /api/v1/capabilities/{id}:install with client-polled progress
- New wire codes 40418 capability.not_found, 40419 plugin.not_found,
  40923 capability.install_in_progress, 40924 capability.unsupported

Mutations flow through IPluginService, so they serialize with other
install paths and fire onDidReload (session skill catalogs and the
capability shelf-install hook converge).

* fix(kap-server): map plugin input errors to 4xx and correct the unsupported test code

- mapPluginError now translates the domain's validation.failed (40001)
  and fs.path_not_found (40409) instead of collapsing client-fixable
  input mistakes (relative source, nonexistent local path) into a
  50001 internal error
- the non-macOS capability install test expected 40923, which this
  branch assigns to capability.install_in_progress; the unsupported
  code is 40924 (macOS runners skip the case, which is why it only
  fails on Linux/Windows CI)

* fix(kap-server): resolve catalog-relative marketplace sources and widen the unsupported-test skip

- The production CDN catalog carries sources relative to the catalog
  URL (./official/*.zip); clients handing them back to POST /plugins
  would hit the local-path normalizer's 40001. Resolve entry sources
  against the configured catalog URL so every returned source is
  directly installable.
- The 40924 install-rejection test only skipped macOS, but kimi-cu is
  also supported on Windows x64 — running it there would start the
  real installer. Skip on every supported platform.

* fix(kap-server): accept the legacy url/downloadUrl marketplace source aliases

Custom catalogs that the CLI already accepts can carry an entry's source
under url or downloadUrl instead of source; the route's strict schema
rejected the whole catalog with 50001. Normalize the aliases before
validation (same precedence as the CLI parser) so those catalogs keep
working through /api/v1/plugins/marketplace.

* fix(kap-server): support local marketplace catalogs and drop conditional spreads

- KIMI_CODE_PLUGIN_MARKETPLACE_URL accepts a plain path or file://
  catalog in the CLI loader; the route only fetched over HTTP, so local
  catalogs 50001'd for desktop/web hosts. Read local catalogs from disk
  and resolve their relative sources against the catalog's directory.
- Replace the marketplace mapping's conditional spreads with direct
  possibly-undefined properties per the repo rule.

* fix: surface capability install notes through klient and convert file:// entry sources

- The klient capabilities contract omitted install.note, so zod parsing
  stripped it and facade callers (node-sdk, TUI) never saw
  'user-skill-migrated'. Add the field and pin it in the facade test
  fixture.
- A marketplace entry source given as a file:// URL fell through to the
  relative-branch and came back as a garbage path; convert with
  fileURLToPath so the advertised source stays installable.

* test(kap-server): keep the new route tests portable to Windows x64

- The capabilities list assertion treated every non-macOS host as
  unsupported, but kimi-cu is supported on Windows x64 — derive the
  expectation from the same platform predicate.
- file:///abs/... is not a valid absolute file URL on Windows (no drive
  root); build the fixture with pathToFileURL from a temp path instead.

* refactor: align the capability note and test helper with repo conventions

- agent-core-v2 keeps explanatory docs in the top-of-file block only;
  the note contract already lives in the capability types header, so
  drop the two member-level doc blocks.
- The plugins route test helper sets the optional fetch body directly
  instead of via a conditional spread.

* fix(kap-server): expand ~ in local marketplace catalog paths

The CLI loader expands ~/ against the home directory; the route read
the path literally, so KIMI_CODE_PLUGIN_MARKETPLACE_URL=~/catalog.json
50001'd for desktop/web hosts while working in the CLI. Share one
localCatalogPath helper (file:// conversion + tilde expansion) between
the catalog read and the relative-source resolver.

* fix(kap-server): expand home-relative marketplace entry sources

A catalog entry with source '~/...' fell through to the catalog-relative
branch and came back as <catalog-dir>/~/... — unresolvable by POST
/plugins. Expand ~ via the shared helper before the absolute/relative
decision.

* fix(kap-server): match CLI field semantics for source aliases and stub the Windows home

- A blank or non-string source no longer shadows the url/downloadUrl
  aliases; the first valid (non-blank, trimmed) of source/url/downloadUrl
  wins, mirroring the CLI parser's stringField.
- The tilde test also stubs USERPROFILE so os.homedir() resolves to the
  fixture home on Windows runners.

* fix(kap-server): read a blank marketplace tier as missing

The CLI parser trims tier and treats a blank as absent (third-party);
the route's enum rejected the whole catalog with 50001. Normalize the
tier alongside the source aliases in the same preprocess.

* fix(kap-server): derive marketplace versions from GitHub release sources

Entries that omit version but encode it in a GitHub release/tag (or
tree/commit) source never surfaced updateAvailable. Derive the version
from the resolved source — same URL shapes as the CLI parser, validated
with the route's strict x.y.z rule (no semver dependency).

* fix(kap-server): fail catalog validation on a source with no usable value

A whitespace-only source with no valid alias passed z.string().min(1)
untrimmed and resolved against the catalog URL into nonsense. Drop the
key during normalization so the schema reports the entry as missing its
source (same outcome as the CLI's 'must define source').

* fix(kap-server): resolve latest versions for bare GitHub marketplace entries

A catalog row whose source is a bare GitHub repo (the production curated
rows are shaped this way) kept version undefined, so updateAvailable
never fired for exactly the entries most likely to update. Resolve the
latest release tag through the /releases/latest redirect — the UI route,
not the rate-limited API — same as the CLI, degrading to no version on
any failure.

* docs(kap-server): note the marketplace version resolution in the plugins route header

* feat(kap-server): mark capability wiring rows in the marketplace response

A client following only /plugins/marketplace + POST /plugins would
install a capability's wiring plugin without its binary runtime, with
no wire-level way to tell. Entries whose id matches a capability's
wiring plugin now carry capabilityId, so clients route them through
/capabilities/{id}:install — the client-side routing pattern the CLI
established (the upstream design that replaced the server-side hook).

* fix(kap-server): fall back to the source-checkout catalog for the default location

When the marketplace location is the built-in default (no server option
or env override) and the fetch fails, read the repo checkout's own
plugins/marketplace.json — the CLI loader's behavior for offline
source-checkout dev. An explicitly configured catalog still fails hard
with 50001. Bundled installs have no checkout file, so the fallback
simply never fires there.

* fix(kap-server): resolve fallback catalog sources against the fallback file

readMarketplaceCatalog returned only the JSON, so entries from the
source-checkout fallback resolved their relative sources against the
(unreachable) CDN URL — coming back as unusable https paths instead of
local directories. The reader now returns the location actually read,
and source resolution uses it.

* fix(kap-server): honor the CLI's marketplace metadata aliases

Custom catalogs using name / shortDescription / websiteURL (accepted by
the CLI parser) lost those fields to schema stripping, falling back to
the entry id. Normalize the aliases in the same preprocess as the
source/tier normalization.

* fix(kap-server): filter marketplace keywords instead of rejecting the catalog

A keywords array with non-string or blank members failed the strict
schema and took the whole catalog down with 50001. Normalize to the CLI
parser's semantics: non-array reads as missing, arrays keep trimmed
non-blank strings only.

* fix(kap-server): treat a blank or non-string marketplace version as missing

The CLI parser reads version through its lenient stringField and falls
through to source-derived versions; the route's schema rejected a
numeric version with 50001 for the whole catalog. Normalize version in
the preprocess like the other fields — the gh-plugin fixture now
carries a numeric version and still derives 2.0.0 from its tag source.

* fix(kap-server): trim marketplace entry ids before the install-state join

A whitespace-padded id survived validation raw and never matched the
installed records (updateAvailable silently lost). Normalize the id in
the preprocess — trimmed, blank rejected — matching the CLI's
requiredString.

* fix(kap-server): gate capability markers to the default catalog

A custom catalog (env or server option) may legitimately carry a
same-id fork of a capability's wiring plugin; marking it capabilityId
would route users to the built-in install. Apply the marker only for
the default catalog (including the source-checkout fallback), matching
the CLI injecting built-in rows only for the default catalog.

* fix(kap-server): compare marketplace versions with real semver

The hand-rolled strict x.y.z check rejected valid semver the CLI
accepts (v-prefixed, prerelease tags), so updateAvailable diverged
between CLI and wire clients. Take the semver package (already in the
monorepo via the CLI) for the update check and the two source-derived
version validators.

* fix(kap-server): validate marketplace entry types and count the dev server as default

- Custom catalog rows with an unsupported type (e.g. integration) were
  stripped by the schema and advertised as installable plugins; the CLI
  rejects the catalog outright. Model the same plugin/managed/guide
  vocabulary.
- scripts/dev.mjs marks its repo-owned catalog with
  KIMI_CODE_PLUGIN_MARKETPLACE_FROM_DEV_SERVER=1 — honor the flag in
  the isDefault check so capability markers and the checkout fallback
  behave exactly like the CLI under the dev marketplace.

* fix(kap-server): join capability rows through their platform wiring plugin id

kimi-cu installs its wiring plugin as kimi-cu-win on Windows x64, so a
catalog row keyed kimi-cu never matched the installed record there (no
installed state, no updateAvailable). The row mapping now knows each
capability's wiring plugin ids and joins through them.

* fix(kap-server): map plugin load failures to 40001

An install source pointing at a directory/zip with a missing or invalid
manifest throws plugin.load_failed — a client-fixable input error that
fell through to 50001. Map it to validation.failed alongside the other
input mistakes.

* build(kap-server): align @types/semver with the workspace version

sherif rejects multiple workspace versions of one dependency; the CLI
pins @types/semver at ^7.7.0.

* refactor(agent-core-v2): share the plugin marketplace client/parser across hosts

The kap-server marketplace route grew its own copy of the CLI's catalog
loading/parsing logic (lenient aliases, blank-means-missing fields,
source resolution, GitHub version derivation) — two implementations of
a public, hand-writable format would drift on every catalog change.
Move the read/parse/version machinery into the plugin domain as
app/plugin/marketplace (pure functions, no DI): the CLI keeps a thin
wrapper owning configured-source resolution and its checkout fallback,
and the route keeps only the wire concerns (install-state merge,
capabilityId markers, error envelopes). plugins.ts drops ~230 lines of
duplicated machinery.

One deliberate behavior fix rides along: tilde entry sources now expand
against the home directory at parse time (the CLI previously passed
them through literally, failing later at install validation).

* docs(agent-core-v2): fold the marketplace module's member docs into the file header

The package convention keeps explanatory comments in the top-of-file
block only; the moved parser carried several function/member-level
JSDoc blocks from its CLI home. The header now carries the format
contract, leniency rules, source/version resolution order, built-in
masking semantics, and the fallback gating rule.

* docs(agent-core-v2): drop the remaining statement comments in the marketplace module

The header carries the rationale (update semantics, GitHub ref shapes,
the releases/latest choice); the convention allows nothing beside
statements.

* fix(kimi-code): import the shared marketplace module by its deep path

constant/app.ts is evaluated on every CLI invocation; re-exporting from
the agent-core-v2 root would pull the whole engine module graph into
startup. The package's wildcard subpath export lets both CLI files take
only the pure marketplace module (node builtins + semver).

* feat(kap-server): fan plugin and capability lifecycle out as global WS events

Clients currently poll the plugins/capabilities REST surfaces and can
hold stale rows while another client mutates the set. Publish two global
events instead:

- event.plugin.changed — fired off IPluginService.onDidReload, so any
  install/enable/disable/remove from any client reaches every host
- event.capability.changed — every capability install progress
  transition (CapabilityService gains onDidChangeInstall), so rows
  update live and settle is observable without polling

Both ride the existing global fan-out (no subscription needed) and are
documented in the wire schema registry.

* fix: register the lifecycle events in the wire union and tidy the contract header

- event.plugin.changed / event.capability.changed were declared but not
  part of agentEventSchema, leaving the wire catalog incomplete.
- The onDidChangeInstall member doc moves into the capability contract
  file header (package comment convention).

* feat(protocol): mirror the plugin/capability lifecycle events in the shared WS schema

Clients and e2e harnesses validating server frames against
@moonshot-ai/protocol would reject event.plugin.changed /
event.capability.changed. Register both in the shared catalog (TS
interfaces, zod schemas, and both unions), matching the
model_catalog.changed precedent for global events.

* fix(kap-server): prefer the platform wiring plugin when joining capability rows

A stale same-id record (e.g. a raw kimi-cu plugin next to the real
kimi-cu-win wiring on Windows x64) previously won the join, showing the
wrong installed state and update availability. Capability rows now join
through the wiring plugin ids in platform preference order before
falling back to the catalog id.

* fix(kap-server): put the github metadata of plugin summaries on the wire schema

GitHub-sourced plugin summaries carry github {owner, repo, ref,
installedSha} from the domain; the route serializes raw domain objects,
so the field reached clients undocumented. Declare it in
pluginSummarySchema so the OpenAPI surface matches reality.

* test(node-sdk): cover the new lifecycle events in the exhaustive switch

The event-type exhaustiveness test broke when the shared protocol union
gained event.plugin.changed / event.capability.changed.

* fix(kap-server): mark capability progress events volatile

Per-chunk download progress transitions ride the same fan-out as
durable frames and were being persisted to the __global__ journal —
hundreds of stale frames per install. event.capability.changed is
live-only state, so it joins the volatile list alongside
event.di.unit_changed; the settle frame stays recoverable via a direct
capability read. event.plugin.changed remains durable (rare, and a
reconnecting client should replay it).

* feat(kap-server): inject built-in capability rows into the default catalog response

The checked-in production catalog carries kimi-webbridge but not
kimi-cu — the CLI injects built-in rows client-side, so wire clients
never saw Kimi Computer Use in /plugins/marketplace. For the default
catalog the route now appends supported capabilities the catalog lacks
(static descriptors via ICapabilityService.describeCapabilities — no
detector probes), marked with capabilityId and a capability:<id>
sentinel source so installs still route through the capability
surface.

* fix(kap-server): run injected capability rows through the install-state join

The injected kimi-cu row hardcoded installed: undefined, so an
already-installed capability still read as installable. Injection now
happens before projection, so injected rows get the same backing-plugin
join (installed state, update badge, capabilityId marker) as catalog
rows. Also moves the describeCapabilities note into the contract header
(package comment convention).

* test(kap-server): gate the injected-row assertions on platform support

kimi-cu injects only where supported (macOS / Windows x64); on Linux CI
the row is correctly absent.

* fix(protocol): classify capability progress as volatile in the shared catalog

kap-server never journals event.capability.changed (it is in the
server-local volatile list); shared-protocol clients reading
isVolatileEventType would treat per-chunk progress frames as durable
and replayable. Mirror the classification.

* fix(kap-server): hide capability rows on unsupported platforms

Catalog-carried capability rows (kimi-webbridge in the default catalog)
were marked with capabilityId regardless of host support — on an
unsupported platform clients would route into an impossible capability
install. Rows whose capability is unsupported are now excluded from the
default-catalog response entirely (the CLI hides its built-in rows the
same way).
2026-08-14 15:45:34 +08:00
7Sageer
249d8faa34
fix(agent-core-v2): mint interaction ids engine-side (#2911)
* fix(agent-core-v2): mint interaction ids engine-side

Self-hosted OpenAI-compatible endpoints may renumber tool call ids on
every response (Bash_0, Bash_1, ...). The approval/question/user_tool
facades used the provider toolCallId as the interaction id, so a
repeated id was silently swallowed by client-side pending-interaction
dedupe: the approval prompt never appeared and the turn parked
forever (#2908).

Interaction ids are now minted by the engine (approval_<uuid> /
question_<uuid> / user_tool_<uuid>); the provider toolCallId stays on
the payload for correlation. This matches v1 semantics, where the
approval id was already a daemon-minted id independent of the tool
call id.

* fix(agent-core-v2): normalize duplicate provider tool call ids at ingestion

Self-hosted OpenAI-compatible endpoints may renumber tool call ids on
every response (Bash_0, Bash_1, ...), and every downstream keying
assumes an id identifies exactly one call: context rebuild silently
drops the second tool result with a duplicated id, the strict
projector discards duplicate calls, transcript frames merge, and
approval/activity correlation misfires.

A per-agent ToolCallIdNormalizer in the llmRequester stream boundary
now tracks ids already claimed (seeded from the restored context).
The first occurrence passes through unchanged; later occurrences —
across responses or within one — are rewritten to a readable
<id>__<n> suffix, kept consistent between streamed deltas and the
finalized message, and logged for provenance. A failed attempt rolls
its claims back so a projection retry re-streams the same logical
calls under the same ids.

* fix(agent-core-v2): thread the minted approval id through events and status

The permission.approval.requested/resolved events only carried the
provider toolCallId, so AgentActivityView exposed approvalId =
toolCallId and the agent.status.updated approval phase forwarded an id
that POST /sessions/{sid}/approvals/{id} cannot resolve — the kernel
parks under the minted approval_<uuid>.

Mint the interaction id at the agent call site and include it in the
approval request payload: the kernel honors the explicit id, the
events carry it, and the activity view keys pendingApprovals by it
(falling back to the toolCallId for id-less events).

* fix(agent-core-v2): surface minted interaction ids in facade listPending

The approval/question facades returned only the original payload from
listPending(), so once the kernel id stopped deriving from the
provider toolCallId, hosts listing pending requests had no id to feed
back into decide()/answer()/dismiss() without reaching into the
kernel. Merge the parked interaction id into each returned request —
the klient contract schemas already carry the optional id field, so
the RPC surface becomes round-trippable as well.
2026-08-14 14:47:00 +08:00
weiwei Wang
53909d91e3
fix(kimi-web): cache content-hashed assets (#2865) 2026-08-14 13:08:30 +08:00
Haozhe
cb30a7799d
fix(kap-server): expose parent_tool_call_id and subagent_type in /tasks responses (#2912) 2026-08-14 13:03:56 +08:00
Haozhe
eb72aebeeb
fix(kap-server): remove the 64 MiB web session export limit (#2910) 2026-08-14 11:48:56 +08:00
Haozhe
325913a532
feat(kap-server): expose the subagent registry id in /tasks responses (#2907) 2026-08-14 11:28:07 +08:00
7Sageer
5857ba23b0
test(kap-server): deflake three timing-sensitive tests (#2894)
* test(kap-server): poll the read-model immediate-read assertions

The 'prepares the read model at boot and serves immediate reads' test
sampled the session list / workspace session_count / paged read exactly
once after creating a session. While a mirror flush is in flight its
batch is only per-shard atomic and the pending-queue cleanup is not
linearized with reads, so a single-sample read landing inside that
window can transiently miss or double-count the new session (seen
twice on main CI as 'expected false to be true' and 'expected 0 to be
1'). Poll with vi.waitFor instead; the transient lasts at most one
in-flight flush (~100ms cadence).

* test(kap-server): retry the transcript test temp-dir teardown rm

The engine's file log writers flush synchronously on scope dispose but
their trailing async close can still create a file under the test home
after server.close() resolves, so the afterEach rm occasionally fails
with ENOTEMPTY on a loaded CI runner. Retry the rm (maxRetries: 5),
matching the existing pattern in questions.test.ts / fs.test.ts.

* test(kap-server): drain the in-flight search sync before appending

The 'serves the published generation without waiting for a blocked
background sync' test appended the delta right after a warm-up search
that had kicked a fire-and-forget background sync pass. On a starved
CI worker thread that pass can read the file after the append and
publish both documents early ('expected 2 to be 1'). settleSync before
the append makes 'no pass can index the delta' structural.
2026-08-13 21:02:34 +08:00
Haozhe
4425409cea
fix(features): retract contributed service metadata (#2886)
* fix(features): retract contributed service metadata

- tie contributed service discovery to feature disposal
- reject duplicate providers for the same scope and service
- cover feature unload and debug channel resolution

* test(features): preserve contributed service registry

- remove the global contributed-service test reset
- keep provider cleanup local to each regression test

* fix(features): scope service discovery to each app

- store feature service metadata in the app-local collection tree
- bind debug lookup and test overrides to the owning app root
- keep duplicate provider activation atomic within one app
2026-08-13 19:13:00 +08:00
7Sageer
6be26978b1
feat: auto-generate session titles via the managed chat_title tool (#2351)
* feat: auto-generate session titles via the managed chat_title tool

With the auto-title experimental flag on and a managed OAuth login, the
session title is generated from the first prompt, replacing the
truncated-prompt easy title. A custom title set by the user is never
overwritten, and generation failures degrade silently to the easy title.

- oauth: fetchChatTitle for the platform /tools chat_title method
- agent-core (v1): fire-and-forget generation on the first prompt
- agent-core-v2: sessionTitle domain watching the easy-title event
- kap-server: POST /sessions/{id}/title/generate for manual regeneration

* fix: harden auto-generated session titles

* fix: preserve managed title request headers

* Pair auto-title endpoint overrides with matching OAuth credentials

* fix: preserve legacy custom session titles

* fix: preserve automatic session title invariants

* refactor: keep only the on-demand session title generation interface

Drop the automatic wiring on both engines: the v1 (TUI) first-prompt
trigger and the v2 easy-title event watcher. SessionTitleService's
generateTitle() stays as the single on-demand entry point behind the
auto-title flag, backing the kap-server title/generate route. The
changeset goes away too: with no shipped consumer, the remaining
surface is not user-perceivable.

* feat: generate session title from the first recorded prompts

Record up to three sanitized natural-language prompts in session
metadata (skill / plugin activations excluded) and compose the
chat_title input as order-labeled lines truncated to a 1000-char
budget, falling back to lastPrompt for sessions without recorded
prompts.

* test: make session title race tests deterministic

* Generate session titles from agent conversation history

* fix: reject title generation without user prompts

* fix: bound session title prompt history

* feat: enable session title generation without an experimental flag

* test: cover session title generation through the public REST path

* feat: request session title generation from the TUI after each turn

* Retry auto title generation for prompt-derived session titles

* feat: record session title source and harden the generation lifecycle

- persist titleSource (prompt/generated/custom); skip auto-generation over
  an already-generated title unless forced, and never over a custom one
- plumb the force option from the core through klient and node-sdk to the
  REST title/generate endpoint
- drop the title write-back when the session scope was superseded
  mid-flight, and retry once with a force-refreshed token on a 401
- stop closing sessions a concurrent public resume has handed out in the
  temporary resume paths (generateSessionTitle, renameSession)
- accept session.meta.updated patches without lastPrompt in klient event
  validation, and emit exactly one metadata event per applied title
- remove the retired prompts field heal and drop the changeset (the
  behavior is only perceivable on the experimental v2 engine)

* chore: follow agent-core comment convention

* fix: ignore stale session title callbacks

* fix: preserve session title state invariants

* refactor: seed session lifetime instead of querying the workspace handler

The session title service must not depend on the Workspace-tier handler
registry. The handler now seeds each session scope with an abort signal,
fires it synchronously when a close begins, and the title service carries
the signal on its request, drops the write-back once aborted, and drains
an in-flight generation through the onWillCloseSession hook.

* fix: honor the legacy custom title marker over a stale titleKind

A v1 rename spreads the original state.json document, so an explicit
isCustomTitle: true can travel with a stale titleKind. The explicit
marker now wins on load, and every persist double-writes the derived
isCustomTitle so released v1 builds keep recognizing the custom title.

* fix: serialize session access and expose the session title state

The temporary resume/rename/close paths and the public lifecycle
operations now share a per-session queue, so a public resume can never
receive a handle whose cleanup close is already in flight. Session
summaries carry the canonical title state, letting the TUI skip title
generation for sessions whose title was already generated or customized
instead of re-asking after every turn.

* chore: add session title changesets

* fix: close the session lifecycle races around close and title generation

A close/archive is now tracked in a closing registry from its first
synchronous step until disposal: get/list hide the closing session and
resume waits the close out instead of returning the doomed handle, and
fork waits out an in-flight source close. The title service tracks the
whole generateTitle call as the unit the close hook drains, and the
generated-title write re-checks the lifetime signal inside the serialized
metadata update so an abort landing while the update is queued still
vetoes the write-back.

* feat: project the session title state through the session index

readSummary and the read-model mirror carry titleKind, so listSessions
reports the same canonical title state as a resumed session's summary.

* fix: serialize the remaining session access paths in the SDK

forkSession and explicit-id createSession join the per-session queue, and
the harness resume fast path skips a session whose close is in flight
instead of returning the closing facade (which then failed every call
with session.closed); its late onClose no longer evicts the fresh
session either. The harness rename event now carries isCustomTitle so
the TUI stops asking for a generated title after a local rename.

* fix: detach the external abort listener once the chat title request settles

* fix: harden the session close/archive and create/fork lifecycle

The closing registry now records the operation kind: an archive arriving
during a plain close waits it out and lands the archived flag on the
persisted document instead of riding the close to success, and a failing
close hook no longer strands a half-closed session — the teardown always
completes while the hook error still reaches the caller. create and fork
reserve their target id synchronously with the existence check, so a
concurrent create/fork of the same id loses up front and can never tear
down the winner's scope or directory.

* fix: keep forced title regeneration independent and veto queued title writes atomically

Plain generateTitle calls still coalesce onto one shared in-flight
generation, but a forced regeneration always runs on its own so it is
neither swallowed by a plain call's early exit nor shares its result; the
close hook drains every active generation. The allowWhen veto now runs
inside applyUpdate with no await between the check and the mutation, so
an abort cannot slip into the gap.

* fix: carry the title state through the session index and klient contract

The klient session summary schema no longer strips titleKind, and the
index readSummary honors a legacy isCustomTitle marker over a stale
titleKind, so listSessions reports the same canonical title state as a
resumed session.

* fix: coalesce harness resumes, lock fork targets, and cover the title state end to end

Concurrent public resumeSession calls now share one in-flight resume and
one facade instead of building parallel facades over the same engine
handle (a close on either would strand the other). forkSession takes the
source and target queues in sorted order, so fork(A->X) is atomic against
create(X) and fork(B->X) without an ABBA deadlock. The emitMetaUpdated
patch type drops the redundant undefined union, and the SDK tests now
cover facade coalescing and the title state across list and resume.

* fix: serve the canonical title state from the session index and version the read-model cache

readSummary now derives the title state with the same priority chain as
the metadata document's canonical normalization (explicit custom marker,
valid titleKind, legacy false marker, customTitle, plain title), so list
and resume agree on legacy documents too. Read-model cache entries carry
a summary version stamp and older-stamped entries are treated as cold
misses, so an upgraded reader never serves a stale-shaped summary.

* fix: let the newest title generation request win the write-back

A forced regeneration could be followed on disk by an earlier plain
call's slower backend response. Each generation now carries a
monotonically increasing sequence (assigned only once a request actually
proceeds to generation), and the serialized metadata write is vetoed
unless the writer is still the newest request.

* fix: fold archive into close and own the create/fork rollback

An archive requested during a plain close is applied through the live
metadata during the teardown (or lands on the persisted document when it
arrives too late or the close fails), publishes the archived event, and
works on cold sessions too. A resume waiting on a failed close retries
instead of propagating the hook error, the teardown completes even when
the agent drain fails, and the create/fork rollback only ever removes
its own handle — a loser of the reservation race can no longer tear down
the winner's live scope.

* fix: key harness resume coalescing by the full input

Concurrent resumes only share a facade when their inputs match — a
caller passing different dirs, replay, profile, or kaos options gets its
own resume instead of having its options silently dropped.

* refactor(agent-core-v2): drop session close-awareness from title generation

Auto title is best-effort: a generation racing session close no longer
cancels its fetch or guards its write-back, so the per-session
sessionLifetime AbortSignal seed, the onWillCloseSession drain, and the
close-time invalidation go away. The newest-request-wins write-back
predicate stays.

* Delete .changeset/sdk-session-title-kind.md

Signed-off-by: 7Sageer <sag77r@hotmail.com>

* refactor(session-title): drop the unused force regeneration path

Nothing calls force: with it gone, plain calls always coalesce onto the
shared in-flight generation, so the generation sequence and the
caller-supplied allowWhen veto lose their only purpose and go with it.
The title/generate REST route takes no body anymore.

* refactor(agent-core-v2): drop the title state projection from the session index

The listed-session titleKind had no consumer: the TUI's title-generation
gate seeds from the resumed summary, which reads the live metadata
document, and the kap-server REST wire never carried the field. Removing
the projection also retires the read-model summary version stamp (the
remaining shape is fully field-checkable) and the duplicate title-kind
derivation that had to stay in lockstep with sessionMetadata. The klient
list contract and the node-sdk list mapper drop the field with it; the
resumed/live summary still reports the canonical title state.

* refactor(agent-core-v2): inline the transcript live-tail merge into messageLegacy

mergeContextTranscriptWithLive had a single caller; move the logic into
messageLegacyService as the private mergeLiveTail and drop the export.

* refactor(agent-core-v2): drop the closing registry from the session lifecycle

Auto title no longer consumes close-awareness, so the machinery goes
back to the simple forms: close/archive run straight through, resume
no longer waits out an in-flight close, create/fork drop their target
reservation, and a cold archive is a no-op again. Reverts the behavior
of e7c397a7c and cd1cea0fd on top of the sessionLifecycle rename.

* fix(agent-core-v2): complete the HostRequestHeaders migration in the title test

The main merge reduced #/kosong/model/hostRequestHeaders to the pure
port contract; define the test's headers as a plain value matching it
and tidy the SDK test import grouping.

* fix(node-sdk): mark resume telemetry field ignored

* feat(tui): request the session title as soon as a prompt is accepted

* fix(agent-core-v2): drop the numbered user prefixes from the title request input

* refactor(tui): ask for the session title only once per session attach

* refactor(tui): drop the automatic session title trigger

Keep the capability only: the engine-side title generation service, the
POST /sessions/{id}/title/generate route, and the SDK generateSessionTitle
method stay; the TUI no longer requests a title on prompt accept or on
session attach. The changeset now covers the SDK capability instead of a
CLI-facing auto title.

* Delete .changeset/session-title-generation.md

Signed-off-by: 7Sageer <sag77r@hotmail.com>

* feat(agent-core-v2): add forced session title regeneration

ISessionTitleService.generateTitle and the metadata write-back take an
optional force flag that bypasses the custom/generated guards, so an
explicit user request (the desktop/web rename field's Gen Title action)
can overwrite any current title; the applied title is marked generated.
Forced calls skip the in-flight coalescing, and a forced write is plain
last-writer-wins.

kap-server's POST /sessions/{id}/title/generate accepts an optional
{ "force": true } body; klient and the node-sdk plumb the option through
(GenerateSessionTitleInput.force).

Also renumber SESSION_TITLE_UNAVAILABLE to 40923: main assigned 40922 to
PAGE_TOKEN_MISMATCH after this branch forked.

* feat(agent-core-v2): selectable conversation excerpts for title generation

generateTitle gains a source option alongside force:

- user_prompts (default): the existing first-prompts window, unchanged.
- first_turn: the opening user prompt paired with the first turn's final
  assistant text — strict, so a caller asking before the first reply lands
  simply gets unavailable and can retry at the next turn boundary.
- digest: first prompt + latest prompt + the latest turn's final assistant
  text, tolerating a compacted window by using whatever segments survive;
  meant for explicit regeneration on multi-turn sessions.

Assistant segments keep only natural-language text parts (tool calls,
thinking, and media never contribute) and pass through the shared metadata
sanitizer, which redacts secrets and long base64-looking runs; each
segment is capped (user 300, assistant 600/400) so the composed
chat_content stays within the 1000-char budget. The kap-server route,
klient contract, and node-sdk plumb the option through. Excerpt extraction
is covered against the real context memory (loop-event folding), and the
REST surface gains a digest composition case.

* feat(agent-core-v2): gate session title generation behind an experimental flag

Registers the  flag (off by default; env
KIMI_CODE_EXPERIMENTAL_SESSION_TITLE, the master flag, or the
[experimental] config section) and makes generateTitle report
unavailable while it is off, so every entry point — the kap-server
route, klient, node-sdk, and through them the clients' auto trigger and
rename-field action — is inert unless the user opts in.

* refactor(agent-core-v2): rename the title flag to auto_session_title

Snake-case id matching search_worker / persistence_minidb_readmodel; env
KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE.

* fix(agent-core-v2): land the merge resolution leftovers

The main-merge commit captured pre-fix snapshots of four files; the
actual resolutions only lived in my working tree: the LifecycleScope
import move to #/app/scopes, the titleKind port of
applyPromptMetadataUpdate, the Promise<void> pinning of the metadata
update queue, and the reloadSession runSessionAccess closure.

* test(node-sdk): enable the auto_session_title flag in the title suites

Generation now reports unavailable with the flag off, so the two
title-generation harnesses opt in through the written config's
[experimental] section.

* chore: changeset for the experimental web session titles

* chore: scope the title changeset to the SDK package

* chore: cover the internal packages in the title changesets

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-08-13 17:59:57 +08:00
Haozhe
314b39489e
refactor(agent-core-v2): extract swarm into a scope-organized feature (#2874)
* refactor(agent-core-v2): extract swarm into a scope-organized feature

- move src/agent/swarm, src/session/swarm, and src/agent/tools/agent-swarm
  into src/features/swarm/{agent,session,tools/agent-swarm}; swarmOps.ts
  stays a static import=register wire channel at the feature root
- add SwarmFeature carrying the three runtime registrations
  (IAgentSwarmService, ISessionSwarmService, IAgentSwarmTool) with
  ScopeActivation.OnScopeCreated preserved
- switch src/index.ts to precise leaf exports and update import sites,
  including the kap-server and kimi-inspect deep-path imports
- move tests to test/features/swarm and re-assert service overrides in
  the test harness so stubs keep winning over feature contributions

* fix(agent-core-v2): keep feature-contributed tools in Agent tool descriptions

SubagentTool.knownToolReferences() now reads the full AgentToolContribution
collection (static registrations and feature contributions alike) instead
of the static contribution table. A caller profile that does not activate
a feature-contributed tool (e.g. AgentSwarm) no longer drops it from the
per-profile tool listings the description advertises for spawned profiles
when a workspace/session restriction forces explicit enumeration.

Add a regression test with a caller profile lacking AgentSwarm under a
global tool restriction.

* refactor(kap-server): lift session profile updates to the route edge

- add sessionProfile.ts/sessionAgentConfig.ts route helpers that resume
  the session and dispatch title/metadata and the agent_config patch to
  the native v2 services directly
- drop updateProfile from ISessionLegacyService, leaving only the
  status rollup and the goal read in the legacy adapter
- wire shape and client-visible behavior unchanged
2026-08-13 13:35:49 +08:00
7Sageer
c9bfe8b2c8
feat: replace the secondary-model experiment with a declarative subagent model pool (#2700)
* feat: replace secondary-model experiment with [subagent.models] pool

Add a declarative subagent model pool to agent-core-v2: [subagent.models]
maps [models] entry ids to selection hints rendered in the Agent/AgentSwarm
tool descriptions, and [subagent].default_model picks the spawn model when
the caller passes none. The tools' model parameter becomes a free-form
alias string (stripped when no pool is configured), description rendering
is caller-aware (primary (alias) [main model]), and a session-start
validation service fails fast with CONFIG_INVALID on a missing/invalid
default_model or an unresolvable pool alias.

Remove the secondary-model experiment from the v2 engine, node-sdk,
kap-server, and the TUI (the /secondary_model command), and drop the
agent-profile modelPreference / model_preference frontmatter field on v2.
The legacy v1 engine keeps the experiment unchanged; v2 ignores leftover
[secondary_model] config silently.

* fix(agent-core-v2): harden subagent model-pool validation and error/picker mapping

Deep-review follow-ups to the [subagent.models] pool:

- validate the pool before session materialization (after config.ready)
  and before the fork file copy, so a broken pool no longer leaves
  orphaned session dirs or leaked MCP overlay connections; the
  Session-scope validation service stays as a backstop
- reject the reserved "primary" pool alias at startup, and again
  defensively in resolveSubagentBinding so a pool broken by a runtime
  config edit fails loudly at spawn instead of binding the wrong model
- keep the [default] marker when the caller's own model is the pool
  default (primary (alias) [main model] [default])
- recompile the cached tool-args validator when a tool advertises a new
  schema object (mid-session pool edits no longer hit a stale validator)
- map config.invalid to VALIDATION_FAILED in kap-server's session routes,
  the debug transport mapper, and the catch-all error handler
- hide the v1-synthesized __secondary__ entry from the /model and
  /provider pickers again
- fold per-export doc blocks into file headers per package comment
  conventions; add pre-flight/reserved-key/validator/mapping tests and
  document that create/resume/fork all fail on a broken pool

* feat: re-add /secondary_model and accept a lone subagent default_model

- v2 engine: a pool-less [subagent] default_model forms an implicit
  single-entry pool — validated at session create/resume/fork like an
  explicit pool, and advertised through the Agent/AgentSwarm model
  parameter.
- Tool descriptions: the caller's own alias is a normal pool entry
  marked [main model]; the primary line stays distinct because only it
  inherits the caller's thinking level.
- TUI: /secondary_model returns, persisting [subagent] default_model
  (merging into an existing pool with an empty description); the picker
  hides the no-op Thinking footer and rejects the reserved primary
  alias.
- kap-server: /api/v1/config accepts and echoes subagent; the
  snake-to-camel patch conversion preserves user-defined map keys under
  providers/models/experimental/raw without leaking preserve mode into
  a colliding alias's own fields.
- v1 config schema learns subagent.defaultModel/models so the shared
  config.toml round-trips; the v1 engine still ignores them at runtime.
- Docs (en/zh) and changesets updated.

* docs: use public model identifiers in the subagent model pool examples

* refactor: rename /secondary_model to /secondary-model

* test: cover the /secondary-model command name resolution

* Revert "test: cover the /secondary-model command name resolution"

This reverts commit 98a4a6d999.

* feat(agent-core-v2): move the subagent model pool to [secondary_model]

The pool keys (default_model, [secondary_model.models]) now live in their
own [secondary_model] config section instead of [subagent], which keeps
only timeout_ms; legacy [subagent] pool keys are ignored with a
deprecation warning. The SDK config contract carries the pool on the
secondaryModel field, so the TUI /secondary-model command (now also
aliased /subagent-model) and the kap-server /config wire read and write
it directly with no translation layer.

* docs: correct default engine guidance

* feat(agent-core-v2): pin subagents to default_model with [secondary_model] force

force = true removes the main agent's per-spawn model choice: the Agent
and AgentSwarm tools stop advertising the model parameter and every spawn
binds default_model; an explicit choice, "primary" included, is rejected.
The setting requires default_model, rejects a [secondary_model.models]
table, and is validated loudly at session create/resume/fork (lifecycle
preflight plus the Session-scope backstop). The v1 engine declares the
key for write round-trips and excludes it from the recipe patch.

Also documents pool entries as per-alias thinking-level variants via
default_effort overrides.

* docs: use real managed model aliases in the secondary_model examples

The pool examples invented aliases (kimi-hs, fable, codex) and referenced
non-existent model IDs (model = "codex"); they now reference only the
managed aliases provisioned by /login (kimi-code/k3,
kimi-code/kimi-for-coding, kimi-code/kimi-for-coding-highspeed), with the
effort variant derived as kimi-for-coding-highspeed-deep. Also replaces the
versioned kimi-k2.5 alias with kimi-for-coding per the docs model-ID rule.

* chore: simplify the subagent model pool changeset

* feat(agent-core-v2): honor the legacy [secondary_model] model key as a fallback default

* refactor(node-sdk): export the reserved model-alias constants from the SDK

Restore the SECONDARY_DERIVED_MODEL_ALIAS re-export and add
PRIMARY_SUBAGENT_MODEL_CHOICE so the TUI imports both from
@moonshot-ai/kimi-code-sdk instead of vendoring local copies.

* feat(agent-core-v2): keep the subagent model pool behind the secondary-model experiment

Restore the secondary-model flag gating so this change only adds the pool:
with the experiment off the [secondary_model] pool keys stay inert — the
Agent/AgentSwarm tools strip the model parameter, spawns inherit the
caller's model, and startup pool validation is skipped. The /secondary-model
slash command is gated behind the experiment again, and the docs and
changeset describe the flag.

* fix(node-sdk): cascade provider removal into the subagent model pool

Deleting a provider left [secondary_model] entries pointing at the removed
model aliases; with the secondary-model experiment on, every subsequent
session create/resume/fork then failed pool validation. planProviderRemoval
now filters dangling pool entries, and drops the whole section when its
effective default (defaultModel, or the legacy recipe's model fallback)
dangles — folded into the same atomic multi-section replace.

* fix(agent-core-v2): close two subagent model pool validation gaps

Spawn-time resolveSubagentBinding now rejects force combined with a
[secondary_model.models] table, matching the startup pre-flight — a live
session could otherwise reach that invalid state through a deep-merged
config patch and only fail on the next create/resume. The session
lifecycle pre-flight also awaits the kosong model/provider registries'
ready alongside config.ready, so a cold bootstrap no longer fails a valid
pool with CONFIG_INVALID against an empty registry.

* test(agent-core-v2): stub the model/provider registries in handler-chain tests

SessionLifecycleService now awaits IModelService/IProviderService readiness
in its pool pre-flight, so the tests that assemble the real service through
a hand-built container must register the two tokens.

* fix(kap-server): cascade REST provider deletion into the subagent model pool

The DELETE /providers route rewrote only the providers and models
sections, so a pool referencing one of the deleted provider's aliases was
left dangling and the engine's create/resume/fork pool validation failed
every subsequent session until the user repaired the TOML by hand. Filter
dangling pool entries and drop the section when its effective default
dangles, mirroring the SDK's planProviderRemoval semantics.

* fix: keep the subagent pool consistent on provider replace and preserve legacy recipe fields

PUT /providers rebuilds the provider's alias set and can drop or rename
aliases referenced by [secondary_model]; the pool now cascades there too —
renamed aliases are repointed (mirroring the global default-pointer
migration), dropped aliases are filtered, and the section is cleared when
its effective default dangles.

The v2 [secondary_model] schema also declares the legacy recipe patch
fields (default_effort, max_output_size, ...) so validation no longer
strips them: pool resolution keeps ignoring them, but config reads/writes
now round-trip losslessly instead of silently deleting them from
config.toml on any pool write.

* fix(agent-core-v2): cascade the subagent pool on catalog refresh writes

A background provider/model refresh rewrites the [models] table without
touching [secondary_model], so a dropped alias left the pool dangling and
every subsequent session create/resume/fork failed validation until the
user repaired the TOML by hand — the same gap the SDK and REST write
paths already had, but triggered unattended.

The cascade helper now lives in agent-core-v2 next to the section it
protects (cascadeSubagentModelPool): the discovery service folds the pool
into the same atomic replaceSections transition, and kap-server's
provider write routes reuse the shared helper instead of a local copy.

* fix: cover the last two model-table write paths for the subagent pool

ModelsDevImportService's catalog and custom-registry imports rebuild the
[models] table without the pool cascade, so an import that drops a pooled
alias left a dangling pool behind; both final write passes now fold the
pool through cascadeSubagentModelPool (the drop passes deliberately skip
it). The StubConfigService test double now treats a null section value as
a delete, matching the real ConfigService.

The TUI's provider overwrite flow removes an existing provider before
re-adding it, which ran the removal cascade against a model table where
every alias of that provider was absent and silently dropped the pool;
the flow now snapshots secondaryModel up front and restores the entries
that survive the re-add, via the cascade helper re-exported from the SDK.
2026-08-13 12:29:03 +08:00
Haozhe
23e68eee8b
refactor(agent-core-v2): remove the agent RPC aggregation layer (#2871)
* refactor(agent-core-v2): remove the agent RPC aggregation layer

- delete src/agent/rpc/ (AgentRPCService, IAgentRPCService, core-api,
  prompt-metadata, types) and sink each method's orchestration into its
  owning domain service
- prompt: new submit/submitSteer composing disabledTools gating,
  MAIN-only session metadata, and engine-side {turn_id} settlement
- skill: activate now returns PromptLaunchResult and writes session
  metadata internally (MAIN-only, unified across prompt/steer/skill/
  pluginCommand); node-sdk and kap-server drop their edge-side writes
- pluginCommand: new agent-scope domain owning command activation and
  the plugin_command.activated domain event
- permissionMode/loop/fullCompaction: new setModeAndBroadcast /
  cancelFromUser / cancel; setMode and loop.cancel stay pure for
  internal callers
- klient: agentRpcContract split into per-domain contracts; facade
  re-routes to domain channels with its public API unchanged
- node-sdk, kap-server, kimi-inspect and the v2 test harness now call
  domain services directly; ctx.rpc keeps its name as a composed
  adapter
- externally visible: the agentRPCService debug channel is gone and
  session metadata writes are now MAIN-agent-only (see changeset)

* refactor(agent-core-v2): move disabledTools gating out of the prompt domain

Prompt should not own session tool policy: submit no longer accepts or
applies disabledTools. The klient facade keeps its prompt({ disabledTools })
API and composes it edge-side — applying agentToolPolicyService
setSessionDisabledTools before calling agentPromptService.submit, the same
way kap-server's prompt route already does. Over klient, a profile-less
engine now surfaces the raw profile error instead of request.invalid.

Also restores the RPC-removal changeset, which did not make it into the
previous commit.

* chore(agent-core-v2): drop the RPC-removal changeset

* refactor(klient): drop disabledTools from the prompt entry entirely

The prompt path no longer carries session tool gating on any surface:
the klient facade prompt() loses the disabledTools field and calls
agentPromptService.submit directly, and the node-sdk
SessionPromptRpcInput stops accepting or forwarding it (v1 always
ignored the field). Session tool gating remains available through
IAgentToolPolicyService.setSessionDisabledTools, composed at the edge
the way kap-server's prompt route does; the klient toolPolicy contract
added for facade-side composition is removed as unused.
2026-08-13 10:57:01 +08:00
liruifengv
43c68f58f5
feat(agent-core-v2): keep session updatedAt stable across meta management writes (#2815)
* feat(agent-core-v2): keep session updatedAt stable across meta management writes

Rename, archive/restore, and fork no longer bump a session's updatedAt,
so recency-sorted session lists stop reshuffling on management actions:

- setTitle/setArchived pass touchUpdatedAt: false; an explicit
  patch.updatedAt always wins (fork inherits the source's recency, so a
  fork lands next to the source instead of floating to the top)
- new SessionMeta.archivedAt records the archive moment (cleared on
  restore) and is surfaced through the session index, the v1/v2 session
  routes (archived_at), and the klient contract, so the archived list
  keeps an accurate archive time without relying on the updatedAt bump

* fix(agent-core-v2): normalize a legacy ISO-string updatedAt when forking a cold session

A cold legacy/v1 state.json read from disk can still carry an ISO-string
updatedAt; passing it through as the fork's explicit patch.updatedAt
would persist a string into the v2 metadata. Normalize with toEpochMs
(falling back to now when absent/unparseable).

* fix(agent-core-v2): write fork metadata after agent recreation

Registering each copied agent during fork is an ordinary metadata write
that bumps updatedAt, which overwrote the inherited source recency and
still floated normal forks (sessions with agents) to the top. Move the
fork's metadata update after the agent recreation loop so the inherited
updatedAt is the final write.

* fix(agent-core-v2): preserve persisted recency when restoring a cold session

Resume creates the main agent for a cold session that has no persisted
agents.main entry (e.g. an empty session), and that registration bumps
updatedAt — so unarchiving an empty session still floated it to the
top. Capture the index summary's updatedAt before resume and re-apply
it in the restore write (archived:false, archivedAt cleared, explicit
updatedAt wins over the bump).

* fix(agent-core-v2): make agent registration non-touching for recency

Registering an agent is a structural write, not content activity — but
it went through an ordinary metadata update that bumped updatedAt. That
reordered recency-sorted listings whenever materialization created an
agent: resume of a cold session without a persisted agents.main (so
archive-via-resume and restore of empty sessions still floated), and
runtime subagent registration mid-turn. registerAgent now passes
touchUpdatedAt: false; restore goes back to the plain unarchive write
and no longer needs the capture/reapply workaround.

* fix(agent-core-v2): duplicate cron tasks only after the fork metadata is durable

With the metadata write moved after agent recreation, cron duplication
ran before it — a rejected metadata update left cloned cron records
pointing at a fork whose directory the catch block just removed. Keep
cron duplication after the durable metadata write.

* style(agent-core-v2): fold new invariants into module headers

The package convention keeps comments in the top-of-file block only —
move the touchUpdatedAt precedence, non-touching registration, and fork
ordering notes out of statement-level positions into the respective
module headers.

* chore: scope the changeset to agent-core-v2
2026-08-12 13:40:26 +08:00
Haozhe
26a37f30a3
fix(kap-server): restrict debug RPC fallback to feature-contributed services (#2808)
The decorator-registry fallback resolved every decorator name, including
kernel tokens like instantiationService — a call to
instantiationService/dispose would tear down the root container. Record
Feature.contributeService tokens in a contributed-service table and fall
back to that table only, so runtime-contributed services stay callable
while unregistered kernel tokens remain unreachable.
2026-08-12 12:35:01 +08:00
qer
619564dcf9
fix(kap-server): add WebSocket heartbeat to survive proxy idle timeouts (#2813)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
CI / lint (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
The v1 WS connection had no keepalive: by design it stayed open until the
client disconnected, which only holds for direct connections. Behind a
reverse proxy or gateway with an idle timeout (30s defaults are common),
any quiet stretch — e.g. waiting on a slow model response — got the
connection killed, surfacing as a recurring 'Realtime connection error'
in the web UI.

Send an application-level ping every 10s and advertise heartbeat_ms in
server_hello (the schema and all shipped clients already answer pong).
Application-level rather than protocol-level ping because browser JS
cannot observe the latter, and the client's stale-socket detector keys
on incoming message frames. Any inbound frame refreshes liveness; after
two silent cycles the connection is presumed half-open and closed with
1001 so dead peers get reaped instead of leaking.
2026-08-12 00:28:03 +08:00
Haozhe
860354976e
feat(agent-core-v2): add event-subscription introspection (#2806)
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
- name Emitters and surface their subscriptions as on:<name> ledger
  labels through a named EventSubscription class and IDisposableDebugLabel
- add IDebugEventsService.subscriptions(), merging unit-book entries
  with per-bus listener counts, contributed at App scope by the new
  debugEvents feature
- kap-server debug dispatcher falls back to the global decorator
  registry so runtime-contributed services stay callable
- kimi-inspect: add an Events panel to the DI view
2026-08-11 14:55:08 +08:00
Haozhe
7cd64766c8
feat: isolate the full-text search index from the session index and the main thread (#2701)
* feat(minidb): instrument open lifecycle with phase timings and status

Add MiniDb.lifecycleStatus() exposing the no-generation/generation-load/
wal-catch-up/full-rebuild/ready/degraded state machine plus per-phase
timings (generation candidate load, store/non-text/text image load,
postings integrity check, WAL scan/apply, full recovery, text rebuild
hosting), so snapshot load, WAL catch-up and full rebuild can be told
apart in diagnostics.

Also add a repeatable open-lifecycle bench (small data, large WAL delta,
large full-text generation, corrupt generation) and fixtures proving a
healthy generation open performs no full-corpus tokenization while a
corrupt or missing generation falls back. Log search-index and
query-store open diagnostics in kap-server and agent-core-v2 so a
listSessions call can be attributed to the database it touches.

No persistence format or product behavior change.

* feat(agent-core-v2): isolate the session index from the global search index

Harden the separation between the session read model and the full-text
search index so session operations never depend on search availability:

- Reject text index definitions in MiniDbQueryStore at definition level,
  keeping the session query-store a structural-only read model with no
  postings/tokenizer artifacts, and assert its generation carries no
  full-text files.
- Share one authoritative scan between the first list and the initial
  projection (single-flight) instead of scanning twice; reads may only
  join an in-flight scan, and every fallback read folds the mirror's
  pending queue so read-your-writes holds while preparing.
- Keep withReadModel() fallback semantics pinned by tests:
  uninitialized/preparing reads hit authoritative metadata immediately,
  ready reads use the read model, degraded keeps falling back with a
  diagnosable status reason.
- Guard session metadata writes so a mirror failure degrades only the
  read model and never fails the session lifecycle.
- Prove via tests that listSessions/--resume/--continue never open the
  global search DB (including when search-index is unopenable), and that
  only real full-text search requests report building/stale/degraded.

* perf(minidb): slice open-time work so it never blocks the main thread

Make the whole generation-open path cooperative:

- Replace the synchronous postings/store CRC verification with chunked
  async variants (readGenerationFileCheckedAsync, verifyFileIntegrityAsync)
  that keep the exact bytes/crc-mismatch error semantics.
- Give the WAL-delta apply a primitive-op + wall-clock budget
  (walApplySlicer), so a batch frame unrolling into thousands of ops can
  no longer run as one uninterruptible slice; torn-tail, corrupt-batch
  and read-only behaviors are unchanged.
- Slice the big attach loops: Store.bulkLoadRefsAsync +
  SkipList.bulkLoadAsync for the store image, async parsers and
  loadImageAsync for secondary/compound images, and
  TextIndex.attachImageAsync for the docs/dictionary map construction.
- Queue text builds on worker-slot pressure (WorkerSlots.acquireBounded,
  bounded by MiniDb.textBuildSlotWaitMs, abort-aware) instead of falling
  back to an unbounded inline build; a persisted drought hosts the
  bounded inline core as the explicit last resort with stats accounting.

Bench (bench/open-lifecycle, seed 42): event-loop delay max across the
four open scenarios drops from 45/734/331/492 ms to ~12-28 ms with wall
time flat or better.

* feat(kap-server): run the global search index in a dedicated worker

Move the whole search-index MiniDb lifecycle (open, generation load,
WAL replay, sync, rebuild, compaction) off the main thread into a
long-lived worker_threads host, so it never shares the event loop with
TUI input:

- Add a versioned request/response protocol and worker entry hosting a
  host-agnostic SearchIndexCore; the same core also backs an inline
  backend kept as the explicit rollback
  (KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER=false, flag default ON).
- The worker exclusively owns the search-index handle. The lock token
  is reported at acquire time (new MiniDb OpenOptions.onLockAcquired
  hook) and reaped on dirty exit; an orphan-lock detector (same-pid
  lock row whose token no live holder owns) recovers the window where
  the token report is lost, so a mid-open crash can never freeze the
  index into a silent permanent read-only.
- Crash handling: in-flight requests are rejected with typed errors,
  respawn uses capped exponential backoff, per-request watchdogs
  terminate wedged workers, and beginClose propagates into the worker
  so dispose stays bounded during a long sync. Page tokens pin a
  boot-salted generation, so tokens issued before a transparent worker
  restart fail closed with invalid_page_token.
- The main process keeps the sync coordinator (debounce/coalescing/
  single-flight), live transcript routing, query normalization and
  page-token codec; searches keep reading the published generation and
  report building/stale/degraded instead of waiting for sync/rebuild.
- Wire the worker into the CLI packaging: self-contained worker bundles
  for npm dist and the SEA asset manifest/installer/smoke check, plus a
  dev runtime (type-stripping + .ts resolve hook) scoped to worker
  execArgv.

* feat(kap-server): model search and session-index lifecycles explicitly

Consolidate the two-index separation into explicit, diagnosable
lifecycles:

- Surface the global search state machine (stopped / opening / building
  / ready / degraded / closing) end to end: SearchIndexCore.lifecycleState,
  SearchWorkerHost lifecycle snapshots cached from RPC responses (and
  invalidated across worker generations), a never-throwing status()
  carrying the lifecycle, and a synchronous lifecycleReport() that
  neither kicks the open nor spawns the worker. Corrupt search-index
  rebuilds are announced with a dedicated warn log so building, stale,
  degraded, corrupt and worker-unavailable stay distinguishable.
- Turn MiniDb read-only replica catch-up fully cooperative:
  catchUpWalAsync scans frames with the windowed async scanner and
  yields per primitive op on the shared walApplySlicer budget, while a
  per-instance catchUpChain serializes concurrent catch-ups so each
  caller keeps its atomic watermark advance. The stale synchronous
  implementations are removed.
- Pin the dependency direction and availability timing with tests:
  session list/create/resume survive a corrupt or unopenable search
  index (also end-to-end with a dead query-store), search generation
  reuse and stale-serving keep working across restarts, concurrent cold
  callers open the index / spawn the worker exactly once, resume-then-
  fetchSessions performs no duplicate authoritative scan, and a clean
  dispose releases the lock and settles at stopped.
- Document the experimental flag surface (persistence_minidb_readmodel,
  search_worker) in the root guide.

* feat(agent-core-v2): default the session read model on and roll out the separation

Rollout and validation for the index separation plan:

- Flip persistence_minidb_readmodel to default ON (rollback via
  KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL=false or the
  experimental config section); session list/--resume/--continue now
  always go through the isolated session read model with the
  authoritative fallback. Test harnesses pin the flag off where shared
  fixtures require hermetic homes, while the dedicated suites keep
  explicit on/off coverage.
- Add a probe proving the main thread stays responsive while the
  search worker rebuilds and swaps a generation (reindex), completing
  the TUI responsiveness matrix.
- Record the rollout state in the agent-core-v2 guide (session index
  section) and the root flag line.
- Add changesets for the CLI (worker isolation, session index
  independence) and minidb (cooperative open lifecycle).

Validation: full suites green across minidb (551), agent-core-v2
(4760), kap-server (1005), node-sdk (343), klient (91) and the CLI app
(2567); open-lifecycle bench event-loop delay max is down from
45/734/331/492 ms to ~16-22 ms across the four scenarios with wall
time flat or better.

* fix(agent-core-v2): evict deleted sessions from the mirror queue and drain the index on close

Two issues surfaced by the read-model default in the acp-server suite:

- ISessionIndex.remove only deleted from the query store, but a summary
  still queued in the mirror was folded back into reads (and re-written
  by the next flush), resurrecting a deleted session in listings. The
  mirror now exposes evict(id): drop the queued summary and wait out an
  in-flight flush before the store delete.
- RunningAcpServer.close and SDKRpcClientV2.close disposed the engine
  without awaiting the asynchronous mirror flush / query-store close,
  so a host removing homeDir right after close() raced in-flight shard
  closes (ENOTEMPTY). Both now follow the kap-server shutdown order:
  drain the mirror while the store is open, dispose, then await the
  drains.

* fix(minidb): pause active expiry during the sliced bulk load

The store's active-expire timer is armed at construction, so during a
sliced bulkLoadRefsAsync a tick can fire mid-load: it reaps a TTL key
from the map while the order skiplist is still the old empty one, and
the final bulkLoadAsync then rebuilds order from the stale orderEntries
snapshot — resurrecting the expired key in the ordered index (and
duplicating it if the key is later set again). The sync bulkLoadRefs had
no yield windows, so guard the async path with a bulkLoading flag that
defers expiry ticks until the load settles (finally-safe).

* chore: consolidate changesets into the TUI startup freeze fix
2026-08-07 07:38:16 +08:00
qer
335588e259
feat(agent-core-v2): persist the last turn outcome into session metadata for cold listings (#2666)
* feat(agent-core-v2): persist the last turn outcome into session metadata for cold listings

A cold session (no live handle) reported no lastTurnReason, so after a
server restart the session list could not mark a session whose last turn
failed until it was opened and resumed.

A new Session-scope SessionOutcomeRecorder subscribes to the activity
aggregate's turn_ended changes and persists the outcome
(completed/failed) into the session metadata document; the summary
pipeline (mirror + cold reader) carries it as SessionSummary
.lastTurnReason, and toWireSession falls back to it when no live fact
exists. 'cancelled' is deliberately not persisted: it is also what an
in-flight turn ends with during scope disposal, and writing there races
the host's home-dir teardown.

Verified end to end with an isolated home and a dead provider: a turn
fails, the server restarts, and GET /sessions reports
last_turn_reason=failed without opening the session.

* fix(klient): carry lastTurnReason/lastTurnOutcome in the validated contracts

Review follow-up: zod strips unknown keys on parse, so the new outcome
fields never reached klient callers; add them to the session summary and
metadata/patch/key schemas (contract parity test covers the engine
mirror).

* fix(agent-core-v2): persist user-cancelled outcomes, never teardown aborts

Review follow-up: skipping every 'cancelled' left a stale earlier outcome
in the metadata (e.g. a prior failed reported for a session whose latest
turn was stopped by the user). The recorder now subscribes to the main
agent's turn.ended facts directly and keys on interruptReason:
user_cancelled is persisted like any other terminal state, while
programmatic aborts — including the cancel every in-flight turn suffers
during scope disposal — are never written, so no metadata write races the
host's home-dir teardown.

* fix(kap-server): only fall back to the persisted outcome for cold sessions

Review follow-up: a warm session that just started a new turn clears its
live lastTurn, and the unconditional ?? fallback would then report the
previous turn's persisted outcome for a turn that is still running.
SessionFacts now reports whether a live handle exists, and the wire
projection only reads the persisted value when the session is cold.

* docs(agent-core-v2): keep the outcome-recorder header at role level

* fix(agent-core-v2): settle turn outcomes on turn start and drain metadata writes on close

Review follow-ups:

- a new main turn now clears the persisted outcome (turn.started), so a
  process that dies mid-retry no longer reports the previous turn's
  terminal state for a turn that never ended
- the dedupe marker only advances after a successful write, so a failed
  persist no longer suppresses the next identical outcome
- session metadata writes are tracked in a module-level pending set with
  drainSessionMetadataWrites(), awaited by kap-server close alongside the
  mirror/query-store drains — an event-driven write (e.g. the outcome
  recorder) can no longer land in a session dir while the host removes it

* fix(agent-core-v2): track the metadata dispose flag locally

Disposable exposes no public isDisposed accessor; keep a class-local flag
set in the dispose override.

* fix(kap-server): drain session metadata writes before the mirror and disposal

A write still in flight when close() begins must settle before the mirror
flushes its summary into the read model and before scope disposal marks
the service disposed — not after.

* fix(agent-core-v2): reattach the recorder when the main agent is recreated

Review follow-up: a failed bootstrap still fires onDidCreate before the
handle is dropped; the subscription then pointed at a dead bus and the
guard blocked any later reattach. Track onDidDispose and reset so the
next main creation attaches cleanly.

* test(agent-core-v2): resolve the recorder through the scoped DI harness

Review follow-up: construct SessionOutcomeRecorder via registerScopedService
+ a Session-scope test host (stubbed lifecycle/metadata), so the test covers
the production registration path; add the durable-value adoption case.

* fix(agent-core-v2): unbreak CI — iterable Promise.all and the debug channel surface

- Promise.all takes the pending-writes set directly (oxlint error)
- the disposed flag moves into a _register'd marker instead of a public
  dispose() override, which the debug channels listing (and its test)
  correctly rejects as framework plumbing

* fix(kap-server): surface persisted failures on the v2 session status

The v2 list folds the outcome into activity.status, which previously
read only live facts — a cold session always looked idle. Cold sessions
now map a persisted failed outcome to status 'failed' (completed and
cancelled stay idle, matching the live fold); warm sessions are
unchanged, and the statuses filter inherits the mapping.

* refactor(agent-core-v2): name the persisted field lastTurnReason

Aligns with the established name for the same concept end to end
(activity view's lastTurnReason, the v1 wire's last_turn_reason, and the
SessionSummary mirror), instead of introducing a third variant.

* fix(agent-core-v2): drain pending metadata writes before session teardown

Review follow-up: closing/archiving a session right after a turn ended
could dispose the scope while the outcome write was still queued, and
delete() removes the session dir immediately after close. Await the
pending metadata writes before the handle goes away.

* fix(node-sdk): carry lastTurnReason through the SDK session summary

Review follow-up: the in-process SDK path maps the engine summary through
v2SummaryToSessionSummary, which dropped the new outcome field. Add it to
the public SessionSummary type and the mapper; the parity gate projects
it away (the v1 engine never records an outcome).

* fix(node-sdk): populate lastTurnReason on live SDK summaries

Review follow-up: resumeSession/reloadSession build their summary from the
live session's metadata document, which now carries the outcome — surface
it there too so the SDK reports it consistently for live and listed
sessions.

* fix(agent-core-v2): carry the last turn outcome across session forks

Review follow-up: fork skips state.json when copying the session dir, so
the fork's fresh metadata never had the outcome and a restart dropped a
marker the warm fork was still reporting. The fork's metadata patch now
inherits the source's lastTurnReason.

* fix(agent-core-v2): settle pending outcome writes before reading a fork source

Review follow-up: a fork requested right after the source's turn ended
could read the metadata before the recorder's queued write landed,
inheriting a stale or absent outcome. Drain pending metadata writes
first.

* fix(agent-core-v2): backfill restored outcomes into the session metadata

Review follow-up: for sessions whose last turn ended before this field
existed, the cold-resume seed restores the outcome into the activity view
without a turn.ended fact, so the recorder never persisted it and cold
listings stayed blank. The recorder now also watches the main agent's
activity updates and backfills the restored outcome when nothing is
persisted yet.

* fix(agent-core-v2): never backfill restored cancellations

Review follow-up: a restored 'cancelled' cannot be told apart from a
programmatic abort (the activity event carries no interruptReason), and
those are never persisted. Backfill now covers only completed/failed;
user stops are still persisted from the live turn.ended fact.

* refactor(agent-core-v2): rename the outcome recorder to outcome mirror

Mirror is the codebase's established term for a write side that reflects
live state into a store (SessionIndexMirror); Recorder has no precedent.

* fix(agent-core-v2): backfill without bumping recency; header-only comments

Review follow-ups:

- a mere resume must not float an old session to the top of the list:
  metadata updates accept touchUpdatedAt:false and the outcome mirror's
  backfill uses it (live outcome writes keep bumping — turn end is a
  recency moment)
- the mirror service's inline notes move into the file header per the
  package comment convention
- drop the redundant |undefined from the SDK's optional outcome field

* fix(node-sdk): read the live outcome for resumed session summaries

Review follow-up: on a fresh resume the restored outcome can still be
queued as a metadata backfill, so the document may lag a tick; the live
activity aggregate already holds it. Resume/reload summaries now prefer
the live value and fall back to the metadata field.

* fix(agent-core-v2): confine the outcome backfill to pure resumes

Review follow-up: the view publishes its turn.ended fold before this
mirror's own turn.ended handler runs, so a live ending reached the
backfill branch first and got persisted without the recency bump. The
backfill now only applies when no turn ever started in this process —
live endings always take the bumped write.

* fix(agent-core-v2): drain the session-index mirror before session teardown

Review follow-up: settling the metadata write alone left the fresh
summary in the mirror's pending queue, so a list right after close could
read a stale outcome from the read model. close/archive now also drain
ISessionIndexMirror. Test harnesses register a mirror stub for the new
dependency.

* docs(agent-core-v2): fold the metadata drain contract into the file header

* chore: include the SDK package in the changeset; fold the drain note into the header

* fix(agent-core-v2): backfill restored cancellations too, quietly

Review follow-ups: dropping every restored cancel loses legitimate user
stops whose live write never landed (or was rejected) before a restart —
cold surfaces never mark cancelled anyway, so healing them is harmless
and strictly more accurate. The metadata disposal note moves into the
file header per the comment convention.

* fix(node-sdk): prefer the live outcome over the index in SDK listings

Review follow-up: a live session that just started a new turn after a
failure can briefly keep the stale outcome in the index while the
mirror's clear is queued. listSessions now reads the live activity
aggregate for warm sessions, matching the kap-server cold-only fallback.

* fix(node-sdk): never read the metadata outcome for a live session

Review follow-up: with a retry in flight the live aggregate has no
outcome while the document may still hold the previous failure — the
fallback showed the stale one. Live summaries now take the live
aggregate's answer alone; the restored outcome is already seeded there
on resume.
2026-08-06 20:14:05 +08:00
Haozhe
02c026d487
feat(agent-core-v2): tombstone removed MCP servers and freeze plugin prompt inputs (#2694)
* feat(mcp): tombstone removed MCP servers and apply plugin changes immediately (20 files)

- add 'removed' MCP server status: workspace config removals call markRemoved
  instead of remove, keeping tool registrations alive while short-circuiting
  calls with a removal notice
- fire onDidReload after every plugin mutation (install/enable/disable/remove)
  so workspace consumers refresh contributions immediately
- TUI renders the removed status in the MCP panel/startup summary and shows an
  apply-immediately hint on the v2 engine

* feat(agent-core-v2): freeze plugin prompt inputs for live agents (2 files)

- snapshot the model skill listing and plugin system-prompt sections on the
  first successful prompt build and reuse the frozen values for the agent's
  lifetime, so plugin install / enable / disable / remove / reload never
  rewrites a live agent's prompt (same keep-live-sessions-stable philosophy
  as the MCP tombstone)
- freeze only on success: a not-yet-ready skill catalog or a failed
  enabledSystemPrompts() read must not pin empty values for the agent's
  lifetime
- refreshSystemPrompt still rebuilds on catalog change events but reuses
  the frozen values, so the prompt only moves when non-plugin inputs change
  (AGENTS.md, [tools] section, session tool policy, compaction); new agents
  snapshot the then-current state

* chore(changeset): add changesets for MCP tombstone and frozen plugin prompt inputs

* docs: describe immediate plugin changes and the removed MCP status on the v2 engine

* fix(klient): mirror the removed MCP server status in the wire contract

* docs: drop the legacy-engine behavior notes from the plugin and MCP pages

* fix(agent-core-v2): freeze plugin sections only on a loaded snapshot

- enabledSystemPrompts() resolves to its consumption fallback (never
  rejects) while the initial plugin load has failed; freezing that empty
  read locked plugin sections out of the live agent even after a later
  successful reload
- expose hasLoadedSnapshot() on IPluginService so resolvePluginSections
  can tell a real empty snapshot from the fallback before freezing
2026-08-06 19:19:51 +08:00
liruifengv
cfd14a1fe2
feat(kap-server): accept attachments on skill activation (#2693)
* feat(kap-server): accept attachments on skill activation

The :activate endpoint only took {args?}, so REST clients (web/desktop
composers) could not attach uploads to a /skill invocation — attachments
were silently dropped at the edge.

- activateSkillRequestSchema gains an optional attachments field carrying
  the image/video/file subset of the prompt content wire shape.
- The skills route resolves them through the same edge pipeline as prompt
  submissions (validate file refs → materialize/compress → convert),
  extracted from routes/prompts.ts into lib/promptMedia.ts.
- AgentSkillService.activate appends the resolved parts after the rendered
  skill prompt in the activation's user message; SkillActivationInput
  gains an optional content field. The native RPC/TUI path is unchanged.
- Attachment failures map to 40407 file.not_found / 40001
  validation.failed, mirroring the prompts route.

* fix(kap-server): drop the unused parseKimiFileUrl import in promptMedia

* refactor: address review — header-only comments in the skill domain, provider id on protocol URL sources

- agent-core-v2 keeps comments solely in the top-of-file block (scoped
  guide): SkillActivationInput.content documented in the skill.ts header,
  the activate() note folded into the skillService.ts header.
- packages/protocol's image/video URL source gains the optional
  provider-issued id, matching the kap-server wire schema so parsing the
  public contract no longer strips it.

* fix(kap-server): validate the skill before materializing activation attachments

An unknown or non-user-activatable skill name with attachments ran the
media pipeline first, streaming bytes into the session/cache dirs and
compressing images for a request that activate() would reject with
40415/40912. The route now checks the session catalog up front (the
service still re-validates) so invalid activations leave no disk or CPU
side effects.
2026-08-06 19:09:32 +08:00
Haozhe
8c766a6c30
feat(agent-core-v2): add the L3 unit layer and the Feature seam (#2678)
* feat(agent-core-v2): add the L3 unit layer and the Feature seam

- introduce the L3 Service/Fiber unit layer: the Service base class with this.provide/effect/on/get/ref capabilities, the fiber runtime with thenable FiberHandles, collection contribution points, and the per-scope-kind ScopeUnits materialization fold
- provide each scope's static registration batch as one atomic provideAll cascade transaction (waiting-area activation, sticky Failed on construction error)
- add the DI unit inspection surface: App-scope debug ledger / dependency graph / cascade history services and the kimi-inspect DI view
- add the Feature unit seam (IFeatureManager + feature assembly), port plan mode onto it, and add the contributed-command seam (agent-command domain + node-sdk RPC types)
- remove the legacy dep-graph tooling
- apply the header-only comment convention across src and test: strip non-header narration, keep the file header, tooling pragmas, and NOTE comments

* feat(kap-server): gate the event.di.* debug feed to kimi-inspect connections

- add an opt-in target set in SessionEventBroadcaster; the global fan-out
  now skips event.di.* frames for connections that never opted in, so
  kimi-web and other clients no longer receive the high-churn DI feed
- WsConnectionV1 opts a connection in when client_hello carries
  client_id 'kimi-inspect'; removeGlobalTarget drops the opt-in on close
- temporary gate until a client-declared event-type whitelist lands

* chore(agent-core-v2): fix oxlint errors in the DI unit layer

- build the live-ref container chain without aliasing this (no-this-alias)
- snapshot the materialized map with Array.from and document why the copy
  is required (no-useless-spread)

* test(klient): use string scope kinds in the lifecycle handle fakes

The engine's LifecycleScope is a string enum now; the facade test doubles
still returned the old numeric kinds and failed the handleWireSchema output
validation.

* build(nix): update the pnpmDeps fetch hash
2026-08-06 18:22:36 +08:00
liruifengv
7b2784b9b7
feat: surface the bound model and thinking effort on subagent UIs (#2679)
* feat: surface the bound model on subagent UIs

The subagent.spawned event now carries the display-normalized model alias
(the derived __secondary__ entry resolves to its base alias), so clients can
show which model a subagent is bound to. The TUI subagent card, swarm panel
header, and background-agent entry show it at spawn; the WS snapshot roster
and REST /tasks (background/detached subagents) carry it too, keeping the
model visible across client reconnects.

* feat: carry the subagent thinking effort alongside the model

The spawned event, snapshot roster, and REST /tasks now also carry the
child's effective thinking effort (read from the child profile at spawn, the
same vocabulary as agent.status.updated). UIs show it only when it diverges
from the main session's current effort — an inherited level adds no
information, and 'off' is never shown.

* feat(tui): show the bound model and effort in the /tasks browser

The task browser's Detail pane renders Model and Effort rows for agent
tasks (raw alias and level — it is the inspector surface, so no diff
filtering), and its minimum height grows to fit the new rows. The values
were already persisted on SubagentTaskInfo; the TaskInfo union, its zod
schemas (protocol, kap-server, klient contract), and the v1 type
declaration now carry them so nothing strips them in transit.

* feat(tui): show concrete subagent effort levels unconditionally

Display rule simplified: any concrete effort tier (low/high/max/…) is
shown next to the model — including when it matches the main session's
level. Only the boolean states stay hidden: 'off' (no thinking) and 'on'
(generic thinking) carry no level information.

* docs: trim the changeset entry

* fix(tui): keep the model and effort on background-agent entries across resume

replayBackgroundProjection only copied agentId/parentToolCallId/
description, so a background subagent that outlived a resume lost its
model/effort on the later terminal transcript entry. The projection now
threads the persisted values (catalog-mapped model; boolean effort states
dropped), and session replay passes the loaded model catalog through.

* fix(agent-core-v2): normalize the derived secondary alias regardless of the flag

A child bound while the secondary-model experiment was on keeps
__secondary__ in its persisted binding; if the flag is later switched off
with the recipe still configured, resolveSecondaryModel() gated the
normalization and the sentinel leaked back onto resumed subagents.
subagentDisplayModel now reads the recipe straight from config (the flag
gates new bindings, not the interpretation of existing ones), which also
drops SessionSwarmService's now-unused IFlagService dependency. Also adds
the SDK package to the release: the new SubagentSpawnedEvent/AgentTaskInfo
fields are SDK-visible types.

* fix(agent-core-v2): normalize the status-frame model at the source

A derived-bound child republishes agent.status.updated right after spawn
with its raw modelAlias, which overwrote the spawned event's normalized
display model on single-subagent cards (swarm headers were first-wins and
escaped). emitStatusUpdated now maps through subagentDisplayModel, a no-op
for the never-derived main agent. Also moves the inline comments added by
this branch into top-of-file headers per the v2 comment convention.

* fix(tui): clamp the /tasks detail frame to the available body

At terminals near the minimum height the forced 10-row detail frame
overflowed the body and truncated the preview frame's border. The detail
height now caps out at whatever leaves the preview its borders plus one
content row, with a regression test at exactly MIN_HEIGHT.

* fix: normalize inherited derived aliases and keep model/effort on replayed terminal entries

- resolveSubagentBinding's caller-fallback branch also maps through
  subagentDisplayModel: a caller itself bound to the derived entry (a
  resumed subagent making a nested Agent call) no longer publishes
  __secondary__.
- The replayed background-task terminal notification builds its metadata
  with the persisted model (catalog-mapped) and concrete effort, matching
  the live completion path.
- Drops the inline comments this branch added inside v2 test bodies; the
  scenario context lives in the source file headers.
2026-08-06 16:59:45 +08:00
Haozhe
2b893733f9
fix(kap-server): bypass Node arg quoting for explorer /select, on Windows (#2645)
explorer.exe parses its raw command line rather than argv, so Node's
default spawn quoting breaks the `/select,` argument whenever the path
contains spaces: the command line becomes `"/select,\"C:\...\""`, which
explorer rejects, silently opening the Documents folder instead of
selecting the file. Quote only the path portion and launch with
windowsVerbatimArguments so the command line keeps the documented
`/select,"C:\some dir\f.txt"` form.
2026-08-05 20:15:27 +08:00
Haozhe
510fbe7ec5
refactor(kap-server): wrap /api/v2/sessions in the v1 response envelope (#2644)
- return the domain-grouped page payload inside { code, msg, data,
  request_id } and carry business outcomes in code (40001 invalid
  params with details, 40922 page_token mismatch) instead of raw HTTP
  statuses plus an { error: { code, message } } body
- add ErrorCode.PAGE_TOKEN_MISMATCH (40922)
- register the route via defineRoute (shared runtime validation and
  envelope-wrapped OpenAPI docs); fold include-domain validation into
  the query schema and replace the preprocess/doc-twin pair with
  scalar-or-array union params
- update the kimi-inspect client to unwrap the envelope and sync the
  two AGENTS.md guides
2026-08-05 18:20:00 +08:00
Haozhe
6f1cd7ca22
feat: add v2 sessions API and spreadsheet-like session table in kimi-inspect (#2640)
- kap-server: add GET /api/v2/sessions with a domain-grouped response
  (workspace / meta / activity, opt-in git), status / archived /
  updated_after filters, three sort orders, and fingerprint-bound opaque
  cursor pagination
- kimi-inspect: rebuild the chat sidebar as a spreadsheet-like session
  table on the v2 endpoint — preset views (All / Opened / Archived /
  By workspace / Git), column visibility config, header sort toggles,
  cursor-paged Load more, and localStorage-persisted panel prefs
- live activity frames from the WS hub override the REST status badge;
  session created / meta-updated events invalidate the v2-sessions query
2026-08-05 17:28:11 +08:00
Haozhe
2a4990182d
docs: slim root AGENTS.md, move deep package docs to package guides (#2626)
- move the kimi-inspect, kap-server, transcript, and minidb project-map
  entries into new package-level AGENTS.md files
- move the standalone Agent class rule to packages/agent-core/AGENTS.md
- merge root-only agent-core-v2 facts (MCP persistence, trust routes,
  seed contract examples) into packages/agent-core-v2/AGENTS.md
- compress the remaining long project-map entries to summaries with
  pointers, and add an anti-bloat rule for map entries
- drop the stale server-e2e entry; the package no longer exists
2026-08-05 13:43:33 +08:00
Kai
98ee35afd2
feat(agent-core-v2): add custom agent identity (#2573)
* refactor(agent-core-v2): simplify context tags and shared copy

Rename the context-injection tags to `<skill-loaded>` and
`<plugin-instructions>`, drop the product prefix from the CronCreate tool
description and the default agent description, and point the MCP OAuth
callback page back to "your terminal" instead of naming one client.

The callback page is shared by the ACP host, the web UI, and embedding
hosts, so naming a single client was inaccurate there. The tags and the
two descriptions read exactly the same without the prefix. Verified no
runtime consumer matches the old tag names; the updated snapshots cover
the tool descriptions that changed.

* feat(agent-core-v2): add a switch for the product-documentation skills

Five builtin skills document this CLI itself — `update-config`,
`custom-theme`, `mcp-config`, `check-kimi-code-docs`, and
`import-from-cc-codex`. Their names and descriptions sit in the system
prompt on every turn, which is dead weight for runs that will never
reconfigure the CLI.

Add a top-level `builtin_product_skills` field (also settable through
`KIMI_CODE_BUILTIN_PRODUCT_SKILLS`) to drop them. On by default, so
nothing changes unless it is set; the trade when off is that the model
loses the guided flows for those tasks.

Filtering happens where the catalog is assembled — a later filter would
leave the skills advertised to the model. The whole section is one
scalar, so it exercises the section-level env binding branch and needs
its own strip: `stripEnvBoundFields` only walks object fields, so an env
override would otherwise be written back into `config.toml`.

* feat(agent-core-v2): add custom agent identity

Add an `[identity]` config section (`name`, optional `slug`, both also
settable through `KIMI_CODE_IDENTITY_NAME` / `KIMI_CODE_IDENTITY_SLUG`)
that sets the identity the agent presents: the name it calls itself in
the system prompt, the `User-Agent` product token sent to third-party
providers, and the client name announced to MCP servers. Leaving it
unset changes nothing.

Until now every one of these was fixed, which left no way to run the
agent as part of another product — an internal deployment, a fork with
its own branding, an embedding host.

The identity resolves inside the engine rather than being seeded by each
host, so it applies to every launch surface — including headless runs,
which today seed no display name at all and fall through to the built-in
default.

Two deliberate asymmetries:

- The display name is a filling value with a fallback chain (config >
  host-declared > the consumer's own default); the slug is a rewriting
  value with two states only, so with no identity configured the
  rewriting paths are equivalent to not existing.
- The rewrite happens in the outbound header assembly, the one layer
  that knows which vendor it is building for. Vendors declaring
  `hostHeaders: 'full'` keep the host's own product token, which that
  header set is built around and which backends key on; the configured
  identity applies to the third-party path.

Resolution is lazy throughout: config loads asynchronously, and a
constructor snapshot would freeze the pre-load value under some startup
orderings.

Two input edges the resolver has to absorb, since both would otherwise
reach the User-Agent builder and either break it or quietly rewrite the
header: blank and whitespace-only values read as unset in the file just
as they already did in the env, so a stray `name = ""` cannot claim an
identity; and a name that folds away to nothing under slug
normalization (a CJK-only name, say) falls back to a neutral token
rather than producing a blank product, which the builder rejects.

* fix(agent-core-v2): keep the file value when a scalar env binding fails to parse

`config.ts` documents that an env value failing its binding's `parse` is
ignored, and `applyEnvBindings` honors that for object fields by
assigning only when the resolved value is defined. `applySectionEnv`
returned the parse result straight through for whole-section scalar
bindings, so a blank or mistyped variable resolved to `undefined` and
cleared the configured file value instead of being ignored.

Nothing hit this before: every existing section either binds object
fields or is env-only. `builtin_product_skills` is the first
whole-section scalar binding, where exporting an empty or misspelled
`KIMI_CODE_BUILTIN_PRODUCT_SKILLS` would silently undo a configured
`false`.

* feat(agent-core-v2): extend the custom identity to discovery and global MCP

Two outbound paths still announced the built-in product name under a
configured identity:

- `DiscoveryService` read the host User-Agent straight from bootstrap
  args when refreshing provider models, so custom registries — which are
  third-party endpoints — saw the original token while chat requests to
  the same class of endpoint saw the configured one.
- `SDKRpcClientV2` builds its own global `McpOAuthService` plus a
  throwaway `McpConnectionManager` for server testing, neither of which
  goes through the workspace-owned manager that carries the resolver.

Both now resolve the identity from the App scope.

* refactor(agent-core-v2): neutralize remaining copy and align comments

The synthetic MCP authentication tool description is injected into the
model context and still named the product; it and the OAuth callback
pages now use client-neutral wording. "Return to your terminal" was no
improvement over naming a client — both assume what the host is, and
that page serves the ACP host, the web UI and embedding hosts alike.

Comments introduced by the identity work move into their module headers,
per the domain convention. Interface field docs stay: the rule names
functions, methods and statements, and field-level docs are established
across the codebase.

The new tests gain scenario headers and dispose the scoped hosts they
create, and the `[identity]` docs state which engine reads the section.

* fix(agent-core-v2): read the product-skill switch after config is ready

`BuiltinSkillSource` is the lowest-priority skill source, so the workspace
catalog loads it first — before `IConfigService` has finished loading — and
keeps the contribution it returns for the life of the handler, with no
reload path and no change event. Reading `builtin_product_skills` eagerly
therefore stranded the startup configuration: an explicit `false` could be
ignored for the whole process. `UserFileSkillSource` already awaits config
readiness for exactly this ordering; this source now does the same.

Also record the identity collaborator in the two module headers that gained
the dependency without documenting it, and scope the
`builtin_product_skills` docs to the engine that reads it, matching the
note the identity section already carries.

* fix(agent-core-v2): apply the product-skill switch to session-less listings

`builtin_product_skills = false` only reached the scoped skill source. The
SDK's `listWorkspaceSkills` and the server's `GET /workspaces/{id}/skills`
both composed the raw `BUILTIN_SKILLS` constant, and the web app feeds its
pre-session onboarding menu from that route — so the five product skills
stayed listed until a session existed, then vanished from the session's
catalog.

Move the decision into `visibleBuiltinSkills(enabled)` next to the constant
and route every consumer through it, reading the switch via the shared
`builtinProductSkillsEnabled`. Keeping "what counts as a product skill" in
one place is the point: three copies of the predicate would drift the next
time a builtin is added. The SDK listing also awaits config readiness,
which it did not do before.

* fix(node-sdk): await config before materializing the global MCP OAuth provider

`McpOAuthService` caches providers by store key and stamps the client name
when it first builds one, and the preceding `globalMcpConfig.get()` reads
`mcp.json` directly rather than through `IConfigService`. So a
`beginGlobalMcpServerAuth` call made right after the harness is created
could resolve the identity before config finished loading, pinning the
built-in label for the rest of the process — including the OAuth dynamic
registration a third-party MCP server records.

`testGlobalMcpServer` already awaited config readiness for its own reasons;
this path now does too.

* refactor(agent-core-v2): drop the unused builtin-skill registrar

`registerBuiltinSkills` stamped the raw constant into a catalog for "edge
composition without a Session" — exactly the shape that now has to respect
`builtin_product_skills`. It has no callers in v2 and is not exported from
the package index, so it was dead code that also stood as an invitation to
bypass the switch. v1 keeps its own copy.

Every remaining path composes builtins through `visibleBuiltinSkills`.

* fix(agent-core-v2): send the configured identity on custom-registry imports

`:import_registry` fetched a user-supplied third-party URL with a
hardcoded `kimi-code-kap-server` User-Agent, so the first request to a
registry announced the product while every scheduled refresh of the same
registry announced the configured identity. The hardcoded value was wrong
on its own terms too: that token names the server, and this path also runs
in the CLI.

Both services now project the identity through `identityUserAgent`, which
carries the two guards (no host header, or no identity) once instead of
per caller. The model catalog keeps an inline copy on purpose — kosong is
a foundational layer and must not import an app domain.

Sweeping the remaining outbound User-Agent sources found no further gaps:
WebFetch deliberately sends a Chrome-like UA, the models.dev catalog fetch
sends none from the CLI, and kap-server's `user-agent` reads are inbound.

* docs: scope the identity env vars and condense the changeset

The environment-variable reference advertised all three new variables
without noting that only the agent-core-v2 engine reads them; the
configuration page already carried that note. Added in both locales.

The changeset had grown into two paragraphs of implementation detail,
which is what would land in the CLI release changelog. `gen-changesets`
asks for one short sentence plus at most a one-line usage hint.

* docs(agent-core-v2): describe the identity as what the agent calls itself

The module headers had drifted into describing the feature by what it
keeps off the wire rather than what it configures. Reworded so they state
the capability: the identity is the name the agent uses for itself, and
the unset case is a no-op rather than something "safe". The product-skill
switch excludes skills rather than hiding them.

Wording only; behavior and structure unchanged.

* test(agent-core-v2): cover the identity on custom-registry imports

The import path switched from a hardcoded `kimi-code-kap-server` token to
the host User-Agent projected through the identity, but nothing asserted
it. Two cases pin both halves: a configured identity reaches the request,
and an unconfigured one leaves the host header intact — the second matters
because a single case would also pass if one hardcoded value had simply
replaced another.

Both fail against the previous implementation.

* fix(node-sdk): guard every global MCP OAuth path behind config readiness

`McpOAuthService` caches providers by store key and stamps the client name
when it first builds one, so any path that can materialize a provider has
to run after config has loaded. `beginGlobalMcpServerAuth` awaited
readiness, but `resetGlobalMcpServerAuth` reaches the same cache through
`invalidate()` -> `getProvider()` without waiting: resetting auth right
after the harness is constructed pinned the built-in client name, and the
await added to the begin path could not help because it then reused that
cached provider.

Rather than add the missing await, the accessor is now async and holds the
guard itself, so the service cannot be obtained before config is ready and
a future entry point cannot forget. The remaining `configReady` in
`testGlobalMcpServer` stays — that one is for its own `[mcp]` section read.

* fix(agent-core-v2): send the configured identity on models.dev requests

The directory fetch behind `listModelsDevProviders` / `getModelsDevProvider`
still hardcoded a `kimi-code-kap-server` User-Agent, so browsing or importing
from models.dev announced the built-in product — and claimed to be the server
even when running in the CLI. Only the custom-registry import had been fixed.

`getModelsDevCatalog` now takes the User-Agent from its caller: the module is
plain module-level state with no container access, and the value depends on
the host and the configured identity, which only the calling service can see.
All four third-party fetches in that service share one helper.

Where the host states no User-Agent, a neutral token stands in rather than
dropping the header — these are directories the service chooses to call, so
there is no host intent to preserve, unlike the provider requests the model
catalog assembles.

Both new tests fail against the previous hardcoded value.

* test(agent-core-v2): assert the product-skill set literally

The expected sets were derived from the same `productSpecific` field the
production filter reads, so a builtin silently losing its marker would just
move between sets and leave every assertion green — while staying visible to
the model once the switch is off. The five names are now literal, with a test
asserting the marked set matches them exactly.

Dropping the marker from one skill now fails four tests instead of none.

Also states the App scope in the identity contract header, per the domain's
comment convention for contract files.

* fix(agent-core-v2): normalize the host-declared display name too

Blank and padded values were normalized on the config side but not on the
host fallback, so an embedding host passing `displayName: "   "` rendered
"You are   ," into the system prompt, and a padded name kept its padding.
Same rule now applies to every source of the name.

Also names `agentIdentity` as the collaborator in the request-headers
adapter header, which described the value it obtains without saying which
domain resolves it.

The three new cases fail against the previous implementation.

* fix(agent-core-v2): keep the configured slug when the host sends no User-Agent

The neutral fallback added for hosts that state no `User-Agent` discarded a
configured identity along with it: `identityUserAgent` returns `undefined`
as soon as there is no host header to rewrite, so `?? DEFAULT_IDENTITY_SLUG`
sent the literal `agent` even when `[identity].slug` was set — precisely the
case that fallback exists to serve. The configured slug now stands on its
own, with the neutral token reserved for having neither.

The four combinations of (host header, configured slug) had three tests; the
missing one is the one that was wrong. It now fails without this change.

`outboundUserAgent` also awaits config readiness before reading the identity,
so a browse issued right after bootstrap cannot send the pre-load value — the
guard lives in the accessor rather than at its four call sites, matching how
the same race is handled elsewhere in this branch.

Both headers here and in `discoveryService` now name `agentIdentity` as the
collaborator resolving that token.

* test(acp-server): follow the renamed skill-activation tag

`acp-server` arrived on main after the tag rename, so its two assertions
still expected `kimi-skill-loaded` and failed once the branches met. Also
updates the web app's CSS comment, which named the old tag from the start
of this branch — a comment, so nothing ever failed on it.

Found by CI: the merge verification only ran agent-core-v2's suite, and
this package is neither a dependency nor a dependent of it.

* fix(agent-core-v2): present the configured slug on registry refreshes too

The previous round taught the import path to fall back to the configured
slug when the host states no `User-Agent`, but left the scheduled refresh
of the same registry on the bare projection — so one registry could see
`acme` on import and the runtime default on refresh.

Extracting `identityUserAgent` had made the two paths share a function
without sharing the policy. The choice itself is now the shared piece:
`identityUserAgentOrDefault` always yields a value, for the directories
this process chooses to call, while `identityUserAgent` stays the form
that rewrites only what the host already sends — what a provider request
needs, where the host's silence is its own choice.

* docs(agent-core-v2): move new member docs into the module headers

The domain's comment convention is absolute — comments live solely in the
top-of-file block — and I had read the "functions, methods, or statements"
clause as leaving interface members out. It does not: only 25 of 734 v2
sources carry an indented block, so the members I documented were the
exception, not the pattern.

Seven members across six files move into their headers. `types.ts` had no
header at all, so it gains one.

* fix(agent-core-v2): connect session MCP overlays after config is ready

The shared manager reaches `connectAll` through `initialize()`, which awaits
the config domain first; `sessionOverlay` called it straight away. A session
carrying ephemeral `mcpServers` created right after bootstrap therefore
resolved the client name before config had loaded and initialized under the
built-in one.

The blast radius is wider than that one connection: a remote server sends
the overlay through `hasTokens()`, which materializes an OAuth provider on
the *shared* service and caches it by store key — so the early name outlives
the connection that raced. The overlay now connects behind `mcpConfig.ready`,
leaving the returned readiness promise unchanged.

* fix(agent-core-v2): reload builtin skills when their switch changes

The workspace catalog keeps each source's contribution for the life of the
handler, so a `builtin_product_skills` toggle never reached an existing
handler's sessions. That was harmless while every surface read the same
constant — but routing the session-less listings through the config made the
two views disagree, since those read the switch on every call.

Follows `ExtraFileSkillSource`: subscribe to the owning section and fire
`onDidChange`, which the catalog already turns into a source reload. The
test asserts an unrelated section does not trigger it.

* fix(agent-core-v2): apply the identity to self-configured web services

`[services.moonshot_search]` and `[services.moonshot_fetch]` name their own
`base_url`, so both services can point at an endpoint the user chose — but
each forwarded the host request headers verbatim, sending the built-in
product token there under a configured identity.

Only the services-config path is rewritten; the managed OAuth path keeps the
host headers as they are, being the endpoint the session authenticated
against. The distinction is the same one the model catalog draws per vendor.

`identityHeaders` carries the rewrite across a whole header set, so this is
the fourth caller sharing the projection rather than repeating its guards.
A pair of tests pins both halves.

My earlier sweep classified these two as official by their names instead of
asking who chooses the URL, which is why they were missed. The contract
header is also condensed here, per the convention below.

* docs(agent-core-v2): condense the identity headers to their contracts

The comment convention is one sentence with two halves — comments live only
in the top-of-file block, *and* that block states the module's role without
narrating implementation. Moving the member docs up last round satisfied the
first and broke the second: the headers ended up spelling out the slug
folding algorithm, the strip mechanics, and the load order.

Kept what a caller or the next editor would get wrong without it (why the
value is read rather than snapshotted, what `undefined` obliges a consumer
to do, why this source waits for config). Dropped what the code already
says. 22/12/12/13 lines, against 53 in `catalogService.ts` — length was
never the problem.

* fix(agent-core-v2): rebuild active prompts when the builtin skills change

Reloading the catalog on a `builtin_product_skills` toggle left existing
agents holding the old listing: `AgentProfileService` refreshes the prompt
only for the plugin source, so a disabled switch kept advertising skills
that were gone, and enabling it left them missing until an unrelated
refresh.

The plugin source is special because it also contributes prompt sections
(#2314), and the file-backed sources are left out for cost — their fs
watches would rebuild every agent's prompt on each edit. The builtin source
has no watch: it changes only when its config switch is toggled, so it
belongs with the plugin source rather than with the file ones.

Subscribing to the catalog rather than the config section is load-bearing.
The catalog fires after the contribution is replaced, whereas a config
subscription would race the reload, and `resolveSkillListing` only awaits
the catalog's *initial* readiness — so the rebuilt prompt could read the
listing it was meant to replace.

The source id is a named constant now, so the subscription does not match
on a bare string.

* refactor(agent-core-v2): freeze the agent identity for the process lifetime

The identity is announced outward (MCP initialize, OAuth registration,
provider request logs) and cannot be re-announced, so mid-process changes
could only ever apply partially. Resolve it once when config first loads
and hold it for the life of the process: IAgentIdentity now hands out a
frozen snapshot via resolved()/current(), carrying finished products
(outbound User-Agent variants, rewritten header set) so call sites stop
composing host headers with the slug themselves. The kosong host-headers
port carries two finished layers and the catalog only picks one; consumers
gain no invalidation obligations because the value can never change after
the freeze. [identity] edits take effect on the next start (documented).

* fix(agent-core-v2): locate the User-Agent header case-insensitively

HTTP header names are case-insensitive, but the snapshot builder looked up
'User-Agent' by exact key: an embedding host spelling it 'user-agent' got no
third-party UA and kept its own product token on the services path even with
an identity configured. The builder now locates every case variant and
rewrites each in place, keeping the host's spelling. Also corrects the two
web-service headers that still described both paths as sending the bootstrap
headers, naming agentIdentity as the collaborator behind the config path,
and documents that a resumed session keeps its recorded system prompt.

* fix(agent-core-v2): attribute header provenance from the finished third-party layer

Inspection reconstructed the non-full host layer from the raw headers with an
exact-case 'User-Agent' lookup, so a host spelling the header 'user-agent'
got a resolved User-Agent with no provenance entry even though the runtime
sends the rewritten value. buildModel now captures the port's finished
third-party layer in the trace and attribution reads it, keeping inspect()
on the same resolution pass as get(). Also condenses the identity contract
header to its external role, and documents that an existing MCP OAuth
authorization keeps the client registration it was granted under (reset the
server's auth to register under the new identity).

* fix(agent-core-v2): keep web tool backends from racing the identity freeze

An env-configured [services] endpoint is visible before config finishes
loading, and FetchURLTool / WebSearchTool materialized their backends at
construction — so a fast bootstrap could hit the identity snapshot's
pre-freeze guard during agent creation, and the composed backend pinned
config and login state for the agent's lifetime against the service's
documented per-call resolution. Both tools now resolve their backend per
invocation, the WebSearch activation gate checks presence alone through the
new hasWebSearchProvider() (no provider composition, no identity read), and
bind() awaits the identity freeze before materializing the model, whose
resolution reads the identity through the host-headers port.
2026-08-04 22:35:15 +08:00
Haozhe
119a33f7f1
feat(minidb): persistent index generations and lifecycle hardening (#2604)
Some checks are pending
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* perf(minidb): skip idle everysec fsyncs and add lifecycle stats

- everysec WAL now fsyncs on the timer only while dirty (tracked by a
  write/sync generation watermark); close() keeps its unconditional
  final sync, and background sync failures surface via walFsyncErrors
  plus a sticky lastWalFsyncError instead of being silently swallowed
- add WAL queue/group-commit counters (walQueuedBytes,
  walMaxQueuedBytes, walGroupCommits, walGroupCommitFrames) and
  lifecycle phase stats: recovery bytes/frames/duration, index/text
  rebuild durations, compaction total/snapshot/rotation/postings
  durations, rotation pause, and query candidates/decoded/sorted
  rows; add a syncIntervalMs open option threaded through compaction
  WAL rotation
- rewrite the bench on fixed-seed synthetic data with a stable
  machine-readable JSON report (cold open 10k/50k/100k, word/ngram
  search, idle-fsync acceptance, 100k compaction, event-loop delay,
  peak heap/RSS per scenario) and pin the schema in
  test/bench-json.test.ts; add app-side baselines with loose
  complexity budgets in sessionIndex and searchService tests
- fix ClusterDb lock-pool closeAll() leaking in-flight shard opens
  and drain the query store's async close on server shutdown,
  eliminating the ENOTEMPTY directory-teardown race

* perf(minidb): bound startup rebuild and steady-state hot paths

- rebuild all derived indexes in one shared store walk: a single decode
  per record fans out to staged builders, dt rebuild reads record
  metadata only, and index-less opens no longer decode at all
- rank full-text results with a bounded min-heap plus a stable key
  tie-break instead of sorting every candidate
- remove/overwrite text docs via a docID -> delta-terms reverse map
  instead of scanning the whole delta vocabulary
- validate unique batches incrementally against touched postings
  instead of copying the full per-index owner map
- reap due TTL entries from the expiry heap on the write path instead
  of a full-store sweep per write

* feat(session-index): add minidb read model with keyset pagination

- add ISessionIndex read-model lifecycle (prepare/status, ready/degraded
  states) behind the persistence_minidb_readmodel experimental flag
- add ISessionIndexMirror write side recording fresh summaries into a
  bounded, coalescing queue after the authoritative document is durable
- replace the offset cursor with before/after keyset pagination; rename
  list/countActive to listRecent/count
- extend IQueryStore with ordered columns and pageByColumn, plus
  getMany/listKeys/dropCollection
- wire the read model through kap-server routes and start, and update the
  klient sessions contract
- index every session for global search instead of the 500 most recent

* feat(kap-server): bound search sync lifecycle, pagination, and query budgets

- split search requests from sync work: searchIndex() no longer awaits
  runSync/reopen/reindex; a single-flight sync coordinator with debounce
  and backpressure runs in the background, stale generations keep
  serving with explicit stale/degraded state, and refresh/sync/reindex
  failures surface via lastRefreshError instead of being swallowed
- scope file-meta keys by session id (\0meta\file\<sessionId>\<hash>)
  with lazy + one-shot background migration from the legacy hash-only
  keys, so one session sync only touches its own meta rows
- make authoritative scans incremental (mtime/ino/size rescan
  conditions, unchanged files no longer rewrite meta) and read wire
  deltas in 1 MiB chunks instead of whole-file buffer + split
- replace offset pagination with versioned v2 keyset page tokens
  (fingerprint + index generation + sort boundary); generation changes
  fail old tokens with invalid_page_token, legacy v1 offset tokens are
  served once and upgraded, and pages collect via bounded top-K instead
  of full sort + offset skip
- add query budgets enforced at the postings/score stage: max query
  terms, literal length cap, postings visit budget (minidb
  searchBounded/maxVisits with prefix decoding that never fabricates
  hits and skips the postings LRU), candidate caps, deadline and text
  budget; truncation is reported via incomplete reasons
  candidate_cap/postings_budget/deadline
- reopen read-only dbs by opening the next handle before closing the
  previous one so a failed refresh keeps the old generation serving;
  failed opens now self-heal through search traffic

100k-message bench: first page p95 < 300ms and page-100 cost on par
with page 1; event-loop delay during queries stays sub-millisecond.

* fix(minidb): poison, roll back, and recover the WAL on write failures

- give WAL writes a commit point: a failed flushBatch poisons the WAL
  (WAL_POISONED, tracked separately as walWriteErrors vs walFsyncErrors),
  rejects queued frames in reverse enqueue order, and stops scheduling
  further batches; everysec background sync failures stay non-rejecting
  per stage-1 semantics
- recover in place to a known-safe point: a serialized recovery chain
  truncates the WAL back to the first un-acked frame, rebuilds
  size/nextOffset, and clears the poison; writes queue behind the
  recovery gate (zero-cost when idle), a failed truncate flips the
  instance into an explicit writeDisabled state, and a stale truncate
  offset (WAL file replaced by a rotation) skips the truncate
- roll failed flush groups back as a unit: frames are stamped with
  their batchId, MiniDb keeps per-group earliest pre-state, and the
  first rejection restores every key of the group (rejected writes no
  longer reappear after reopen, and in-memory state matches reopen for
  any failure interleaving); the per-op seq guard remains for
  cross-group and rotation-retry races
- wrap applyOp and the following in-memory mutations so a contract
  violation poisons the WAL and rolls the group back instead of
  escaping as a half-commit; frames never enqueued (seal race) roll
  back per-op without poisoning
- tag errors past the commit point with ambiguous: true so callers can
  distinguish "definitely not applied" from "maybe applied but revoked"
- close() waits for the recovery chain to go idle and backup() fences
  behind in-flight recovery before copying files

Controlled A/B bench (22 alternating iterations, 100k concurrent sets):
write-path throughput regression is within the 2% budget.

* fix(minidb): turn the file lock into an instance-owned serialized lease

- distinguish lock ownership by instance instead of pid: every acquire
  mints a pid:uuid token carried by lock/bid/watch files, inspect().mine
  compares tokens, liveness still follows pid, tokenless legacy files
  keep the old stale-takeover path, and hasLiveForeignWatch excludes
  self by token so same-process contenders see each other (closing the
  double-win takeover and the cross-instance release); a live same-pid
  lock is still respected, and re-acquiring a held lock is idempotent
- serialize acquire/renew/release through a per-instance promise-chain
  mutex: renew re-checks held inside the chain and release waits for an
  in-flight renew, eliminating the renew/rename-after-unlink ghost lock
- make MiniDb.close() a state machine (open/closing/closed) with a
  shared closePromise: cleanup runs per-resource try/catch in
  dependency order (text indexes, store, valueReader, WAL, lock),
  aggregates every cleanup error into an AggregateError, stays in
  'closing' on failure so a retry finishes the cleanup, and no longer
  leaks the lock when the WAL close fails; a rejected in-flight
  compaction no longer escapes the cleanup pass

* fix(minidb): keep readers on one consistent file generation

- add an internal persistent-files module as the single source of truth
  for the persisted file set (snapshot, WAL, sidecars, postings
  pattern, fingerprint subset); lock-pool fingerprints, persistentFiles,
  open stale-tmp cleanup, and backup/restore filtering all derive from
  it, and fingerprints upgrade to dev:ino:size:mtimeMs so compound
  sidecar changes can no longer hide from cluster readers
- pair snapshot and WAL generations during recovery (transitional
  stat-pairing until stage-5 manifests): each pass anchors the fds it
  scans, re-stats afterwards, tolerates append-only WAL growth, retries
  bounded times on generation churn with a clean store reset, and
  throws RECOVERY_GENERATION_CHURN when churn exceeds the budget; the
  disk-mode ValueReader attach re-validates inodes so stale offsets
  never read a replaced file
- make the rotation directory fsyncs strict: failures abort the
  rotation through the existing rollback path instead of being
  swallowed, while platforms without directory fsync degrade once with
  a warn and stats.dirFsyncUnsupported

* fix(minidb): serialize index-definition sidecar mutations, persist before publish

- extract the promise-chain mutex into a shared createSerializer() and
  give each sidecar family (secondary/compound/text) its own chain:
  create/drop run uninterruptibly (memory change + rebuild + persist),
  different families stay independent, and the data write path never
  shares these chains
- reverse the publication order to staged -> persist -> publish: a
  create stages the definition, rebuilds via the staged builder,
  persists the sidecar including the new definition, then publishes
  atomically; any failure discards the staged state leaving live and
  sidecar untouched (no phantom indexes, retry-safe); a drop persists
  the sidecar without the definition before removing it live; text
  index create/drop adopt the same pattern, replacing the hand-rolled
  unwind, and a dropping marker keeps compaction postings rebuilds out
  of the persist window
- feed staged indexes from the incremental write path (add/remove/
  checkUnique/checkUniqueBatch visit live+staged) so writes landing in
  the persist window are not lost at publish; queries still see live
  only
- harden writeFileAtomic: instance-unique tmp names (.tmp-pid-seq),
  a strict fsyncDir after rename so a successful persist is crash
  durable, and whitelist-based stale-tmp cleanup that never touches
  lock tmp files

* fix(minidb): validate writes before any side effect, canonicalize values once

- canonical value at the write boundary: the json codec re-parses the
  encoded bytes once and every downstream consumer (unique checks,
  secondary/compound/text indexes, dt extraction) sees exactly the
  persisted representation, so getter/toJSON/Proxy documents can no
  longer diverge between the index view and the storage view
- reorder the set/batch pipeline so every fallible check happens before
  any visible side effect: prepare (key/ttl checks, encoding, canonical
  decode, index field extraction, tokenization) -> unique checks ->
  ensureMemoryFor eviction -> commit; a constraint failure now leaves
  the database untouched (no more evicted victims on rejected inserts),
  and applyOp is structurally pure against pre-validated data
- tokenize at the prepare boundary: TextIndex gains prepareAdd/
  addPrepared and the buildQueue carries validated key+tokens mutations
  instead of raw docs, so a throwing custom tokenizer can no longer
  poison the live view or the queue, and custom-tokenizer output is
  rejected per token over 0xffff bytes before it can permanently break
  postings rebuilds; prepared tokens are keyed by index instance so a
  same-name drop+create mid-write re-tokenizes instead of crossing
  tokenizers
- strict batch structure validation: scanBatchOpRefs/decodeBatchOps
  reject unknown op types, out-of-bounds lengths, and trailing bytes
  (offset must equal body length), so a valid-CRC but malformed batch is
  skipped as a unit and counted via RecoveryInfo.corruptBatches instead
  of being partially applied

Bench vs the stage-1 baseline: json write throughput regression is
within the 5% budget (median ~2-4% depending on the measurement).

* feat(minidb): add OpTracker drain primitive and atomic backup, harden tests

- introduce the internal OpTracker (close gate + in-flight counter with
  enter/leave/close/whenIdle and reference-counted pause/resume) and
  drive every shutdown/drain path from it: WAL background syncs are
  tracked so close() waits out an in-flight sync before closing the fd,
  cluster lock-pool closeAll() closes the gates and drains busy
  callbacks before closing handles, and MiniDb writes pass a write gate
- make backup() atomic with a defined linearization point: pause the
  write gate, drain in-flight writes (every acknowledged write is now
  included), copy to a sibling temp dir with per-file fsyncs, write the
  manifest last as the commit marker, and rename into place; failures
  clean up and leave no partial backup, and concurrent writes are
  rejected with BACKUP_IN_PROGRESS
- reap emptied compound-index groups on remove (the groups map no
  longer grows monotonically), move the open-time mkdir behind the
  readOnly check so a read-only open of a missing directory fails with
  ENOENT instead of creating it, and never run a destructive rebuild
  for a read-only open failure (explicit or onLockFail fallback)
- consolidate every review fault-injection repro into the formal suite
  behind deterministic barrier helpers (programmable writev/sync/
  rename/tokenize hooks) and convert the six timing-based tests to
  barrier/tick-driven assertions; the .tmp repro scripts are removed

The converted timing tests and the full suite pass 50 repeat runs
(including under CPU load injection) with zero flakes.

* feat(minidb): persist derived indexes as atomic generations, open from WAL delta

- checkpoint the store, dt/secondary/compound indexes, and text
  dictionary/postings/docs into immutable generations under
  generations/g-NNNNNN published atomically (tmp build, per-file
  checksums and fsyncs, dir rename, CURRENT swap, strict dir fsyncs);
  the manifest records the format version, WAL/snapshot checkpoint
  anchors, per-index definition hashes, and codec/value-mode
  compatibility
- open now loads the published generation and replays only the WAL
  delta after its checkpoint: no full value decode, corpus
  tokenization, or postings rewrite on a normal reopen (warm opens are
  3.5-13.8x faster at 100k/1M records); a definition change rebuilds
  only the affected index, and corrupt generation files fall back to
  the previous generation or the legacy full recovery without ever
  touching the authoritative snapshot/WAL
- build generations transactionally with compaction (rotation plus
  derived state publish as one unit, replacing the synchronous
  rebuildTextPostings tail), capture concurrent writes through a sealed
  op queue with byte/op caps, hard-link clean postings and the snapshot
  into the new generation, and repoint every live text base into the
  CURRENT generation after publish
- cluster/read-only refresh watches CURRENT and the WAL watermark:
  pure generation publishes keep readers on incremental catch-up while
  rotations reopen onto the new generation; writers building the next
  generation never disturb readers of the current one
- legacy databases open through the old path unchanged and gain their
  first generation in the background; OpenOptions.indexGenerations:
  false fully restores the pre-generation behavior

* feat(minidb): workerize text-index builds and split MiniDb into facets

- split the monolithic src/index.ts into facet modules (mini-db, types,
  value-codec, memory-guard, backup, query-engine, text-registry,
  wal-group, generation-builder/loader, write-path, read-path,
  index-admin, lifecycle, stats) and move text-index.ts to text-index/
- run corpus-scale text-index builds off the main thread via the bounded
  worker engine (src/worker/), exported through the new worker-runtime
  subpath, with inline fallback for small corpora and rollback switches
- defer the open-time fallback text rebuild into a maintenance task;
  searches on a not-yet-committed base raise TextIndexBuildingError
- add the unified maintenance scheduler, bounded async read surface,
  and a maintenance bench
- kap-server search: switch to searchBoundedAsync and serve the
  building page while the index base rebuilds after fallback recovery
- kimi-code: install the SEA-bundled minidb text-build worker at
  startup, bundle it via the native asset scripts, and add the
  startup-trace util plus the KIMI_TUI_INPUT_LATENCY debug probe

* fix(minidb): treat win32 EPERM as unsupported directory fsync

- extract isUnsupportedDirectoryFsyncError and cover win32 EPERM
- drop the one-shot console.warn; stats.dirFsyncUnsupported carries the degraded state

* fix(kap-server): harden search-index dispose and drain lifecycle

- dispose() now closes an OpTracker gate and drains in-flight sync/refresh
  passes before closing the db, so no background write can hit a closed
  handle; the deleteSessionDocs loop and trailing stats write skip once
  the gate closes (review #20)
- drainGlobalSearchDisposals loops to a fixpoint so disposals registered
  while a drain is in flight are also awaited (review #21)
- pin the post-open failure semantics with a regression test: a failed
  text-index setup closes the handle and the next open reacquires the
  writer lock instead of self-locking read-only (review #19)
- export OpTracker from the minidb root for the search service's drain

* chore: fix oxlint type-aware lint errors
2026-08-04 20:38:42 +08:00
zy
3126422757
feat(mcp): carry an absolute expiresAt on the OAuth authorization-url update (#2609)
* feat(mcp): carry an absolute expiresAt on the OAuth authorization-url update

The authenticate flow waits for the OAuth callback with a fixed budget,
but the authorization-url tool update surfaced to embedding hosts did not
say when that window ends — hosts had to hardcode a mirror of the
15-minute constant to render countdowns. Include the absolute deadline
(now + effective wait timeout) in the update payload for v1 and v2.

Resolve #2607

* fix(protocol,kap-server): accept expiresAt in the OAuth authorization-url update schemas

The zod validators mirrored the pre-expiresAt payload shape and would
strip the new field at the kap-server boundary.

---------

Co-authored-by: zouying <zouying@moonshot.cn>
2026-08-04 20:02:47 +08:00
github-actions[bot]
4ac7240fff
ci: release packages (#2469)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-04 15:11:04 +08:00
Haozhe
c39687318c
fix(kap-server): accept question ids containing colons on resolve (#2585)
Some checks are pending
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(kap-server): accept question ids containing colons on resolve

Some OpenAI-compatible providers emit tool_call ids like
`AskUserQuestion:0`, which the question service adopts as the question
id. The action-suffix parse then rejected the bare resolve POST as an
unsupported action (40001), so clients could never submit answers.

When the suffix parse fails, fall back to matching the full tail
against the pending question list before emitting 40001. Also add
maxRetries to the test home cleanup to absorb the async query-store
shard flush (ENOTEMPTY on macOS), matching fs.test.ts.

* fix(kap-server): preserve 40902 on duplicate resolve of colon-id questions

A retried bare resolve of a colon-bearing question id re-entered the
invalid-suffix fallback after the question settled, found no pending
match, and returned 40001 — bypassing the recently-resolved idempotency
window. Accept the tail in the fallback when it is recently resolved so
the shared duplicate-resolve path emits 40902 as documented.
2026-08-04 12:58:51 +08:00
Haozhe
1328b32037
feat(acp): add experimental agent-core-v2 ACP server (kimi acp-v2) (#2571)
* feat(acp): add agent-core-v2 ACP server

- add ACP session lifecycle, configuration, permissions, and event bridging
- expose the experimental kimi acp-v2 command with terminal authentication
- add integration coverage and workspace build configuration

* test: use neutral example domains in test fixtures and docs

- replace placeholder hostnames (evil.com, foo.com, internal.corp,
  real.corp) with example.test / example.com in agent-core-v2 and
  kap-server tests
- replace fixture emails (x@y.com, a@x.com) with example addresses in
  minidb tests and README

* fix(acp): align acp-server with agent-core-v2 interfaces and address review

- add missing appendText to AcpHostFileSystem (IHostFileSystem drift)
- replace IAgentPromptService.prompt with inject
- use Turn.cancel() instead of abortController
- gate FS reverse-RPCs on client capabilities, fallback to local FS
- return PROTOCOL_VERSION constant instead of echoing client version
- remove misleading mcpCapabilities from initialize response
- dispose old session wrapper before replacing on load/resume
- fix object stringification lint error in convert.ts
- add acp-v2 to expected CLI sub-command list in test

* fix(acp): use enqueue for prompt submission, stop advertising unimplemented builtins

- replace IAgentPromptService.inject with enqueue so onBeforeSubmitPrompt
  hooks (prompt-blocking policy) are not bypassed
- stop advertising builtin slash commands (/help, /status, etc.) until
  builtin command execution is implemented
- add comment explaining appendText stays local (ACP has no append RPC)
- update skills test to match new availableCommands behavior

* fix(acp): filter turn events by turnId, surface auth failures as auth_required

- track turnId in driveTurn and ignore events from unrelated turns,
  preventing queued prompts from settling on the running turn
- reject prompt requests with auth_required when turn fails with an
  auth-related error code, enabling ACP client re-auth flow

* fix(acp): gate acp-v2 behind experimental flag, filter sessions by cwd

- add acp-v2 experimental flag (KIMI_CODE_EXPERIMENTAL_ACP_V2) and gate
  CLI command registration behind it
- filter session/list results by requested cwd instead of returning
  sessions from all workspaces
- detect hook-blocked prompts via PromptHandle.state and add TODO for
  streaming block messages once the hook context exposes them

* refactor(acp-server): rewire ACP server onto the klient facade

- replace direct agent-core-v2 scope/service access (ISessionLifecycleService,
  ISessionIndex, IEventBus, ISessionInteractionService, etc.) with the Klient
  facade: klient.global.sessions / klient.session(id) / agent('main') handles
- drive turns via agent.prompt() + session-level agent event subscriptions
  instead of per-prompt IEventBus wiring; settle on turn.ended
- route approval/question bridging through session.interactions events
- hide the thinking config option and skill catalog behind KLIENT-GAP markers
  until klient exposes those surfaces
- acp-fs: pass realpath through to the local inner backend
- klient: session.restore() rejects both null and undefined handles

* feat(agent-core-v2): add session delete and ephemeral per-session MCP servers

- add ISessionLifecycleService.delete: close a live session first, then
  remove its persisted data, evict the index read-model entry, and append
  a deleted tombstone to session_index.jsonl; unknown ids raise
  session.not_found
- add CreateSessionOptions/ResumeSessionOptions.mcpServers: session-owned
  MCP overlay merged over the workspace manager via
  MergedMcpConnectionView (an ephemeral name shadows a workspace server),
  never persisted, released when the session scope tears down
- return PromptLaunchResult from activateSkill so callers get the
  launched turn id and activation failures (unknown skill, busy) surface
- add ISessionSkillCatalog.list() as a wire-friendly catalog snapshot
- add ISessionIndex.remove for read-model eviction on delete

* feat(klient): expose session delete, per-session MCP, skills, and stream events

- session lifecycle contract: delete, resume/restore options, and
  CreateSessionOptions.mcpServers (ephemeral per-session MCP servers)
- add the session skills contract and facade accessors for the
  wire-friendly skill catalog snapshot
- register tool.call.delta, tool.progress, and compaction.* agent stream
  events so consumers can subscribe with typed payloads

* feat(acp-server): align ACP v2 server with acp-adapter capabilities

- complete the klient-facade rewire: ACP client connection holder and
  the terminal/* reverse-RPC runner routed through the Agent scope
- negotiate the protocol version on initialize instead of pinning v1
- compress oversized prompt images at the ACP ingestion point with a
  format gate, caption, and persisted originals; a cancel arriving
  mid-compression settles the prompt as cancelled without a turn
- stream tool call args via tool.call.delta (lazy pending create,
  cumulative replace, started upgrade) and refresh titles via
  tool.progress status updates
- report compaction progress and results after /compact via the
  compaction.* events
- answer unknown slash commands locally instead of sending them to the
  model
- accept legacy "<id>,thinking" model ids and legacy approve /
  approve_for_session approval option ids
- keep sessions without cwd metadata in cwd-filtered session/list
- sanitize wire errors: auth codes map to auth_required, turn.agent_busy
  to invalid_request, everything else to a fixed internal-error message
- bump @agentclientprotocol/sdk to ^1.3.0

* fix(cli): drop stale registerServerCommand call and sherif ACP SDK split

- commands.ts called registerServerCommand, which no longer exists on
  current main (the deprecated `kimi server` shim is registered via
  registerWebCommand), breaking typecheck, build, and every CLI test
  that builds the program
- sherif rejects the @agentclientprotocol/sdk major split between
  acp-adapter (^0.23.0, production kimi acp) and acp-server (^1.3.0,
  experimental); the two hosts legitimately target different SDK
  majors, so ignore the dependency in the sherif invocation

* test: update fixtures for acp-v2 flag and domain rename, refresh nix deps hash

- kap-server origin.test: two CORS cases still used foo.com after the
  whitelist moved to foo.example.com, so the origin was no longer
  whitelisted and the expected CORS headers were withheld
- node-sdk config.test: expect the new acp-v2 experimental flag in the
  harness feature metadata
- flake.nix: update the fetchPnpmDeps hash for the
  @agentclientprotocol/sdk 1.3.0 lockfile change

* fix(acp): widen the ACP v2 auth gate beyond OAuth-only providers

The gate consulted only auth.summarize(), which iterates providers
declaring an oauth section — configurations that authenticate with a
plain apiKey or provider env-bag credentials (no OAuth at all) were
rejected with auth_required even though the default model is fully
usable.

- klient: expose authSummaryService.ensureReady on the global auth
  facade (the contract already declared it)
- acp-server: gate on the engine's own readiness probe for the default
  model — config apiKey / env-bag / OAuth token all count, matching how
  the model is actually used — and fall back to "any logged-in OAuth
  provider" (the legacy adapter's first branch)
- test: an apiKey-only config passes the gate with auth enforcement on;
  the OAuth logout regression is unchanged

* fix(acp): reject concurrent prompts instead of displacing the in-flight turn

A second session/prompt while a turn is running overwrote the session's
only TurnDriver: the engine quietly queues plain prompts submitted
during an active turn (the launch resolves undefined, indistinguishable
from a hook-blocked launch), so the first prompt never settled and both
turns' events went unattributed.

Guard both model-bound launch paths (plain prompt and skill activation)
with a synchronous in-flight check and reject with invalid_request
(turn.agent_busy), matching the legacy adapter's busy semantics. Local
slash handling (builtins, unknown-command answers) is unaffected.
2026-08-04 10:20:24 +08:00
Haozhe
21185447fe
feat(agent-core-v2): add tokenCounting service with strategy config and measured anchors (#2563)
* feat(agent-core-v2): add tokenCounting service with strategy config and measured anchors

- add IAgentTokenCountingService as the single owner of token counts:
  context size, full-request size, and estimate primitives, replacing
  the scattered contextSize/tokenEstimate/fullCompaction paths
- add [token_counting] config section with strategy = measured+estimated
  (default) / measured / estimated, plus the KIMI_TOKEN_COUNTING_STRATEGY
  env override; measured zeroes all estimates, estimated ignores anchors
- keep a live measured-anchor ledger in TokenCountingModel: each LLM
  exchange writes a real anchor, undo truncates the ledger so the
  surviving prefix restores its REAL measured size instead of a
  re-estimate, and compaction rebases to a single anchor that blends the
  compaction exchange's measured summary output tokens
- skip writing an anchor when the stream reports no usage event instead
  of anchoring emptyUsage() zeros, which zeroed the context size and
  silenced compaction for providers without usage reporting
- return the strategy-resolved size (not measured) from rpc getContext
  so the tokenCount contract stays correct under the estimated strategy
- migrate all consumers (contextMemory, fullCompaction, llmRequester,
  rpc, mirrorAgentRun, sessionLegacy, kap-server legacyStatus, node-sdk,
  kimi-inspect) to the new service; edge bridges no longer read the wire
  model directly
- document [token_counting] and KIMI_TOKEN_COUNTING_STRATEGY in the
  bilingual config reference

* fix(kap-server): omit maxContextTokens instead of pushing 0 when unknown

- readLegacyStatus falls back to the default model's context limit when no
  model is bound, and omits maxContextTokens entirely when the limit is
  unknown (0 is the engine's UNKNOWN_CAPABILITY marker, not a real limit)
- profileService no longer emits maxContextTokens in agent.status.updated
  when the bound model alias does not resolve

* fix(agent-core-v2): resolve token_counting strategy only at the reporting edge

- keep measured anchors and heuristic estimates both recorded and feeding
  internal logic (compaction triggers, budgets, overflow backoff) regardless
  of the configured strategy
- add IAgentTokenCountingService.statusSize() as the single strategy-resolved
  outward reading and route the WS/REST/RPC status surfaces through it
- fix the context-size display falling back to provider-reported usage under
  the estimated strategy
- fix compaction overflow backoff retrying identical messages until failure
  under the measured strategy (the strategy-gated estimator read as 0)
2026-08-04 09:44:21 +08:00
Haozhe
6ba75a173b
feat(config): add deprecation mechanism and rename loop retry limit (#2572)
* feat(config): add deprecation mechanism and rename loop retry limit

- agent-core-v2 config: declarative section `deprecations` (deprecated TOML
  keys are ignored and report a warning diagnostic; the file is never
  rewritten) and env binding `deprecatedEnv` (old var still resolves as a
  fallback with a warning), surfaced via the new
  `IConfigService.onDidChangeDiagnostics` event
- loop_control: rename `max_retries_per_step` to `max_attempts_per_step` and
  `KIMI_LOOP_MAX_RETRIES_PER_STEP` to `KIMI_LOOP_MAX_ATTEMPTS_PER_STEP`;
  `max_steps_per_run` moves onto the same mechanism (no longer silently
  mapped)
- kap-server: push the global `event.config.warning` WS event to every
  connection whenever the config warning set changes
- TUI: show config diagnostics in warning yellow at startup instead of the
  dim startup notice
- docs: config-files/env-vars (en+zh), regenerated config manifest, and the
  agent-core-dev config guide

* feat(cli): validate config.toml against v2 section registry in doctor

- add v2/validate-config.ts: validate config.toml with the agent-core-v2
  ConfigRegistry, reporting registered-section schema failures as errors
  and unknown top-level keys / deprecated keys and env vars as non-fatal
  warnings
- route `kimi doctor` config validation through the v2 validator when the
  KIMI_CODE_EXPERIMENTAL_FLAG master switch is on (lazy dynamic import,
  keeping the v2 module graph off the default path)
- let doctor checks surface non-fatal warning messages on OK results

* chore: downgrade loop-control changeset to patch
2026-08-03 20:17:01 +08:00
Haozhe
071b6a50d9
refactor(kap-server): own v1 message history and snapshot assembly (#2562)
- move the v1 message protocol and projection out of the engine into
  kap-server and delete the engine-side messageLegacy edge adapter
- add a shared message history loader that folds the main agent's wire
  journal into full history across compactions, backing both the
  messages routes and the snapshot endpoint
- drop the disk-reading SnapshotReader fast path; assemble snapshots
  from engine services for cold and live sessions, removing the
  KIMI_SNAPSHOT_READER, KIMI_SNAPSHOT_TIMEOUT_MS and
  KIMI_SNAPSHOT_CACHE_LIMIT knobs
- collect persisted wire record rebuild helpers in the transcript
  service
2026-08-03 17:55:28 +08:00
Haozhe
3e425212b6
refactor(agent-core-v2): code all domain failure modes as Error2 (#2552)
* refactor(agent-core-v2): code all domain failure modes as Error2

- wrap bare throws across agent/session/app/workspace/os/wire/kosong/mcpCore domains in coded Error2, keeping messages verbatim and moving structured data into details with the original error as cause
- add new wire codes (agent.already_exists/already_running/not_a_subagent/not_owned/type_not_allowed/max_tokens_exceeded, task.limit_exceeded, cron.expression_invalid, web.invalid_url/private_address/fetch_failed, mcp.oauth_failed, skill.parse_failed/nested_too_deep, wire.migration_missing) to the protocol KimiErrorCode union and the kap-server zod schema; register shell.git_bash_not_found and session.plan_mode_invalid
- re-base domain error classes onto Error2 (SkillParseError, UnsupportedSkillTypeError, HostFolder*, AgentFileParseError, NestedSkillTooDeepError, AlreadyAuthorizedError, HttpFetchError) keeping class names and instanceof consumers intact
- convert caller-bug and unreachable guards outside _base to BugIndicatingError
- fix the agent tool's task-limit remap never firing by branching on the task.limit_exceeded code instead of a stale message string

* refactor(kosong): make ChatProviderError family born-coded via Error2

- move the provider/context code string constants to kosong/contract/errors.ts
  and compute each class's wire code at construction (status code / finish reason)
- move sanitizeStatusErrorMessage to the contract and fold status details
  (statusCode / requestId / traceId) into Error2 details at birth
- slim translateProviderError down to the abort guard plus the foreign-error
  fallback; ProtocolErrors keeps registering the domain via re-exported constants
- update errors.md conventions and tests for the pass-through behavior

* feat(storage): add permission_denied and disk_full error codes

- extend StorageErrors with storage.permission_denied / storage.disk_full
  (both non-retryable, with user-facing actions)
- map errno at the backend boundary in toStorageIoError: EACCES/EPERM,
  ENOSPC, unexpected ENOENT → not_found, everything else io_failed; the
  message now carries the mapped reason
- register the two codes in the KimiErrorCode protocol union and the
  kap-server zod schema, and document the mapping in errors.md

* fix(protocol): mirror all KimiErrorCode values in kimiErrorCodeSchema

the zod enum lagged the type union by 35 codes (agent.*, os.fs.*,
os.process.*, storage.*, wire.*, skill/task/mcp/cron/web additions), so
protocol consumers could type the new codes but rejected them at runtime
validation; spotted by Codex review on #2552
2026-08-03 15:31:57 +08:00