Compare commits

..

697 commits

Author SHA1 Message Date
Luyu Cheng
d96cd03770
fix(cli): pre-send warning and clearer error for over-long /goal objectives (#2928)
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-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
CI / build (push) Waiting to run
CI / test (1) (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(cli): warn on over-long /goal objectives before sending and keep the input

- Show a live footer warning in the TUI while a typed /goal objective
  exceeds the 4000-character limit, measuring paste-expanded text only
  when the input can be a /goal command.
- Restore the rejected /goal input into the editor instead of losing it.
- Include the file-reference workaround in the GOAL_OBJECTIVE_TOO_LONG
  error messages (TUI, goal queue, agent-core, agent-core-v2).

* fix(cli): keep the /goal length warning in its own footer slot

A transient hint (exit confirm, detach, image paste) that displaced the
warning previously left the footer blank after clearing, because no
editor change re-applied it. The footer now renders the warning from a
dedicated slot whenever no transient hint is active, so the warning
returns on its own.

* fix(cli): gate the /goal length warning on trimmed text

Submitted text is trimmed before slash-command dispatch, so leading
whitespace still runs /goal — normalize with trimStart in the gate and
in the length check to match.

* fix(cli): restore input rejected by the slash-command busy gate

An idle-only command submitted while streaming/compacting was rejected
after the editor buffer had already been cleared, losing hand-typed
input (e.g. an over-long /goal objective that never reached the local
validation).

* fix(cli): restore input at the post-creation busy re-check

The lazy-session race rejects an idle-only command after a first prompt
has already started a turn; the editor buffer is long cleared by then,
so give the submitted input back like the dispatch blocked branch does.

* fix(cli): close the remaining input-loss and gate gaps around /goal

- Restore the submitted input when lazy session creation fails before a
  session-requiring command runs.
- Restore only into a still-empty editor after async gates, so a draft
  typed while creation was pending is never overwritten.
- Expand pastes that can complete a partially typed /goal command
  (e.g. /go[paste #1 …]) in the length-warning gate.

* fix(cli): never displace newer UI state with a delayed input restore

A session-less /goal submission restores its input only after an async
gap (lazy session creation). If the user opened an editor-replacement
panel meanwhile, restoring would tear it down (and leave activeDialog
inconsistent). Track editorReplacementMounted in TUIState and gate all
delayed restores through canRestoreSubmittedInput.

* refactor(cli): move canRestoreSubmittedInput into commands/resolve

Avoids the goal.ts <-> dispatch.ts runtime import cycle flagged by
import/no-cycle; the helper takes a structural host shape instead.

* fix(cli): match the slash parser's delimiter in the /goal length warning

parseSlashInput splits the command name at a literal space only, so a
newline or tab after /goal dispatches as a plain message — the warning
must not fire for inputs the dispatcher will not treat as a goal.
2026-08-15 02:55:33 +08:00
qer
a23680e293
docs(changelog): sync 0.36.1 from apps/kimi-code/CHANGELOG.md (#2926) 2026-08-14 21:56:20 +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
d60d8ff7e8
chore: downgrade plan viewer changeset from minor to patch (#2923) 2026-08-14 20:46:50 +08:00
qer
cd489955bb
chore: sync web dist from code-app (#2922)
code-app: af7ed8fa03278bbe2f988732b3195649e7a6f2bc
2026-08-14 20:36:54 +08:00
qer
6cf315b7bd
fix(pi-tui): stop GFM autolinks at CJK punctuation boundaries (#2917)
* fix(pi-tui): stop GFM autolinks at CJK punctuation boundaries

marked's GFM autolink accepts any non-space characters after the domain
and its backpedal strips only ASCII trailing punctuation, so CJK or
full-width punctuation right after a bare URL was absorbed into the link
text and href (`.../pull/232(本地` rendered as one anchor whose OSC 8
target contained raw CJK and opened a broken address).

Register a CjkBoundaryUrlTokenizer (subclass of the upstream
StrictStrikethroughTokenizer, which stays byte-identical for
re-vendoring) that cuts the autolink match at the first CJK punctuation
character before the ASCII backpedal. CJK ideographs inside the URL path
itself are preserved. Guarded by new bare-URL CJK cases in
test/markdown.test.ts and listed in pi-tui's local-divergence inventory.

* fix(pi-tui): keep balanced full-width parens inside autolinked URLs

Address review feedback on the CJK autolink boundary: cutting at the
first full-width parenthesis anywhere in the match also truncated URLs
that legitimately contain balanced full-width parens in their path
(e.g. wiki disambiguation pages like .../wiki/中华人民共和国(1949年)).

Full-width parens now follow GFM's ASCII-paren rule: a paren-depth scan
keeps balanced pairs in the URL and only an unbalanced ( or )
terminates the match. Non-paren CJK punctuation still always terminates
it.

* fix(pi-tui): keep CJK punctuation inside balanced full-width parens

Punctuation inside a balanced full-width parenthetical is deliberate URL
content (e.g. .../wiki/中华人民共和国(北京,1949年)), so the non-paren
CJK terminator now only applies at paren depth 0. Prose parentheticals
contain spaces and never survive marked's match this far, and an
unbalanced ( still cuts the match at the open paren.
2026-08-14 20:15:33 +08:00
Grapedge
7475c2e2e3
feat(vscode): switch the extension to the v2 engine with a rollback switch (#2916)
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 / Publish native release assets (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
The extension now runs on the agent-core-v2 engine by default. The
interface, sessions, and workflows do not change. Two rollback paths
exist, and one function makes the decision
(config/vscode-settings.ts):

- the kimi.useAgentCoreV1 setting (temporary; a window reload applies
  the change);
- the KIMI_CODE_LEGACY_FLAG environment variable, which wins over the
  setting and has the same semantics as in the CLI.

An engine startup failure shows an explicit error that names the
rollback setting. There is no silent fallback. CI runs the extension
test suite on both engines: the sharded run covers the default v2
engine, and a new test-vscode-legacy job reruns the suite with
KIMI_CODE_LEGACY_FLAG=1.

To keep the v2 path identical to v1 for every method the extension
uses, this change also completes the v2-backed SDK client and the v2
engine:

- Implement session deletion in the v2 SDK client.
- Implement fork truncation at a turn index in the v2 engine, with the
  same rules as v1, and reject a fork while the source session has an
  active turn.
- Stop the session-level /init run when the turn is cancelled, as v1
  does.
- Read session metadata without the archived field as not-archived, so
  sessions written by the v1 engine open correctly.

The SDK parity suite now covers session deletion, cancel, and fork
truncation. The known-difference list for the methods the extension
uses is empty.
2026-08-14 17:22:10 +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
Louis Gu
245e3d56a6
fix(tui): sanitize background task output (#2863)
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-08-14 11:19:27 +08:00
oocz
102984aa66
fix: settle cancelled MCP OAuth callbacks (#2899)
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-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 / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Co-authored-by: yuchengzhen <yuchengzhen@moonshot.cn>
2026-08-13 22:56:06 +08:00
Haozhe
1414d46028
refactor(agent-core-v2): fold session lifecycle hooks into sessionLifecycle events (#2896) 2026-08-13 21:03:51 +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
7Sageer
0473b3aac1
test(minidb): deflake the evict-lru compaction-churn guard (#2892)
The stress test asserted stats.compactions > 0 immediately after the
write loop, but auto-compaction is fire-and-forget through the
maintenance scheduler and the counter only increments once a run fully
completes. On a loaded CI runner the loop can finish before the first
compaction lands, failing the guard even though nothing is broken.

Wait for the first completed compaction with the existing waitFor
helper instead: the guard still proves the churn this scenario
requires happened, and a genuinely broken auto-trigger now fails via
the wait timeout.
2026-08-13 21:00:56 +08:00
Haozhe
67e73f3e76
refactor(agent-core-v2): extract date change feature (#2895)
Move date-change reminders behind the Feature lifecycle and make state registrations disposable so unloading removes the runtime seed.
2026-08-13 20:58:58 +08:00
7Sageer
c60e3e301d
docs: drop legacy secondary-model content and unify subagent terminology (#2891)
- Remove the legacy-engine secondary-model recipe section, the
  KIMI_SECONDARY_MODEL / KIMI_SECONDARY_EFFORT env entries, and the
  model_preference agent-file field: the default engine never reads
  them and the legacy engine is deprecated.
- Remove the backward-compat note for a lone [secondary_model] model
  key; the code still reads it, but the docs now only document the
  current pool scheme.
- Reframe the secondary_model section around the subagent model pool
  instead of a singular secondary model.
- Unify zh terminology: 子 Agent -> subagent, 主 Agent -> main agent,
  covering prose, headings, anchors, the sidebar label, and the
  docs/AGENTS.md term table.
2026-08-13 19:57:46 +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
liruifengv
4a93f70aa2
feat(oauth): add browser-safe ./device subpath export (#2885)
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 / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (1) (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
* feat(oauth): add browser-safe ./device subpath export

* fix(oauth): guard env override lookup for browser consumers

* chore(oauth): add changeset for ./device subpath export

* fix(oauth): resolve env overrides via globalThis for DOM-only consumers
2026-08-13 18:26:10 +08:00
liruifengv
1811bd4baf
fix(tui): keep banner main text readable with long tags on narrow terminals (#2884)
* fix(tui): keep banner main text readable with long tags on narrow terminals

The banner layout inlines the tag and wraps the main text into the
remaining width. Remote banner configs can set a full-sentence tag
(e.g. the 38-char K3 thinking-effort banner), which on narrow terminals
leaves the main text only a few columns, so it wraps into a ragged,
hard-broken column ("balan/ce", "capab/ility").

When the inline tag would leave the main text fewer than 16 columns,
render the tag on its own line and give the main text and subtext the
full width, aligned with the tag text. Short tags stay inline; tags
wider than the terminal are still dropped as before.

* chore: add changeset for banner narrow-terminal fix

---------

Co-authored-by: Mira <mira-bot@moonshot.cn>
2026-08-13 18:12:46 +08:00
Haozhe
4739284fb9
refactor(features): extract session init feature (#2887)
- move the session init domain under features
- contribute the session service through SessionInitFeature
- cover feature withdrawal and restoration
2026-08-13 18:12:08 +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
7Sageer
5912d4c7d1
fix: stop repeated file-watcher errors on Windows drive-root and UNC workspaces (#2876) 2026-08-13 14:35:23 +08:00
liruifengv
f8a88c1bd9
docs(changelog): sync 0.36.0 from apps/kimi-code/CHANGELOG.md (#2880)
Also translate the Chinese pool-example descriptions in
docs/en/configuration/config-files.md into English.
2026-08-13 14:30:40 +08:00
github-actions[bot]
b6144f94ea
ci: release packages (#2846)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-13 13:47:30 +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
6e31722df1
chore: drop deprecations for unreleased [subagent] pool keys (#2877)
The [subagent] default_model / models deprecations added in #2700 guard a
migration path that has no users: the pool keys only existed in #2700's own
intermediate commits and never shipped in any release, so no config written
against a released version can contain them. Remove the two deprecation
entries (the mechanism stays — the released loop_control renames still use
it), the migration notes in the en/zh config docs, the agent-core-dev skill
note, and the obsolete test; regenerate the config manifest.

No changeset: #2700 is still unreleased, so no published version ever
emitted these warnings — the removal is invisible to users.
2026-08-13 12:57:22 +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
Liu Zhongnuo
504e6292ed
feat(mcp): inspect effective authorization state in v1 (#2856)
* feat(mcp): inspect effective authorization state

* test(agent-core-v2): register MCP auth coordinator fixture

* fix(mcp): validate runtime names against full catalog

* fix(mcp): reconnect after pending auth updates

* docs(mcp): describe auth coordinator collaborator

* fix(mcp): ignore disabled runtime name collisions

* fix(mcp): serialize OAuth token refresh

* test(mcp): await OAuth credential writes

* fix(mcp): queue trailing credential reconnect

* fix(oauth): preserve access-only refresh winners

* fix(mcp): preserve legacy offline auth state

* fix(mcp): redact inspection credentials

* refactor(mcp): keep app inspection on v1

* fix(mcp): guard legacy auth status mutations

* fix(mcp): avoid deterministic legacy auth probes

* fix(mcp): cover initialization credential updates

---------

Co-authored-by: 刘仲诺 <liuzhongnuo@dev.msh.team>
2026-08-13 12:00:04 +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
qer
719da94648
chore: rebuild web dist against kimi-code 0.35.0 (#2853)
Some checks are pending
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 / 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 / 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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
code-app: c5440b863d02f99457f48520c16c609eae73f12b
2026-08-13 01:27:30 +08:00
7Sageer
fe3cdae5f8
fix(agent-core-v2): drop interrupted thinking-only assistant messages at step settle (#2819)
* fix(agent-core-v2): drop interrupted thinking-only assistant messages at settle

A turn interrupted while the model is still streaming thinking leaves the
open assistant holding only an unsigned thinking fragment. The fold used
to seal it into history because a non-empty thinking block is not vacuous;
on OpenAI-compatible providers the serialized message then carries neither
content nor tool_calls, and strict gateways reject every later request
with a 400 (#1404). Treat unsigned-thinking-only content as unsendable at
settle so the fold drops the message instead — replaying the records of
an already bricked session repairs it.

* fix(agent-core-v2): preserve reasoning-only assistant history

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-08-12 22:17:02 +08:00
liruifengv
30f56a2d2d
fix(agent-core-v2): disable SDK-internal retries that blocked cancellation (#2855)
* fix(agent-core-v2): disable SDK-internal retries that blocked cancellation

The OpenAI and Anthropic SDK clients default to maxRetries=2 with a
backoff sleep that never observes the request AbortSignal, so Ctrl+C
during a 429/5xx/connection-error retry only took effect after the
sleep elapsed, and the hidden attempts were invisible to the engine
(no turn.step.retrying) while double-counting its retry budget.

Build those clients with maxRetries: 0 so retryable failures surface
to the engine's step-retry layer immediately (observable countdown,
abortable sleep, single retry budget). The Google GenAI main request
path only retries when httpOptions.retryOptions is explicitly set, so
there is nothing to disable; instead its error converter now recovers
the server-directed delay from the wire body's google.rpc.RetryInfo
detail, since the SDK's ApiError drops the Retry-After header.

* chore: simplify the retry-cancellation changeset entry

* fix(agent-core-v2): recover GenAI retry delay from prefixed mid-stream error chunks

Mid-stream error chunks throw ApiError with the message wrapped as
"got status: <STATUS>. {json}", so a strict JSON.parse of the whole
message missed the google.rpc.RetryInfo detail. Locate the JSON object
start before parsing; the non-stream path (pure JSON body) is
unaffected.
2026-08-12 20:40:20 +08:00
liruifengv
ec84a6f9a3
feat(kimi-code): re-baseline pi-tui on upstream v0.84.1 and add fullscreen tui_mode (#2830)
* feat(kimi-code): re-baseline pi-tui on upstream v0.84.1 and add fullscreen tui_mode

Re-baseline the vendored pi-tui fork on upstream @earendil-works/pi-tui
v0.84.1, keeping all local patches: narrow-terminal hardening,
processed-line render caching (re-implemented into TuiMainScreen), editor
history hooks, the paste-burst fallback, and multi-root @ completion.

Upstream highlights absorbed: the renderer splits into TuiMainScreen and
TuiAltScreen behind a TUI interface, the Markdown component gains opt-out
LaTeX rendering (disabled on the kimi-code side), paste-registry repair
on delete/undo, Windows input-latency and Shift+Enter fixes, and Kitty
image layout fixes. Editor.setText gains a preservePasteRegistry option
so paste-marker expansion survives wholesale text replacement.

New tui_mode = "fullscreen" preference mounts TuiAltScreen: the
transcript lives in a primary ScrollView with follow-end, the chrome
docks at the bottom, mouse selection and scrollbar come from the
renderer, and full-screen viewers (tasks browser, output viewer, approval
preview) swap the layout root via screen-takeover. Viewport navigation
keys fall through to the focused component when the primary scroll view
cannot scroll.

* feat(pi-tui): merge upstream main through 40a3d85 (post-0.84.1)

Bring in upstream's merged-but-unreleased changes on top of the v0.84.1
re-baseline:

- Fullscreen transcript search (ctrl+shift+f, next/previous navigation)
- Alternate-screen render-churn reduction (9-18x less per-frame
  allocation by painting full-width rows as direct line references)
- Unbound single-line scroll actions (tui.altScreen.lineUp/lineDown),
  wired into the fork's canScroll gating like the other viewport keys
- SSH-aware escape-timeout default and PI_TUI_ESC_TIMEOUT override
- Search snapping and SGR-mouse fragmentation fixes; LaTeX newline
  argument fix

Conflicts resolved by union: upstream's search/line scroll bindings stay
ungated, fork's primaryScrollable guard applies to all scroll actions.

* fix(kimi-code): keep fullscreen dock from crushing the editor box

The fullscreen layout gave the transcript ScrollView its intrinsic
content height as basis and let the dock participate in shrink
distribution with no minSize. Once the transcript exceeded the screen,
the VStack shrink pass crushed the dock to a couple of rows, and the
editor (3 rows: top border / input / bottom border) lost its bottom
border row to clipping.

Adopt pi's sizing contract: the ScrollView starts from basis 0 and
grows, the dock keeps its intrinsic height, the editor never shrinks
below 3 rows, and the footer below 1. Adds a VirtualTerminal-level
regression test that replays a full streaming cycle in fullscreen.

* docs(kimi-code): document the tui_mode preference in tui.toml

* fix(pi-tui): let terminal focus reports fan out in fullscreen

TuiAltScreen's viewport input listener consumed FOCUS_IN/FOCUS_OUT
reports. Since the renderer installs that listener at construction —
before any app-level listeners — terminal focus tracking and
clipboard-image hints never saw focus transitions in fullscreen mode
(notification_condition = "unfocused" went blind, refocus clipboard
hints stopped). Keep the selection cleanup but stop consuming, matching
the main-screen fan-out. Addresses Codex review on PR #2830.

* fix(kimi-code): wire openUrl and right-click paste in fullscreen

Mouse capture in the alternate screen intercepts the terminal's native
link activation, leaving OSC 8 hyperlinks (like the footer's PR link)
unclickable in fullscreen. Route renderer link clicks to the app's
openUrl, and on Windows feed right-clicks to the focused component as a
bracketed paste read from the clipboard.

* feat(kimi-code): fullscreen prompt navigation, exit replay, progress resync

- Mark user/assistant transcript messages with OSC 133 zones (start /
  end / final) so the fullscreen renderer's Ctrl-Shift-Up/Down prompt
  jumps work; GutterContainer keeps the markers at byte 0 when prefixing
  its gutter, and message render caches store already-marked lines.
- On exit from fullscreen, preserve the frame and replay the transcript
  through a fresh main-screen renderer so native scrollback gets the
  regular inline layout (pi's "transcript" exit form).
- Re-sync the OSC 9;4 progress indicator after a stop/start cycle:
  terminal.stop() clears it, and the cached progressActive flag used to
  suppress the re-send when returning from the external editor mid-turn.

* feat(kimi-code): enable Markdown LaTeX rendering with a render_latex opt-out

Align with the upstream pi-tui default: LaTeX math in Markdown messages
renders as Unicode text. The explicit renderLatex:false we set during
the re-baseline becomes a shared Markdown options helper fed by a new
tui.toml preference (render_latex, default true), wired at startup and
refreshed on /reload.

* refactor(kimi-code): gate fullscreen behind KIMI_CODE_TUI_FULL_SCREEN

Drop the public tui_mode preference from tui.toml before release; the
fullscreen UI is experimental, so enable it with the
KIMI_CODE_TUI_FULL_SCREEN=1 env var instead. Docs move from the
config-file reference to the env-vars page.

* chore(changesets): clarify fullscreen mode and LaTeX formula entries

* chore(changesets): trim fullscreen mode entry

* chore(changesets): trim LaTeX formula entry

* chore(changesets): drop redundant kimi-code entries

* test(kimi-code): add stepRetry to fullscreen layout fixture after main merge

* fix(kimi-code): apply render_latex before theme-driven Markdown rebuilds

Codex review on PR #2830: applyReloadedTuiConfig set the shared LaTeX
toggle after applyTheme(), but theme application invalidates transcript
components and their rebuilt Markdown children copy the options at
construction — so a /reload that only flipped render_latex kept the old
value until some later invalidation. Move the setter before applyTheme
and pin the ordering with a test.

* fix(kimi-code): carry renderLatex through TUI config saves

Codex review on PR #2830: currentTuiConfig omitted renderLatex, so
saving an unrelated preference (theme/editor/upgrade/cache-hint)
serialized render_latex as the default true and silently reset a user's
opt-out. Carry the appState value through the shared save payload.

* feat(kimi-code): report tui_mode in lifecycle telemetry

Tag startup_perf and exit events with the active renderer mode
(regular/fullscreen) so fullscreen adoption is measurable while it is
gated behind KIMI_CODE_TUI_FULL_SCREEN.
2026-08-12 18:23:41 +08:00
liruifengv
35d9a36a69
chore(changelog): thank security reporters in 0.35.0 entry (#2845)
Some checks are pending
CI / lint (push) Waiting to run
CI / test-windows (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 / 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-12 16:27:19 +08:00
Haozhe
3b0936d8e0
fix(agent-core): only discover the root SKILL.md for plugin skills fallback (#2847)
When a plugin manifest omits `skills` and the plugin root contains a
SKILL.md, the fallback treated the whole plugin root as a generic skill
scan directory, so sibling Markdown files such as CHANGELOG.md were
misidentified as skills and inflated the plugin skill count.

Mark the fallback root as root-skill-only so discovery parses only the
root SKILL.md; explicit `skills` entries (including "./") keep the
directory scan semantics. Applied to both agent-core and agent-core-v2.
2026-08-12 14:24:48 +08:00
7Sageer
f40bf04998
refactor(agent-core-v2): unify model-facing reminder scheduling (#2623)
* refactor(agent-core-v2): unify model-facing reminder scheduling

Route every model-facing reminder through the contextInjector boundary
scheduler. Past-tense events go through a persisted once-reminder queue
(reminderQueue) that delivers exactly once at turn, step, compaction,
and restore boundaries; present-tense state renders through
context-injection providers reconciled against live history.

- interruption, goal (cancel/budget/fork-cleared), image-compression
  captions, btw, and init reminders enqueue into reminderQueue instead
  of writing the context directly; the interruptionReminder wire model
  is removed and its recorded type is retired silently on replay
- swarm mode announcements render through a provider seeded from the
  replayed history on restore, replacing live side effects and the
  ContextModel pop reducer on swarm_mode.exit
- loadable-tools announcements become an isNewTurn-gated provider,
  dropping the compaction boundary flag
- plugin session-start guidance re-renders as a supersedes reminder at
  the next boundary via a dirty flag instead of appending immediately
- legacy system_trigger origins of migrated reminders still fold on
  replay

* fix(agent-core-v2): make system reminders undo-aware

* test(agent-core-v2): migrate plugin session-start harness

* fix(agent-core-v2): preserve reminder boundary ordering

* refactor(agent-core-v2): narrow reminder and swarm helper exposure

- drop the swarmInjection re-export from the package index; SwarmInjection
  stays a domain-internal collaborator like permissionMode/plan injections
- move INTERRUPTION_REMINDER text back to a private constant in the service;
  only the variant stays in the Ops module
- make reminderQueue.enqueue return void; no caller consumed the entry id

* chore(agent-core-v2): keep comments in module headers

* refactor(agent-core-v2): track reminder state via injection disclosure

- derive swarm active/inactive state from ctx.lastDisclosure instead of
  byte-matching rendered markdown, with variant-only fallback for legacy
  swarm_mode/swarm_mode_exit journal entries
- record once_reminder disclosure (entry id) on queue-appended messages
  and dedupe the crash window by the contiguous tail id set, covering
  multi-entry drains
- move reminderQueue draining behind a sync onWillInject event so the
  injector no longer depends on the queue domain
- centralize the system-reminder wrap format behind wrapSystemReminder /
  systemReminderContent and use injector-provided positions in the plugin
  session-start provider
- spell out the step-boundary fallback and sync-only contract of
  registerAtTurnStart via shouldRunAtBoundary

* fix(agent-core-v2): isolate failing turn-start providers and warn once per missing sessionStart skill

* refactor(agent-core-v2): compute injection positions on read

Drop the per-provider positions cache from the context injector: the
registration scan, the context.spliced index arithmetic, and the
post-restore resync all existed only to mirror what the history already
records. Each provider call now derives its injected positions by
scanning context memory for its surviving injection messages, so silent
history edits (such as vacuous-step folds) can no longer desync a
cached index.

* refactor(agent-core-v2): formalize injector once-channels and raw message results

* refactor(agent-core-v2): declare dynamic tool schemas at injection boundaries

Move the dynamic-tool schema declaration out of toolSelect.load(): the
loaded names are recorded as pending and drained by a dedicated
toolSelectSchemas provider through the contextInjector boundary
scheduler, so the declaration message lands at a quiescent boundary
instead of mid-step inside a streaming tool exchange. The folded
history remains the loaded-tool ledger, so undo, compaction, and
resume still self-heal by re-folding.

* refactor(agent-core-v2): deliver AGENTS.md reminders through the reminder queue

The tool hook now only observes and enqueues a once-per-agent reminder
through the reminderQueue once-channel instead of prepending text to
the tool result: results stay verbatim for the truncation pipeline and
the reminder can never be truncated away with an oversized output. The
reminderQueue is resolved lazily through the instantiation service at
enqueue time, breaking the contextInjector -> loop -> llmRequester ->
profile -> agentsMdReminder constructor cycle.

* refactor(agent-core-v2): make injection disclosures opaque and domain-owned

contextMemory no longer declares the ContextInjectionDisclosure union:
InjectionOrigin.disclosure becomes an opaque unknown, and providers
bind their own payload type through register<D>, so lastDisclosure
arrives at the provider already typed by its own variant. The date,
swarm_mode, and once_reminder payload shapes move into the dateChange,
swarm, and reminderQueue domains respectively; reminderQueue keeps a
runtime guard for its cross-message tail scan, the only place that
reads disclosures it did not write. Persisted origin shapes are
byte-identical, so existing journals replay unchanged.

* fix(agent-core-v2): isolate failing step context providers

A step or compaction boundary provider that threw or rejected made the
injector's inject() promise reject, which propagated through the
onWillBeginStep hook chain and failed the whole turn, and starved every
provider registered after it. Log and skip the bad provider instead,
matching the turn-start path's existing isolation.

* refactor(agent-core-v2): derive injector isNewTurn per injection boundary

Replace the shared read-and-clear isNewTurn flag (set by turn.started and
injectAfterCompaction, consumed by the first inject()) with values each
trigger supplies from an authoritative source: the loop marks a turn's
first step via BeforeStepContext.firstStepOfTurn (standalone runs never
count), and the compaction follow-up passes true explicitly, so
interleaved triggers can no longer consume or steal the marker.

A compaction follow-up that lands inside a step hook chain (the
auto-compaction path) doubles as that step's new-turn delivery: the
enclosing step then injects with isNewTurn false, so the upcoming request
receives one new-turn injection, not two.

* refactor(agent-core-v2): unify disclosure placement and injector param naming

* fix(agent-core-v2): keep pending tool schemas across compaction splices

A load announced by select_tools sits in pendingLoaded until the next
injection boundary declares it. A compaction fold in that window
publishes a replacement splice, and the splice-time reconciliation
dropped the pending entries before the post-compaction inject could
declare them — the model was told "Loaded: X" yet X never became
available. Drop pending entries only on removal splices (undo/clear,
which carry no replacement messages); compaction's replacement splice
keeps them so the declaration lands at the post-compaction boundary.

* fix(agent-core-v2): consume the plugin session-start refresh after a successful render

reconcileSessionStartReminder cleared the refresh-pending flag before
awaiting the render, so a throwing render (skipped by the injector's
provider isolation) lost the forced refresh until the next catalog
change. Consume the flag only after the render resolves, and move the
warn-once rationale into the module header per the comment convention.

* refactor(agent-core-v2): remove the generic reminder queue

* chore(agent-core-v2): drop the stale reminder-queue mention in systemReminder

* test(node-sdk): align side-question fork parity with event-point reminders

* chore(agent-core-v2): address reminder review standards

* docs(agent-core-v2): condense the model-facing reminders section

* refactor(agent-core-v2): write all system reminders through wrapSystemReminder

* fix(agent-core-v2): preserve reminder lifecycle invariants

* refactor(agent-core-v2): reconcile context injections at the step head

Unify the injector's delivery timings into one point on the
onWillBeginStep chain, before the step's request is built:

- providers run before every request instead of after every step, so
  reminders are visible from the first response of a turn
- a compaction splice re-arms the new-turn flag via context.spliced;
  when compaction runs inside the hook chain (full-compaction's
  beforeStep), a follow-up inject at the chain tail keeps the first
  post-compaction request covered
- registerAtTurnStart and injectAfterCompaction are removed;
  reconcileWhenIdle stays as the v1-parity surface for SDK-driven
  triggers (swarm toggle, plugin reload)

* refactor(agent-core-v2): clarify the injector's step-hook handler

Name the handler reconcileAroundStep, rename the rearm flag to
compactionRearmPending with a single takeCompactionRearm() consumer,
and extract isCompactionSplice. Consuming the flag into a local before
computing isNewTurn also avoids hiding the side effect inside a ||
short-circuit.
2026-08-12 13:52:08 +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
c212ae9715
fix(kimi-code): show MCP launch targets in the workspace trust prompt (#2843)
* fix(kimi-code): show MCP launch targets in the workspace trust prompt

Render each gated project MCP server's launch target (transport, command,
args, cwd, or url) in the workspace trust prompt without leaking env or
header secrets, stripping terminal control characters from the
workspace-supplied text, default the prompt to "Don't trust", and
resolve fd binaries to absolute paths so untrusted workspaces cannot
plant a bare-name fd executable that runs before trust confirmation.

* fix(kimi-code): resolve stty to an absolute path before the trust gate
2026-08-12 13:34:32 +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
Haozhe
dc8db90cdd
docs(server): add local server guide and API reference (#2839)
* docs(server): add local server guide and API reference

* docs(server): qualify binary endpoint HTTP semantics
2026-08-12 12:29:09 +08:00
qer
26ddc1d0fb
feat(plugins): add Modern Web Guidance to marketplace (#2842)
* feat(plugins): add Modern Web Guidance to marketplace

* chore: drop changeset

* chore: backfill 0.35.0 changelog entry
2026-08-12 12:28:36 +08:00
liruifengv
3fc841ff23
docs(changelog): sync 0.35.0 from apps/kimi-code/CHANGELOG.md (#2841)
* docs(changelog): sync 0.35.0 from apps/kimi-code/CHANGELOG.md

* docs(changelog): add Modern Web Guidance plugin entry to 0.35.0
2026-08-12 12:15:07 +08:00
github-actions[bot]
f6ee44e426
ci: release packages (#2710)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-12 11:45:35 +08:00
Haozhe
101c4d1997
feat(agent-core-v2): remove Agent and AgentSwarm from builtin profile tool lists (#2837)
* feat(agent-core-v2): remove Agent and AgentSwarm from builtin profile tool lists

The builtin agent and coder profiles no longer expose the Agent and
AgentSwarm tools, so sessions on the v2 engine do not offer subagent
delegation by default. The tools themselves remain registered; profiles
that list them explicitly can still opt in.

* feat(agent-core): remove Agent and AgentSwarm from builtin profile tool lists

Align the v1 builtin agent/coder profiles with the v2 change: the
default profiles no longer offer subagent delegation, while the tools
stay registered for profiles that list them explicitly.

The parity projection drops v1's inactive Agent/AgentSwarm roster
entries: v1 reports registered-but-inactive builtin tools where v2 only
registers the tools a profile lists, so an inactive entry has no v2
counterpart. Active entries still compare in full.

* fix: keep Agent and AgentSwarm in the builtin agent profile

Scope the removal to the coder subagent profile on both engines: the
main agent keeps Agent/AgentSwarm so default sessions can still
delegate, while coder subagents no longer spawn nested subagents by
default. Snapshots and token counts shift only for the embedded coder
tool list; the v1 parity projection needs no change since the main
agent rosters match again.
2026-08-12 11:41:02 +08:00
qer
68ce3c7a0c
chore: sync web dist from code-app (#2840) 2026-08-12 11:38:17 +08:00
liruifengv
df8ce73e45
feat(kimi-code): show step retry progress in the activity indicator (#2825)
* feat(kimi-code): show step retry progress in the activity indicator

Wire the engine's turn.step.retrying event into the TUI: while a failed
model request is backing off for another attempt, the waiting spinner
shows 'retrying (N/M) · errorName · in Xs' with a dim detail line for
the status code and provider error message, and the loading tip is
suppressed.

The retry state clears on the step's terminal events (completed /
interrupted), turn.ended, and tool.result. It intentionally survives
turn.step.started because the v2 engine re-emits that event for every
retried attempt of the same step.

* fix(kimi-code): show the retry indicator for mid-stream failures

A retryable failure raised after thinking/assistant deltas had already
streamed left the pane in thinking/composing mode, so the retry label
and detail never rendered during the backoff. Drive the pane and the
streaming phase back to waiting when a retry begins.

* fix(kimi-code): drop the stale retry countdown once the attempt starts

The v2 engine re-emits turn.step.started when the retried attempt
begins running after the backoff sleep. Track a backoff/attempt phase
so the label keeps showing the retry attempt and error but drops the
already-elapsed 'in Xs' countdown, instead of either clearing the
state or showing stale timing through a slow attempt.

* fix(kimi-code): advance the retry phase on a timer instead of step starts

The legacy engine retries inside the same step and never re-emits
turn.step.started, so the backoff-to-attempt transition keyed on that
event never fired there and the stale countdown stayed up through the
attempt. Schedule the flip from delayMs instead, which matches when
both engines actually start the next attempt, and drop the step-start
hook.

* fix(kimi-code): cancel the retry phase timer on TUI shutdown

A pending backoff timer survived KimiTUI.stop(), keeping the event
loop alive and firing setAppState against a disposed UI when stop()
runs without an immediate process exit. Expose the timer cleanup and
invoke it from the shutdown path.

* fix(kimi-code): align the retry detail line with the spinner label

* fix(kimi-code): capitalize the retry spinner label
2026-08-12 11:35:44 +08:00
liruifengv
3c9e3b297c
feat(kimi-code): paginate the session picker list (#2826)
* feat(kimi-code): paginate the session picker list

The /sessions picker and kimi -r used to materialize the full session
list before showing anything, which gets slow with hundreds of sessions.

- node-sdk: add listSessionsPage (limit/before -> items + nextCursor);
  the v2 engine pages through the session index (draining past entries
  whose workDir is unrecoverable), the v1 engine answers one full page
- TUI: open the picker on the first page, fetch the next page when the
  cursor reaches the fetched end, and drain remaining pages in the
  background once a search query is typed so search still covers all
  sessions
- kimi -r now fetches a one-item page for the latest session

* chore: simplify session picker changeset

* fix(kimi-code): join in-flight page fetch in session search drain

A query typed while a scroll-triggered page fetch was still running
stopped the background drain at the loadingMore early return, leaving
the search covering only the pages fetched so far. fetchMoreSessions
now optionally joins the in-flight fetch and continues with the next
page; scroll triggers still drop when busy.
2026-08-12 11:21:43 +08:00
Haozhe
e5be39164b
fix(kimi-code): resolve footer git status commands through PATH (#2838)
The footer git status cache spawns git (and gh for PR lookup) on the
startup path, before the workspace trust prompt. On Windows, a bare
command name lets cmd.exe resolve a git.exe planted in the workspace
before the user confirms trust — a gap left by #2695.

Resolve git once at cache creation and gh per lookup with
resolveCommandPath(), which returns an absolute PATH hit and refuses
matches inside the workspace; when resolution fails the cache reports
no repository instead of spawning anything.
2026-08-12 10:51:57 +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
liruifengv
911d41b0f6
chore(changesets): simplify pending CLI changelog entries (#2823) 2026-08-11 21:37:00 +08:00
liruifengv
ad12ad8a14
feat(kimi-code): show live background agent activity in the /tasks panel (#2816)
* feat(kimi-code): show live background agent activity in the /tasks panel

Background agents (run_in_background or Ctrl+B) showed no run details:
the /tasks panel only had static metadata, and its output view stays
"[no output captured]" until completion because agent tasks capture
output only once at the end.

Tee child-agent events into a bounded in-memory per-agent activity
store segmented by the engine's own turn.step.started events (recent
10 steps, bounded text/output tails). The /tasks preview pane now
shows a live activity preview for agent tasks, and Enter/O opens a
full-screen detail view rendering step-grouped Markdown text and
per-tool results through the main transcript's renderers, with Ctrl+O
to expand. Agent tasks without an in-memory record (e.g. lost after
resume) fall back to the captured-output view.

* feat(kimi-code): retain 20 recent steps in the background agent activity view

* fix(kimi-code): cap the streaming-args buffer in the subagent activity store

* chore(kimi-code): simplify the background agent activity changeset

* fix(kimi-code): drop activity records of foreground-only subagents at terminal state

* fix(kimi-code): cap retained tool argument strings in the subagent activity store

* test(acp-server): retry temp-dir cleanup to deflake ENOTEMPTY on CI

* fix(kimi-code): tighten subagent activity store lifecycle edges

- drop delta-only arg buffers when their step is evicted
- keep records of spawn-time background agents even when the task sync lags
- mark records terminal on background.task.terminated for stopped agents
  that never emit subagent.failed

* fix(kimi-code): release leftover arg buffers when an activity record turns terminal

* fix(kimi-code): prune foreground-only activity records when the main turn ends
2026-08-11 21:32:39 +08:00
HydrogenE7
158c81d705
fix: surface a readable error when Git Bash is missing on Windows (#2814)
* fix: surface a readable error when Git Bash is missing on Windows

* fix(agent-core-v2): translate probe rejection into HostProcessError for ready awaiters

- HostEnvironmentService.ready now rejects with the translated
  HostProcessError(shell.git_bash_not_found) instead of the raw
  ProbeShellNotFoundError, matching what sync field reads throw and what
  SDKRpcClientV2.ensureConfigFile() surfaces, while an internal no-op
  handler keeps the rejection from becoming an unhandledRejection.
- Replace the Windows-gated probe-failure tests with vi.mock-stubbed
  deterministic suites that run identically on any platform.
- Move the ProbeShellNotFoundError explanation into the environmentProbe
  file header per the package comment convention.

* fix(agent-core-v2): narrow probe error to Error to satisfy only-throw-error lint

* fix(agent-core-v2): preserve probe error as cause when translating to HostProcessError

* fix(agent-core-v2): keep checked paths out of the public probe error message

* fix(node-sdk): gate the host-environment wait in ensureConfigFile to Windows

The missing-Git-Bash failure is Windows-only, and IHostEnvironment.ready
also covers the login-shell PATH enrichment, which spawns the user's login
shell with a 5s timeout. Awaiting it on POSIX coupled config-only commands
(kimi provider list/remove, export, ...) to the user's shell profile for no
benefit.

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-08-11 21:13:08 +08:00
HydrogenE7
64abebc95a
fix(apps/kimi-code): allow deselecting Other option in multi-select question dialog (#2810) 2026-08-11 19:55:13 +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
e702817124
fix(agent-core-v2): degrade idle-session steer to turn launch like v1 (#2723)
- return the enqueue-launched turn instead of rejecting with
  prompt.not_found when no prompt is pending at steer time
- report steer as queued when a manual compaction holds the context
- sync title/lastPrompt metadata on main-agent steer, matching v1
- update the v1-v2 parity test to assert converged behavior
2026-08-11 14:40:15 +08:00
Haozhe
71ff2a0fff
fix(kimi-code): close pre-trust-gate bare command resolution on Windows (#2695)
On Windows, cmd.exe / CreateProcess resolve a bare command name from the
current directory before PATH. Several startup-path child processes ran
before the workspace trust prompt, so a binary planted in an untrusted
workspace (stty.exe, npm.cmd, fd.exe) could execute before the user
confirmed trust.

- skip the POSIX-only stty save/restore entirely on win32
- defer fd detection from the KimiTUI field initializer to
  startBackgroundFdAutocomplete(), which runs after the trust gate
- add resolveCommandPath(): resolve commands through PATH (PATHEXT-aware
  on win32) to an absolute path and refuse hits inside the cwd
- route update-preflight package-manager spawns and the npm global-prefix
  probe through it
- run the workspace trust prompt before the migration branch as well,
  closing the blind spot where a pending ~/.kimi migration skipped it
- document the no-bare-command-before-trust-gate rule in
  apps/kimi-code/AGENTS.md
2026-08-11 12:53:52 +08:00
Haozhe
2acf22f66e
refactor(agent-core-v2): invert sessionLifecycle/MCP dependency via lifecycle event (#2803)
- add onWillCreateSession to ISessionLifecycleService: a synchronous
  participation event fired before a session's services activate, exposing
  a session-domain facade (readSeed / contributeSeed / onSessionDispose)
- workspaceMcp subscribes and activates ephemeral-server overlays itself:
  the configs travel as the new ISessionEphemeralMcpServers session seed,
  the stdio cwd is read from ISessionContext, the merged ISessionMcpHandle
  is contributed over the seed adapter's workspace projection, and the
  overlay shutdown is attached to the session's teardown
- sessionLifecycle drops its IWorkspaceMcpService dependency, the overlay
  tracking map, handle-dispose wrapping, and the dispose backstop
- rename ScopeOptions.extra to seeds and ScopeOptions.assemble to
  configureContainer
2026-08-11 12:31:47 +08:00
Haozhe
0401ec4286
refactor(agent-core-v2): extract btw into a features/btw Feature unit (#2724)
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
Release / Publish native release assets (push) Blocked by required conditions
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
Release / Native release artifact (push) Blocked by required conditions
- move session/btw to features/btw, mirroring the plan feature layout
- contribute ISessionBtwService at Session scope through BtwFeature
  (contributeService) instead of a static registerScopedService call
- keep the package root exports unchanged; move the test to
  test/features/btw
2026-08-10 11:09:23 +08:00
oocz
01c74e9372
fix(agent-core): isolate builtin profile catalogs per session (#2740)
Some checks failed
CI / build (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test (5) (push) Has been cancelled
CI / test-pi-tui (push) Has been cancelled
CI / test-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
Nix Build / Check flake.nix workspace sync (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Native release artifact (push) Has been cancelled
Nix Build / nix build .#kimi-code (push) Has been cancelled
Release / Deploy docs (push) Has been cancelled
Release / Publish native release assets (push) Has been cancelled
2026-08-08 14:26:17 +08:00
Liu Zhongnuo
437a1b8ba1
fix(sdk): probe MCP auth status through connection (#2731)
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-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-07 22:18:53 +08:00
Liu Zhongnuo
0b2e803d5e
feat(sdk): expose global MCP auth status (#2706)
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-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 / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
2026-08-07 13:11:18 +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
Haozhe
c0b61c6e55
fix(agent-core-v2): count compaction tokens on the full-request basis (#2699)
Some checks are pending
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 / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (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
- tokensBefore/tokensAfter now include the system prompt and non-deferred
  tool schemas, matching the measured-anchor basis the context gauge uses
  between exchanges
- the post-compaction ledger rebase carries the same full-request size, so
  the reported context size no longer dips to a messages-only estimate and
  jumps back on the next exchange
- the PreCompact hook tokenCount uses the same basis
2026-08-07 07:31:11 +08:00
qer
476787fec9
docs(agents): rework changelog curation rules for the user-facing changelog (#2708)
* docs(agents): rework changelog curation rules for the user-facing changelog

* docs(agents): refine catch-all wording and add reviewer notes to the changelog preview

* docs(agents): surface folded entries at the sync review checkpoint
2026-08-07 02:23:37 +08:00
qer
d9ec566e51
docs(changelog): sync 0.34.0 from apps/kimi-code/CHANGELOG.md (#2704)
Some checks are pending
CI / test (5) (push) Waiting to run
CI / lint (push) Waiting to run
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-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 / Publish native release assets (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
* docs(changelog): sync 0.34.0 from apps/kimi-code/CHANGELOG.md

* chore: link
2026-08-06 22:55:50 +08:00
wenhua020201-arch
51ef78b8c9
docs: add Official Plugins section with WebBridge and Computer Use (#2653)
* docs: add Official Plugins section with WebBridge and Computer Use

Group the three official capabilities (Kimi Datasource, Kimi WebBridge,
Kimi Computer Use) under a new Official Plugins section on the plugins
page, with a single shared install/upgrade flow. Add an authorization
walkthrough screenshot for Computer Use and regroup the Datasource
coverage table by category with named data sources.

* docs: add browser extension install steps for Kimi WebBridge

Installing via /plugins is not enough on its own: AI can only drive the
browser after the Kimi WebBridge extension is present. Document both
install paths (Chrome Web Store / Edge Add-ons, and manual load-unpacked
via chrome://extensions with Developer mode) plus a quick way to verify.

* docs: split WebBridge manual install into illustrated steps

Break the manual extension install into numbered steps with per-step
screenshots: enable Developer mode on chrome://extensions, then load the
unpacked kimi-webbridge-extension folder.

* docs: tighten WebBridge install screenshots to the relevant area

* docs: add WebBridge ready-state verification screenshot

* docs: note WebBridge's two-part install in the shared install steps

* docs: even out WebBridge install screenshot edges

* docs: replace WebBridge install screenshots with clean crops

Re-shoot source images: split the two-step manual install guide into
per-step screenshots with clean edges, and replace the ready-state popup
screenshot with the toolbar-icon success indicator.

* docs: use newly provided WebBridge step screenshots

* docs: sharpen Computer Use auth screenshot and center it

Replace the downscaled auth-window image with a crisp native capture,
constrain its display width to 380px, and center it on the page. Also
move the WebBridge two-part install note into an info callout directly
under the shared install steps.

* docs: drop the coverage start year from the Datasource table

* docs: spell out the two WebBridge extension install options

* docs: show the Kimi Code toggle enabled in the Computer Use auth screenshot

* docs: show version badges for WebBridge and Computer Use, rework Computer Use scenarios

Add version badges next to all three official plugin names. Rewrite the
Computer Use capability list around verified task shapes and add a
warning callout for operations that should not be delegated. Keep the
final WebBridge install step inside the numbered list.

* docs: add Windows (WinCU) notes to Computer Use

Computer Use now ships a Windows runtime with a different install path
and behavior: it may briefly take over the real mouse and keyboard
instead of running fully in the background. Document the install
command, system requirements, permission model, and privilege matching,
and stop claiming the feature is macOS-only.

* docs: break up the plugin manager wall of text

Split the Installation and Management paragraph into bullets, drop the
parts duplicated by the Official Plugins section (including the outdated
macOS-only note), and link to that section instead.

* docs: list the plugin manager tabs and drop the tab-behavior block

* docs: give the WebBridge extension install section an English anchor

* docs: restore the /reload or /new activation step for official plugins

* docs: align the plugins page wording with the published docs site

* chore: retrigger CI

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-08-06 22:51:27 +08:00
github-actions[bot]
f0614c53e5
ci: release packages (#2641)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-06 21:56:49 +08:00
Haozhe
794714ebef
fix(agent-core-v2): gate plugin changes behind session baselines and reminders (#2702)
* fix(agent-core-v2): gate plugin changes behind session baselines and reminders

- capture a per-session MCP server baseline (ISessionMcpHandle.isBaselineServer)
  so servers added mid-session (plugin install, mcp.json edit) never register
  tools in live sessions; they take effect on /new, /reload, or resume, while
  removed servers stay tombstoned and fail calls with a removal notice
- stop rebuilding the system prompt on plugin-source catalog changes: the
  frozen skill listing and plugin sections cannot move anyway, and the rebuild
  only churned the ${now} timestamp, invalidating the provider prompt cache
- freeze the Agent tool description's catalog profile list once the session
  catalog has loaded, keeping the tools payload byte-stable across mutations
- append a plugin_change system reminder to live sessions on plugin mutations
  (new IPluginService.onDidMutate; explicit reloadPlugins does not raise it)
- revert the TUI hint to "Run /new or /reload to apply plugin changes." and
  update the plugin/MCP docs and changesets to the corrected contract

* fix(agent-core-v2): import LifecycleScope from app/scopes in sessionOutcomeMirror

#2666 imported LifecycleScope from #/_base/di/scope, which does not export
it (it lives in #/app/scopes), breaking the package build and typecheck on
main.

* fix(agent-core-v2): close the mutation-driven session-start refresh and overlay baseline leaks

Codex review on the PR found two contract leaks:

- a plugin mutation re-pulls the plugin skill source, and the existing
  catalog listener answered with a fresh plugin_session_start reminder —
  injecting the newly installed plugin's instructions into the live session
  alongside (and contradicting) the plugin_change notice. The session-start
  refresh now skips mutation-driven catalog changes (one per mutation,
  counted; explicit reloads keep the old refresh behavior).
- a session created with ephemeral mcpServers kept its MCP baseline open
  until the overlay connect finished; a workspace server added in that
  window (plugin install, config edit) leaked into the live session through
  the merged view. The overlay handle's baseline now freezes on the
  workspace manager's initial load, with the ephemeral names baseline by
  construction.

* fix(agent-core-v2): drop duplicate LifecycleScope import in sessionOutcomeMirror test

---------

Signed-off-by: Haozhe <yanghaozhe@moonshot.ai>
2026-08-06 21:11:03 +08:00
qer
fa3325404b
fix(agent-core-v2): import LifecycleScope from the L3 scopes module (#2703)
The L3 unit layer refactor moved LifecycleScope out of the _base DI kernel
into the app tier, leaving the session outcome mirror with a stale import
that broke typecheck and import-time evaluation on main.
2026-08-06 20:56:54 +08:00
qer
03aa66ca0c
fix(tui): show WebBridge setup steps after install (#2692)
* fix(tui): show WebBridge setup steps after install

* fix(tui): scope WebBridge hint to capability install

* fix(tui): format WebBridge setup links

* fix(tui): align WebBridge setup with live reload

* fix(tui): retain WebBridge session activation step

* fix(tui): compact WebBridge setup links

* fix(tui): list WebBridge setup links clearly

* fix(tui): show clickable WebBridge URLs

* Revert "fix(tui): show clickable WebBridge URLs"

This reverts commit 373ffae4b8cf4256131002b990f9a1869e9ee156.

* fix(tui): restore WebBridge setup heading
2026-08-06 20:19:32 +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
liruifengv
e6e4ba2357
chore: sync web dist from code-app (#2697)
* chore: sync web dist from code-app

* chore: add changesets for the synced web UI changes

* chore: drop changesets already covered by the previous web bundle sync

* chore: correct the drop-folder changeset for web

* chore: drop the drop-folder changeset (desktop-only feature, no web announcement)
2026-08-06 19:58:01 +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
4d39f4fa6f
fix(agent-core-v2): omit max_context_tokens in REST session status when unknown (#2696)
- align the REST status rollup with the WS push: a bound alias that no
  longer resolves omits max_context_tokens instead of reporting 0 (0 is the
  engine's UNKNOWN_CAPABILITY marker, not a real limit)
- fall back to the default model's limit only when no model is bound,
  resolved through IModelService like the WS side
- mark max_context_tokens optional in the shared session status schema
2026-08-06 18:50:16 +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
qer
ef61084009
fix(kimi-code): select compatible PowerShell for Computer Use (#2686)
* fix(kimi-code): select compatible PowerShell for Computer Use

* fix(kimi-code): handle locked Computer Use plugin files

* fix(kimi-code): align Windows Computer Use name

* fix(agent-core-v2): reuse PowerShell fallback for detection

* fix(agent-core-v2): refresh ready Computer Use plugin
2026-08-06 18:08:28 +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
qer
013203421d
fix(tui): surface capability install errors (#2682)
Some checks are pending
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
CI / lint (push) Waiting to run
CI / build (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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-08-06 14:02:59 +08:00
liruifengv
3c75a27da6
feat(tui): add cache-expiry hint dialog for resumed and idle sessions (v2 engine) (#2646)
* feat(agent-core-v2): detect prompt-cache breaks from per-step usage and emit telemetry

Track consecutive turn-scoped LLM requests per agent; when the cache-read
token count drops by more than 5% and by more than 2000 tokens between
requests, log a debug line and emit cache_break_detected with both usages,
the drop ratio, and the interval. Operation requests (e.g. compaction) act
as a baseline barrier so expected drops are not reported.

* feat(tui): add cache-expiry hint dialog for resumed and idle sessions (v2 engine)

Resuming a long-idle session or submitting after a long idle stretch
re-sends the whole history with an expired context cache. Show a dialog
offering to compact, start a new session, continue as-is, or never ask
again (persisted as cache_expiry_hint in tui.toml). Thresholds come from
the client_configs endpoint (estimated_cache_duration) via a generic
per-name cached client; only OAuth-managed providers participate.

* fix(tui): preserve submit order and revalidate session in cache-hint flows

Cold-cache submits during the in-flight config fetch are now swallowed and
replayed through a FIFO chain, so a later prompt can never overtake the
stashed one. Both the resume and idle paths re-check the current session
after the async fetch: a switch mid-flight drops the dialog (resume) or
hands the stashed input back to the editor instead of sending it into the
wrong session (idle).

* chore(agent-core-v2): regenerate state manifest after merging main

* fix(tui): apply cache_expiry_hint on /reload and /reload-tui

* fix(agent-core-v2): skip unmeasured all-zero usage in cache break detection

* fix(tui): restore chained cache-hint submits when the dialog is not sent

When several submits are swallowed during the cold-config fetch and the
first dialog is dismissed (or its compact/new action fails), the stashed
inputs were restored while later chained submits were still released —
reordering the conversation. Chained submits now follow the fate of the
message that opened the dialog, and multiple restores append newline-joined
instead of overwriting the editor.

* fix(agent-core-v2): reset cache-break baseline on model change

Caches are per-model, so a cache-read drop after /model is expected, not a
break. The baseline now carries the model and only same-model records are
compared.

* fix(tui): only count LLM-activity replay records for the resume cache hint

The v2 resume replay also carries local-only state records (permission,
plan, config updates, approval results) that slash commands append without
an LLM request. Filter lastActiveAt to message/compaction records so a
recent local change no longer masks an expired cache.

* style(agent-core-v2): rewrite the cacheBreak impl header per package convention

State the domain role, collaborators, and scope instead of narrating
implementation steps; the behavior guards now live in the code alone.

* fix(tui): drop the resume cache hint when a turn started mid-fetch

The resume dialog is fire-and-forget over an async config fetch; if the
user already sent the first prompt by the time it resolves, mounting would
overlay an active turn and its actions would hit the live session. Re-check
streamingPhase/isCompacting after the await, next to the session check.

* style(agent-core-v2): trim the cacheBreak contract header to contract and scope

* refactor: report cache-break detection from the TUI client

Move the detector out of the engine so the telemetry event carries the
client's own identity (which client produced it is now attributable). The
TUI observes main-loop turn.step.completed usage directly, with the same
guards: first-step/unmeasured/all-zero records skipped, model change and
compaction reset the baseline. The agent-core-v2 cacheBreak module is
removed.

* chore: drop accidentally committed dist-web build output and ignore it

* chore: revert the dist-web ignore rule

* chore: restore dist-web to the tracked content from main

* feat(tui): record cache breaks caused by mid-session model/effort switches

A model or effort change mid-session busts the prompt-cache key — that is
a real cache break worth attributing, not noise. The baseline now carries
model and effort, the same-model exemption is gone, and cache_break_detected
reports prev/curr model and effort alongside both usages.

* chore(changeset): simplify the cache-expiry hint entry

* chore(changeset): trim the cache-expiry hint entry to one line

* fix(tui): cache-hint review follow-ups

- carry the pre-dialog media extraction through compact/new resends so
  pasted attachments survive the image-store clear on a new session
- reset the cache-break baseline after /undo — the context cut makes the
  next cache-read drop expected
- release the stashed submit when a foreground operation started during
  the cold-config fetch instead of mounting the dialog over it
- count a completed compaction as activity so the next submit is not
  judged against the pre-compaction timestamp

* chore(changeset): drop the v2-engine-only suffix

* fix(tui): seed the activity baseline when the resume check skips

* fix(tui): cache-hint review follow-ups

* fix(tui): record cache activity on completed steps, not turn begin

* feat(cli): persist the client-configs cache across restarts
2026-08-06 13:26:37 +08:00
liruifengv
713bf1a5a2
fix(kimi-code): render v2 background task notifications on session replay (#2677)
* fix(kimi-code): render v2 background task notifications on session replay

* chore: add changeset for v2 task notification replay fix
2026-08-06 13:00:53 +08:00
Haozhe
34c4181437
fix(kimi-code): keep kimi -p alive while background tasks are pending (#2675)
The 10-year default print wait ceiling (315360000s) overflowed Node's
setTimeout limit (2^31-1 ms) into a 1ms fire, so the steer/drain wait
returned instantly and kimi -p exited right after the main turn, killing
pending background tasks and subagents.

- add setClampedTimeout in agent-core-v2 _base, clamping delays to
  MAX_TIMER_DELAY_MS, and route every config-driven timer through it
  (timeoutOutcome, task wait/manager timeout, swarm attempt timeout)
- chunk the print turn-endings wait against the real deadline instead of
  returning null on the first clamped timer fire
- restore v1 semantics: a non-positive swarm subagent timeout is unbounded
- default print_wait_ceiling_s to 2147483s (~24.8 days, the timer maximum)
2026-08-06 11:02:34 +08:00
Haozhe
7bd3fd9f6e
feat(agent-core-v2): read UTF-16 text files by transcoding to UTF-8 (#2647)
- detect UTF-16 LE/BE from a BOM or a zero-byte parity heuristic
  (tolerant of CJK content), derived from VS Code's encoding detection
- Read tool and workspace fs.read transcode UTF-16 text to UTF-8
  instead of refusing it as binary; larger than 10 MiB still refused
- refuse other non-UTF encodings (e.g. GBK) with a clearer message
2026-08-06 10:39:21 +08:00
qer
d1ded01b7c
fix(agent-core-v2): seed the activity view's lastTurn from the persisted turn.ended record (#2648)
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 / 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
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
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(agent-core-v2): seed the activity view's lastTurn from the persisted turn.ended record

A cold-resumed agent seeded its activity view only from live loop/task
state, so the last turn's outcome was lost on a server restart: sessions
came back with no lastTurnReason, and clients could not surface a
previously failed turn (e.g. a provider 429 that killed the turn before
the restart).

The loop already persists the terminal turn.ended record (reason, error,
durationMs); fold the latest one into the TurnModel as lastEnded and have
AgentActivityView.seedFromLoop adopt it when no turn is active, so the
session work aggregate (and everything built on it) reflects the last
turn's outcome again after a cold start.

* fix(agent-core-v2): seed lastTurn on wire restore and add a changeset

Review follow-up: the agent scope (and with it this view) is constructed
before wire.restore() replays the journal, so a constructor-time read of
TurnModel.lastEnded always saw the initial state on a cold resume. Move
the wire-backed seed behind the onDidRestore hook (constructor seed kept
for views built after a restore), and drop the inline comments in favor
of the file header per the package comment convention.

* docs(agent-core-v2): trim the activityView header to role and collaborators

Review follow-up: the previous revision narrated the restore-hook
mechanics in the header; the package convention keeps headers at the
module's external role plus collaborators, so drop the implementation
narrative.

* fix(agent-core-v2): keep TurnModel.lastEnded across clock advances

Review follow-up: advanceTurnClock built a fresh state object without
spreading, so a new prompt or a queued cancel silently dropped the stored
last-ended outcome even though no new turn had ended — after a restart the
activity view would again find nothing to seed. Spread the prior state and
cover the prompt/queued-cancel/replace cycle with a model-level test.

* fix(agent-core-v2): clear the stored turn outcome once a newer turn starts

Review follow-up: with the clock advances preserving lastEnded, a prompt
persisted without its turn ever starting would leave the previous turn's
outcome to be seeded after a restart, reporting a stale result for a turn
that never ended. The loop-event fold now drops lastEnded as soon as a
newer turn's events land, while prompts and queued cancels keep it.

* docs(agent-core-v2): keep the turnOps header at the domain role

Review follow-up: the lastEnded keep/clear mechanics read as
implementation narrative in the header; the convention there is role and
collaborators only.
2026-08-06 01:07:22 +08:00
qer
68ba740ebf
feat(kimi-code): support Kimi Computer Use on Windows (#2652)
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-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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-08-05 21:15:02 +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
7Sageer
858812193a
fix(kimi-code): open /feedback to all signed-in users (#2639)
* fix(kimi-code): open /feedback to all signed-in users

Gate the command on holding a kimi-for-coding OAuth token instead of
the active model's provider, so signed-in users on API-key models can
also submit feedback through the authenticated channel. When signed
out, open the sign-up page alongside GitHub Issues.

Also harden the failure paths: a failing auth status lookup or a
rejected submit promise now falls back to GitHub Issues, while
attachment-stage failures degrade to a non-fatal partial failure
instead of triggering the fallback.

* fix(kimi-code): print sign-up and issue links for signed-out /feedback

Opening two browser pages at once is jarring; just print the links in
the transcript instead.
2026-08-05 17:27:54 +08:00
liruifengv
7c919f0376
docs(changelog): sync 0.33.0 from apps/kimi-code/CHANGELOG.md (#2636)
* docs(changelog): sync 0.33.0 from apps/kimi-code/CHANGELOG.md

* docs(changelog): move the v2 engine entry to Refactors and the trust prompt to Polish
2026-08-05 16:46:10 +08:00
github-actions[bot]
53c832dfdf
ci: release packages (#2592)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-05 16:21:32 +08:00
qer
2b3e9a9f79
fix(tui): clarify curated plugin marketplace (#2635) 2026-08-05 16:18:50 +08:00
liruifengv
7a631522fb
chore(changeset): trim the v2 engine entry and drop the custom identity entry (#2634) 2026-08-05 16:08:42 +08:00
liruifengv
421e8f8065
fix(minidb): silence the transform-types warning from the dev worker (#2632) 2026-08-05 15:53:13 +08:00
liruifengv
3bd098b806
chore(changeset): add changesets for the v2 TUI and web UI updates (#2630) 2026-08-05 15:27:27 +08:00
qer
75fe068a01
fix(cli): stabilize built-in capability installation (#2601)
* fix(cli): show built-in capabilities before the first session exists

The lazy-session refactor left capability calls going through
requireSession(), so on a session-less v2 startup /plugins reported the
capabilities unavailable and hid the built-in rows behind the promo.
Like plugin management, capability readiness and installs are app-global
on the v2 engine: the node-sdk harness gains a capability facade over
the global channel, and the TUI resolves session-or-harness for every
capability call.

* fix(cli): count the dev marketplace server as the default catalog

dev.mjs always points KIMI_CODE_PLUGIN_MARKETPLACE_URL at its own
repo-serving server, which the override gate mistook for a user-configured
marketplace and suppressed the built-in capability rows in every dev run.
The dev server now marks itself, and the gate treats that marked URL as
the default catalog while still honoring real overrides (slash-command
source, user-set env, KIMI_CODE_DEV_MARKETPLACE_URL).

* fix(cli): align built-in capability updates
2026-08-05 14:55:04 +08:00
Haozhe
f881cdd970
feat(cli): default CLI surfaces to the agent-core-v2 engine (#2627)
Some checks are pending
CI / lint (push) Waiting to run
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
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
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
* feat(cli): default to agent-core-v2 engine with KIMI_CODE_LEGACY_FLAG opt-out

- invert the engine gate: isKimiV2Enabled() now returns true unless
  KIMI_CODE_LEGACY_FLAG is truthy; KIMI_CODE_EXPERIMENTAL_FLAG no longer
  selects the engine
- replace the experimental `kimi acp-v2` command with the native v2
  implementation as the default `kimi acp`; the legacy acp-adapter path
  remains under the legacy flag
- drop the acp-v2 experimental flag from the registry
- rename the dev:cli:v2 script to dev:cli:legacy
- update en/zh docs for the new default engine and the legacy flag

* feat(cli): route export and provider through the engine gate

- select the harness via isKimiV2Enabled(): agent-core-v2 by default,
  the legacy harness when KIMI_CODE_LEGACY_FLAG is truthy
- close the harness after each one-shot command so the v2 engine's
  watchers do not keep the process alive
- document both commands in the KIMI_CODE_LEGACY_FLAG env-var entry
2026-08-05 14:42:23 +08:00
7Sageer
e3570280bd
fix(agent-core-v2): bound the project skill-root watch fd footprint (#2612)
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-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
* fix(agent-core-v2): bound the project skill-root watch fd footprint

The workspace skill-root source recursively watches the skill-root
candidates with chokidar, which holds one fs.watch fd per file and per
directory on macOS. A skill bundling a large runtime tree can exhaust
the process fd budget and break every subsequent spawn (EBADF).

Mirror the scanner's own pruning (node_modules / dot entries, scan
depth cap) in the watch filter, and add a signal mode to hostFsWatch:
rescan-style consumers get ONE native recursive fs.watch on
darwin/win32, whose fd footprint is constant in the subtree size.

* fix(agent-core-v2): align the skill watch with scanner semantics and harden signal mode

Review follow-up:

- The scanner probes every entry's direct SKILL.md before gating
  recursion, so the watch filter now keeps an excluded entry itself and
  its direct SKILL.md (keepEntryFile) instead of pruning them — skills
  under node_modules / dot directories keep their hot reload.
- The signal-mode native leg now owns its recovery: a native watch
  error fires one root invalidation and re-arms with capped exponential
  backoff; chokidar is used only where recursive fs.watch is
  unavailable, so a transient failure can neither silently end hot
  reload nor downgrade to the per-node watcher.
- Native event path resolution handles absolute filenames and the
  root-basename case, clamping out-of-root events to a root
  invalidation instead of dropping them.
- The event mapping is extracted into NativeSignalMapper with the stat
  call injected, so the native-branch decisions are unit-tested on any
  platform.

* Fix spawn EBADF issue on macOS for large file trees

The skill watcher no longer opens every file it watches, improving performance.

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

* fix(agent-core-v2): harden native signal watch recovery

* fix(agent-core-v2): align skill watch with scanner traversal

* fix(agent-core-v2): make skill watch handoff converge

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-08-05 13:47:26 +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
Haozhe
4e5f36aa66
test(agent-core-v2): stop the reconcile loop during the sessionIndex read baseline (#2625)
The baseline gates warm reads on behavioral work counts, including zero
directory listings inside each counting window. The service's background
reconcile loop (60s interval) runs an authoritative scan on every tick —
two storage.list calls — and once a slow CI runner stretches the test past
that interval, a tick lands inside a counting window on every attempt and
is attributed to the read under test, failing fsLists === 0 even with the
retry (observed on run 30975794688: 168s across two attempts, fsLists = 2
both times).

Freeze the loop right after prepare() via a new stopReconcileLoop() test
hook (sibling of reconcileNow/reprojectNow). The only remaining directory
listing sources inside a window are the fallback read paths themselves,
so a non-zero count is deterministically a real regression again.
2026-08-05 13:43:22 +08:00
liruifengv
541ddd2d89
chore(web): replace apps/kimi-web with the code-app web bundle (#2599)
* chore(web): remove apps/kimi-web in favor of the code-app web bundle

The web UI source now lives in the code-app repo (apps/web); this repo
only ships the prebuilt bundle at apps/kimi-code/dist-web, synced from
code-app via \.

- delete apps/kimi-web (source, tests, docs)
- root package.json: drop dev:web and the kimi-web typecheck leg
- apps/kimi-code: drop the workspace dep and the build-from-source step;
  replace copy-web-assets.mjs with check-web-assets.mjs so packaging
  fails fast when the committed bundle is missing
- CI: _native-build verifies the committed bundle instead of building
  from source; ci.yml and pkg-pr-new.yml drop the kimi-web legs
- docs: AGENTS.md project map, changeset README, gen-changesets and
  sync-changelog skills now key web UI entries on dist-web

* chore(web): stop ignoring dist-web now that the bundle is committed

The ignore entry predates the code-app sync flow, when dist-web was a
local build artifact. The bundle is now the canonical, committed form of
the web UI, so ignoring it only forces every sync to git add -f.

* docs(skills): drop the web-specific changeset and changelog rules

The web: prefix convention and the web-specific dedup guidance belonged
to the in-repo web app. With the source moved to code-app, web UI
changes follow the same generic rules as any other CLI-bundle change.

* chore(web): add changesets for the web UI changes in the bundle

* chore(web): collapse the bundle changesets into one umbrella entry

* ci: unbreak nix and lint after the kimi-web removal

- flake.nix: drop apps/kimi-web from the source fileset and package
  lists, and verify the committed dist-web in the native build phase
  instead of building the web app from source
- .oxlintrc.json: exclude dist-web (a committed build artifact) now
  that .gitignore no longer hides it from the linter

* ci(nix): update pnpmDeps hash for the post-kimi-web lockfile

* chore(web): resync dist-web from code-app main

Bundle rebuilt from code-app main (upstream parity ports #175, pinned
sidebar #176) with the CLI version 0.32.0 embedded.
2026-08-05 13:38:30 +08:00
Kai
e7d5a0aee7
test(agent-core-v2): give the sessionIndex list baseline CI headroom (#2615)
* test(agent-core-v2): give the sessionIndex list baseline CI headroom

* test(agent-core-v2): retry the sessionIndex baseline once to absorb runner load spikes

* test(agent-core-v2): gate the sessionIndex baseline on work counts, not wall clock

The baseline asserted wall-clock medians (list < 300ms, get/count < 50ms)
plus a relative time check — all still load-sensitive on shared CI runners.
Replace every time assertion with behavioral complexity assertions recorded
at the IQueryStore and fs seams: list must be served by bounded pageByColumn
fetches only, get is a single point lookup, count never touches the session
collection, no warm read enumerates session directories, and the per-op work
snapshot must be identical at 1k, 10k, and 50k sessions. Medians are still
logged for phase-to-phase comparison; the retry now absorbs a background
reconcile tick landing inside a counting window.

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-08-05 10:59:28 +08:00
zy
2ee6e43124
fix(mcp): re-register the OAuth client when its redirect URI no longer matches (#2620)
The callback listener binds a random port per flow, while DCR
registration records the redirect URI of the flow that created it —
so every interactive authorization after the first was rejected with
"Invalid redirect URI", an error rendered only in the user's browser
while the client waited for a callback that never came. Detect the
mismatch before invoking auth() and drop the stale registration so the
flow re-registers with the current callback URI (v1 + v2).

Resolve #2606

Co-authored-by: zouying <zouying@moonshot.cn>
2026-08-05 10:24:24 +08:00
Kai
8db7d42f23
feat(tui): add /bug as an alias for /feedback (#2614)
Some checks are pending
CI / lint (push) Waiting to run
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 / Publish native release assets (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
2026-08-04 23:54:21 +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
74c321e4c6
fix(mcp): drop protocol-reserved _meta keys from model-visible output (#2600)
* fix(mcp): drop protocol-reserved _meta keys from model-visible output

Follow-up to #2596. The MCP spec reserves _meta key prefixes whose labels
include "modelcontextprotocol" or "mcp" for protocol use; those entries
carry host/protocol plumbing rather than model-facing data, so filter them
out before serializing the <mcp-structured-result> block. Unprefixed and
vendor-prefixed keys still pass through — their semantics belong to the
server. Also moves the v2 implementation commentary into the module header
per the agent-core-v2 comment convention.

* fix(mcp): reserve _meta prefixes only when a label follows mcp/modelcontextprotocol

Per the spec's key-name rules a prefix is reserved when a
modelcontextprotocol or mcp label is followed by at least one more label;
a trailing reserved word (com.example.mcp/) is a legitimate vendor
namespace and now passes through.

---------

Co-authored-by: zouying <zouying@moonshot.cn>
2026-08-04 20:03:10 +08:00
zy
96f77fe392
fix(agent-core-v2): port the MCP auth:'oauth' opt-in from v1 (#2608)
v1 lets a remote MCP server combine static headers with OAuth via the
auth:'oauth' config marker; v2 neither parsed the field nor honored it,
so any configured headers were treated as static credentials — first
authorization never started and an expired refresh token left the entry
permanently failed. Add the field to the v2 remote config schemas and
mirror the v1 guard in shouldMarkNeedsAuth.

Resolve #2605

Co-authored-by: zouying <zouying@moonshot.cn>
2026-08-04 20:02:57 +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
7Sageer
aec755fad6
feat(agent-core-v2): announce date changes via a system reminder (#2564)
* feat(agent-core-v2): announce date changes via a system reminder

* refactor(agent-core-v2): hide profile rendering behind service

* refactor(agent-core-v2): normalize profile prompt rendering at registration

* chore: retrigger ci
2026-08-04 19:31:00 +08:00
zy
c32e661faa
fix(mcp): pass structuredContent and _meta through to model-visible tool output (#2596)
MCP tool results were narrowed to {content, isError}, dropping the
spec-defined structuredContent field and _meta metadata. Servers that
return structured contracts in these fields (validated against
outputSchema, or namespaced metadata such as browser-handoff payloads)
were invisible to the agent. Surface them as a serialized
<mcp-structured-result> block appended to the tool output, still subject
to the existing text budget.

Co-authored-by: zouying <zouying@moonshot.cn>
2026-08-04 18:01:42 +08:00
qer
0abcd00f7f
feat(cli): add built-in Computer Use and WebBridge capabilities (#2407)
* feat(agent-core-v2): add built-in capabilities (kimi-cu, kimi-webbridge) with REST routes

Add a capability domain holding a closed registry of built-in product
capabilities. Each entry owns layered readiness detection and idempotent
install orchestration: binary runtimes from fixed official CDN URLs
(KimiCU.app + launchd service + TCC permission state; the WebBridge
daemon with start-if-down semantics for Kimi Work coexistence) plus
agent wiring through the plugin service. The WebBridge wiring un-shadows
stale user-source skill copies (user priority beats plugin priority).

kap-server exposes the domain as GET /api/v1/capabilities,
GET /api/v1/capabilities/{id}, and POST /api/v1/capabilities/{id}:install
with client-polled progress and new wire codes 40418 / 40922 / 40923.

The plugin marketplace gains an official kimi-webbridge entry
(browser-control skills) packaged by the existing CDN build.

* fix(agent-core-v2): rename the webbridge wiring plugin to kimi-webbridge-skill

An official kimi-webbridge guide plugin (install/remove setup skills,
v3.0.4) already exists at the marketplace path the capability installer
pointed at — a different artifact owned by another release line. Give
the browser-control usage-skill plugin its own id/path instead of
colliding with (or overwriting) the guide plugin. The capability entry's
detect/install now tracks kimi-webbridge-skill; a machine with only the
guide plugin correctly reports the skill layer as missing.

* feat(agent-core-v2): shelf installs auto-complete capability binary layers

Two changes to make the plugin marketplace a first-class install path:

- Marketplace gains kimi-cu (sourced from the CU team's CDN zip — no
  repackaging) and the kimi-webbridge usage-skill plugin now claims the
  kimi-webbridge id at v4.0.0, deliberately superseding the WebBridge
  guide plugin (v3.0.4, install/remove guide skills): guide users get a
  version upgrade onto the real usage skill.
- The capability service subscribes to IPluginService.onDidReload: when
  a capability's wiring step flips to ok through ANY install path
  (shelf, TUI, CLI), it auto-completes the missing binary layers
  (KimiCU.app + service, or the WebBridge daemon). Triggers only on the
  false→true edge so completed installs with still-missing manual steps
  (TCC permissions) never retrigger heavy downloads on later reloads.

* fix(plugins): keep kimi-webbridge plugin version aligned with the upstream skill

The plugin version tracks the bundled official usage skill (1.11.3) so
version drift against the WebBridge release line stays visible, instead
of minting an independent 4.0.0.

* fix(agent-core-v2): never report the webbridge installer-script version as the product version

The on-disk ~/.kimi-webbridge/bin/kimi-webbridge.version file tracks the
installer's own lineage (3.1.x, bumps on every install/upgrade run),
not the product version (v1.11.3 — daemon, extension, and skills all
share it). A downed daemon would have shown the misleading installer
number; report no version instead (live /status remains the source of
truth).

* chore(plugins): list kimi-cu on the marketplace without a pinned version

Marketplace versions are optional by schema: rows display the version
detected from the installed plugin's manifest, and update prompts only
fire on a valid semver latest > local comparison. A hand-maintained
number would drift just like the guide plugin's did. The locally built
kimi-webbridge entry keeps its manifest-stamped version (1.11.3).

* fix(agent-core-v2): fire onDidReload on plugin mutations, not just explicit reload

installPlugin / setPluginEnabled / removePlugin changed the catalog
silently — consumers listening to onDidReload (session skill-catalog
convergence, the capability shelf-install hook) only converged on an
explicit reloadPlugins(). Fire the same summary-shaped event on every
mutation (added:[id] / [] / removed:[id]) so every install path
converges. This also unbreaks the shelf-install hook on real hosts:
its unit tests passed against a fake emitter that fired on installs,
which the real service never did.

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

Expose the App-scope plugin service over the wire so non-CLI hosts
(desktop, web) can manage plugins end to end:

- 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/plugins, POST /api/v1/plugins {source}
- POST /api/v1/plugins/{id}:{enable,disable,remove}
- New wire code 40419 plugin.not_found

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

* 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 replaces a pre-existing
user-source skill (from the official installer) with the plugin-managed
copy — clients can localize the migration instead of the skill silently
disappearing from the user's directory.

* feat(tui): let the real WebBridge marketplace entry win over the pinned promo

The hardcoded Web Bridge row was built when WebBridge had no plugin
package — it pinned above the Official tab and shadowed any catalog
entry with the same id (open-in-browser only). Now that the marketplace
carries the real kimi-webbridge plugin, flip the precedence: the catalog
entry renders and installs normally, and the pinned promo becomes a
loading/error/legacy-catalog fallback only. Footer counts keep their old
semantics (catalog-only; the promo row is never counted).

* fix(tui): dim the installed state so it stops reading as the install action

Both badges shared a near-identical green-ish treatment in the same
column, making a quiet fact look like a clickable action. States now
recede (installed → textDim) while actions stay loud (install →
primary, update → warning).

* feat(agent-core-v2): converge plugin state across processes sharing a home

Multiple hosts share one KIMI_CODE_HOME (CLI, desktop, other agents), but
each PluginService kept a private in-memory snapshot: a plugin installed
or removed in one process stayed invisible to every other live process
until its next restart — new sessions there kept offering stale plugin
skills/MCP, and the capability shelf hook never saw peer installs.

Watch <home>/plugins for installed.json changes and reloadPlugins
(debounced, echo-suppressed around our own mutations) so all consumers
converge in well under a second: session skill catalogs, plugin MCP
mounts, and the capability shelf-install hook alike.

* fix(agent-core-v2): un-shadow webbridge user skills in BOTH user dirs

kimi-code resolves user-scope skills from two roots (~/.kimi-code/skills
and ~/.agents/skills), both at priority 20 — a stale copy in either
shadows the plugin-managed wiring (priority 5), and also keeps the
capability working after the plugin is removed, which reads as
'uninstall did nothing'. Migrate copies in both dirs during install;
other runtimes' dirs (~/.claude, ~/.codex) remain untouched.

* feat(tui): show live runtime-setup progress for capability installs

Installing a capability plugin (kimi-cu, kimi-webbridge) from the
/plugins shelf kicked off a silent background binary install — the row
flipped to installed while megabytes of runtime downloaded invisibly.
Route capability entries through the capability surface instead: the
panel's inline installing line now mirrors live progress (step +
percent) until the install settles, and the transcript reports
ready / failure-with-retry / still-running accordingly. Capability
removal prints an explicit note that runtime binaries are deliberately
left untouched (the capability keeps working), since that read as
'uninstall did nothing'.

Plumbs the capability service through klient's global facade
('capabilityService' decorator resolves in-process) and the node-sdk
v2 client; Session exposes it with a structural feature-detect so v1
engines fail clearly.

* docs(plugins): keep the kimi-cu marketplace blurb accurate for every client

Only the capability-aware clients auto-install the KimiCU.app runtime;
older builds still get wiring-only (the wrapper's error message then
points at the official setup script). Don't overpromise in the catalog
text every version reads.

* feat(agent-core-v2): install capability wiring from client-bundled plugin copies

The kimi-cu / kimi-webbridge wiring plugins ship inside the client release
instead of the marketplace catalog, binding their visibility to the client
version. Capability installs now resolve the bundled copy (env override,
then npm-layout and source-checkout probes from the module) and install it
as a local path, replacing the two CDN zip URLs. A missing bundle fails the
wiring step with a clear reinstall-or-upgrade message.

* build(cli): bundle the capability wiring plugins into client releases

Vendor the official kimi-cu plugin (v0.5.4, from the CU team's plugin zip)
next to kimi-webbridge under plugins/official, copy both into
apps/kimi-code/bundled-plugins at build time, and ship them in the npm
package (files) and the native SEA blob (a new bundled-plugins asset set
extracted into the native cache at startup, published to the engine via
KIMI_CODE_BUNDLED_PLUGINS_DIR). Desktop points the same variable at its
extraResources copy. The .gitignore build-output entries are anchored so
sources under src/native and test/native stop being silently ignored.

* revert(plugins): remove the kimi-cu and kimi-webbridge marketplace entries

Both capabilities now distribute with the client (bundled wiring), so the
catalog drops back to kimi-datasource / superpowers / vercel-plugin. Older
clients never see the entries; current clients install from the Built-in
section. This also reverts the marketplace blurb commit 0635e99c5.

* feat(tui): add a Built-in capabilities section to the plugins panel

The Official tab now opens with a Built-in section fed by the engine's
capability registry (kimi-cu / kimi-webbridge): per-row install state
(install / finish setup / ready), Enter runs the full capability install
with live progress, and unsupported rows hide (kimi-cu off macOS). The
WebBridge promo fallback only remains for v1 engines — on v2 the real
built-in entry wins. Rows double as the reinstall path: a client upgrade
ships newer wiring, and installing again upserts from the new bundle.

* docs(plugins): document the Built-in section and refresh the capability changeset

* build(nix): stage bundled capability plugins into the SEA build

The native SEA blob now embeds the bundled-plugins asset set, so the nix
derivation needs the plugins tree in its src fileset and the staging step
alongside copy-web-assets before build:native:sea.

* revert: drop the client-bundled wiring distribution

Built-in visibility is simpler to get by injecting the two capability
entries into the marketplace catalog at load time; the wiring plugins
themselves keep installing from their fixed official CDN zips. Removes
the vendored kimi-cu plugin, the bundled-plugins npm/SEA packaging and
flake staging, the engine bundle resolver, and the plugins panel's
Built-in section. Keeps the /agents/ and /native/ gitignore anchors so
sources under src/native and test/native are not silently ignored.

* feat(cli): inject the built-in capability entries into the marketplace catalog

The kimi-cu / kimi-webbridge entries are appended by the client at catalog
load time instead of being served by the remote marketplace.json, binding
their visibility to the client version (older clients never see them). No
version is pinned — reinstalling upserts the wiring — and ids the catalog
already carries always win. In a source checkout the webbridge entry
installs the repo's own plugin copy; packaged builds use the official CDN
zip. This reverts the docs paragraph about the Built-in section, which the
simpler approach makes unnecessary.

* test(tui): select the catalog's own first row in marketplace install tests

The client-injected capability entries suppress the WebBridge promo and
append after the catalog rows, so Kimi Datasource now leads the Official
tab — the extra down-key landed on kimi-cu instead.

* feat(cli): surface the built-in capabilities as client-injected marketplace entries

The kimi-cu / kimi-webbridge entries are injected into the marketplace
catalog by the client (v2 engine, default catalog only) instead of being
served remotely, binding their visibility to the client version; injected
rows mask same-id catalog rows, so what these ids mean stays decided by
the client release — a future official listing only reaches older clients,
whose fix is to upgrade.

The /plugins panel shows capability readiness on the rows (setup
incomplete / installing…), platform-gates kimi-cu to macOS, and Enter
finishes the runtime setup with live progress; v1 keeps the plain plugin
install path and the WebBridge promo fallback.

Capability and plugin calls move from the ad-hoc REST routes onto the
typed klient contract (capabilityService next to pluginService), so the
public REST surface returns to its pre-feature shape. Detection is
presence-only — version pins removed: the current version is always read
live (Info.plist, daemon status, install records), installs are
detect-first and idempotent so an interrupted setup can be retried, and
reinstalling pulls the latest managed artifacts (the passive upgrade
path).

* ci: retrigger checks

* fix(cli): recognize Computer Use CDN plugins as official

* fix(cli): keep built-in entries on catalog outage and isolate detector failures

Two review follow-ups: the client-injected entries no longer disappear when
the marketplace catalog is unreachable (they are not served by it), and a
single capability's failing detect probe degrades to a failed step on that
entry instead of rejecting the whole listCapabilities call.

* refactor(cli): simplify built-in capability integration

* refactor(cli): source built-in catalog rows from the engine and tighten detect probes

The injected marketplace entries are now derived from the engine's
capability registry (listCapabilities) instead of hardcoded client-side
copies — the util only owns the mask/append mechanics, and capability ids
are no longer pinned in the CLI (the remove note resolves them through the
registry too). kimi-cu's detect-path probes (service-status, xpc-ping) get
a 3s timeout — they answer in milliseconds when healthy but run on every
status listing, so a wedged binary must degrade quickly instead of
stalling the panel. Document the Official tab's built-in capability rows
in the plugins guide.

* fix(cli): answer capability id membership without running detectors

listCapabilities() runs every entry's detect probes (seconds on a wedged
binary), so using it to decide whether to print the post-remove hint made
every plugin removal pay a full detection round. The id set is part of the
client/engine contract (mirrored in the klient schema), not product data
that drifts — restore the closed-set check. The injected catalog rows keep
flowing from the registry.

* fix(agent-core-v2): make capability setup recover from disabled, partial, and wedged states

Three review follow-ups on the install path: setup now re-enables the
wiring plugin when a previous disable survived installPlugin's upsert
(detection requires enabled, so it would otherwise strand the capability
at partial); the webbridge daemon-binary step verifies the executable bit
on POSIX, so an install interrupted between rename and chmod re-downloads
instead of failing start with EACCES; and kimi-cu's detect degrades
wedged CLI probes (service-status, xpc-ping) to failed steps instead of
throwing, keeping the detect-first install able to repair the remaining
layers — with the probe timeout injectable for tests.

* fix(agent-core-v2): abort capability downloads whose byte stream stalls

downloadToFile had no inactivity deadline: a CDN connection that stops
producing bytes hung the background install forever, wedging the
capability in a permanent installing state (retries rejected as
in-progress) until the process restarted. An idle watchdog now fails the
download after 30s without a chunk; slow but flowing downloads are
unaffected.

* fix(tui): stop offering capability setup on unsupported platforms

An installed wiring plugin whose capability is unsupported on this
OS/arch (kimi-cu off macOS, webbridge on an unknown arch) was treated
like a partial setup: the Installed tab showed setup incomplete and
Enter routed to installCapability, which the service always rejects.
Setup actions are now gated to actionable states (not_installed /
partial); unsupported renders as a dim fact and Enter opens details.

* fix(agent-core-v2): cover the two remaining install wedge modes

Review follow-ups: the KimiCU app step now requires an executable binary,
so a ditto interrupted mid-copy reads as missing and the next setup
re-copies instead of failing EACCES forever; and downloadToFile's idle
budget now also covers the response-header phase via an AbortSignal on
the fetch itself, so a connection that never completes headers fails the
install (clearing the running state) instead of hanging it.

* fix(tui): render capability rows independently of the catalog fetch

While the marketplace catalog was loading or unreachable, the Official
tab showed only the pinned WebBridge promo — built-in runtime setup was
blocked by an unrelated remote fetch, and Enter opened the browser
instead of installing. Locally-known capability rows (from the engine
registry) now render and install in every catalog state; the promo
remains only as the v1 fallback.

* fix(agent-core-v2): keep KimiCU cleanup timeouts best-effort

stopOldProcesses is documented as || true, but runCommand propagates
timeouts: a wedged old binary made kimi-cu uninstall exceed the command
timeout and the reinstall died before ditto could replace the app.
Cleanup commands now swallow failures (the timeout already attempts a
kill) so the replacement always proceeds; the command timeout is
injectable for tests alongside the probe timeout.

* fix(cli): inject built-in entries only for the default marketplace catalog

Injection is part of the default catalog experience: any explicit
replacement (slash-command source or KIMI_CODE_PLUGIN_MARKETPLACE_URL)
now opts out wholesale — its same-id rows are never masked by the
built-ins, and an unreachable custom catalog surfaces its own failure
instead of being silently replaced by a built-in-only tab.

* refactor: align capability row rendering on the source marker and drop conditional spreads

Marketplace-row capability enrichment (status, badges, issue details,
platform filtering) now keys on the capability:<id> source marker — the
same condition Enter uses to route installs — so a custom catalog row
that merely reuses a built-in id renders and installs as a plain plugin.
Also replaces the conditional-spread optional fields with direct
undefined-valued assignments per the repo coding rules.

* refactor(agent-core-v2): move capability comments to the file headers

The domain's comment convention allows only the top-of-file block:
responsibility and scope context for the recent hardening (detect-first
idempotent install, executability gates, probe-failure degradation,
best-effort cleanup, download watchdog, per-entry detection isolation)
now lives in the module headers, and inline narration beside statements
and members is removed.

* fix(tui): follow an in-progress capability install instead of restarting it

Opening /plugins while a capability setup is already running showed the
installing… row, but Enter called installCapability again and the
service's duplicate-start rejection (40922) surfaced as a fake failure.
The panel now checks the live status first and, when an install is
already running, skips the start call and just polls for the existing
progress.

* fix: align two more replacement paths with their contracts

The EXDEV daemon-binary fallback now stages on the target filesystem and
atomically renames over the destination instead of opening a
possibly-running binary for write (ETXTBSY on Linux). And the panel's
fallback capability rows (catalog loading/error) now follow the same
default-catalog condition as the loader injection, so an explicitly
overridden marketplace fully replaces the Official tab.

* fix(tui): make the built-in row marker unforgeable

The capability:<id> source string was the trust signal for routing rows
into capability installs, but any catalog can write that string — a
custom marketplace could smuggle a row past the third-party trust path
into an official runtime install. Injected rows now carry an internal
builtIn flag that the field-by-field catalog parser never produces;
rendering and install routing key on the flag, and the source string is
purely diagnostic.

* fix(agent-core-v2): include MCP server enablement in capability readiness

A user who disabled the kimi-cu stdio MCP server (/plugins mcp disable)
got a ready capability with no Computer Use tools in new sessions: the
plugin step only checked the plugin toggle, and installPlugin's upsert
preserves per-server state. Readiness now requires every declared MCP
server enabled (reporting e.g. mcp 0/1 enabled), and setup re-enables
disabled servers alongside the plugin toggle.

* fix(agent-core-v2): shell-quote ditto paths in the elevated KimiCU copy

The elevated fallback escaped paths only for the AppleScript string
delimiters, not for the /bin/sh command line inside do shell script: a
TMPDIR with spaces broke the install, and shell metacharacters in the
temp path could inject commands into an administrator-privileged script.
Paths are now POSIX single-quoted first, then the assembled command is
AppleScript-escaped.

* fix(agent-core-v2): never break a working KimiCU on a failed update

The reinstall stopped and uninstalled the old service before the
downloaded archive was unpacked: a corrupt or captive-portal zip then
tore down a previously ready setup. The archive is now staged and
unpacked first, and the app step additionally requires the bundle's
Info.plist, so a partially copied bundle reads as missing and gets
re-copied instead of failing registration against a corrupt bundle.

* fix(agent-core-v2): limit the fetch deadline to the header phase

The 30s AbortSignal stayed attached for the whole request, so a
slow-but-healthy download of a large archive was aborted at 30s total
even while chunks kept arriving — exactly what the per-chunk idle
watchdog was meant to allow. The header phase now uses an
AbortController cleared once headers arrive; the body remains governed
by the inactivity watchdog alone.

* test(tui): provide the harness plugin facade in the capability command fakes

The lazy-session refactor routes session-less plugin calls through
host.harness; the fake host now mirrors that shape.
2026-08-04 17:59:16 +08:00
qer
da6646bf57
docs(changelog): fix 0.32.0 entry formatting and doc links (#2598) 2026-08-04 17:02:08 +08:00
7Sageer
278b6af19d
fix(agent-core-v2): make MCP initial connect non-blocking during startup (#2586)
* fix(agent-core-v2): make MCP initial connect non-blocking during startup

* Delete .changeset/mcp-nonblocking-startup.md

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

* fix(agent-core-v2): wait for MCP readiness before first turn

* fix(klient): wait for MCP startup before listing

* fix(klient): keep MCP server listing non-blocking

* test(acp-server): allow pending MCP snapshot

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-08-04 16:47:55 +08:00
qer
85e4cf0346
docs(changelog): shorten the 0.32.0 loop_control and token_counting entries (#2595)
* docs(changelog): shorten the 0.32.0 loop_control and token_counting entries

* docs(changelog): tighten the 0.32.0 loop_control and token_counting entries further

* docs(changelog): reword 0.32.0 Polish entries from the user perspective
2026-08-04 16:41:59 +08:00
qer
c2e53aef6c
docs: polish the 0.32.0 changelog and fill 0.32.0 doc gaps (#2594)
* docs: polish the 0.32.0 changelog and fill 0.32.0 doc gaps

* docs(changelog): note SessionEnd archive and KIMI_TOKEN_COUNTING_STRATEGY for 0.32.0

* docs(changelog): move the 0.32.0 token_counting entry from Features to Polish
2026-08-04 16:31:25 +08:00
liruifengv
2c3c5a5879
feat(tui): lazy-create the session on first use with the v2 engine (#2458)
* feat(tui): lazy-create the session on first use with the v2 engine

Start the interactive TUI session-less under the v2 engine and create the
session on first use (message, bash input, or a session-requiring slash
command) instead of at startup. Skills and plugin commands are resolved
from the workspace/app-global catalogs without a session, and the footer
shows config defaults (model, permission, plan mode, thinking effort,
context cap) until the session exists.

* fix(tui): serialize lazy session creation and carry session-only thinking

Concurrent first-use triggers (double Enter, a slash command right after a
prompt) both observed `session === undefined` and created their own
session, letting the later setSession close the first one mid-dispatch.
Share an in-flight creation promise instead.

A session-only thinking choice (model picker Alt+S) made before the first
session exists only updated appState, so the lazy-created session fell back
to the engine default while the footer showed the chosen effort. Carry it
as a first-session thinking override and clear it on creation.

* fix(tui): don't re-enter plan mode when creating the lazy session

The v2 engine applies config.defaultPlanMode at session create time
(sessionLifecycleService), so passing the pre-filled appState.planMode as
the create override entered plan mode twice and threw 'Already in plan
mode' on the first message. Pass only the explicit CLI --plan intent for
session-less v2 creation; the config default stays footer-only.

* fix(tui): re-check the busy gate after lazy shell startup, keep /settings session-less

A bash command submitted while the first prompt is still being
lazy-created shared the in-flight creation promise but resumed past
handleUserInput's busy check, running runShellCommand concurrently with
the prompt. Re-check streamingPhase after the await and queue instead.

/settings is a local settings entry point: opening it must not require or
create a session, so it no longer triggers lazy creation; its
session-requiring sub-items keep their own errors.

* fix(tui): re-check busy state after lazy command creation, make /plugins session-free

A skill/plugin or idle-only slash command submitted while the first prompt
was still being lazy-created shared the in-flight creation promise but
resumed past the availability check resolved before the await, running
concurrently with the prompt's turn. Re-check the busy gate after the
shared await in the skill, plugin-command and builtin dispatch paths.

/plugins is app-global on the v2 engine, so a session-less startup no
longer creates a session (or fails with LLM-not-set) just to manage
plugins: the harness now exposes the global plugin API and the command
routes through it until a session exists.

* fix(tui): keep the read-only /add-dir forms session-less

The bare and `list` forms already tolerate a missing session, but the
blanket lazy-create gate forced a session (or failed with LLM-not-set)
before they could run. Only the path-adding form needs a live session, so
it now lazy-creates inside the handler instead of in the dispatch
preflight.

* fix(tui): reflect pending dirs in /add-dir list, refresh plugin commands after reload

/add-dir list looked only at session.summary, so pending startup
additionalDirs were reported as absent until a session existed; fall back
to appState when session-less.

/plugins reload updated the app-global service but left the TUI's plugin
slash-command map stale, so new or re-enabled commands kept parsing as
prompts; rebuild it from the reloaded service (which also covers the
app-global path before the first session exists).

* test(tui): add pluginCommandMap to the MessageDriver interface

* fix(tui): hydrate lazy defaults on sessionless reload, guard /plan re-entry

/reload refreshed only the model/provider dictionaries while session-less,
so defaults edited externally (or a newly added default model) left
appState.model empty/stale and the first lazy-created session failed with
LLM-not-set or used the old value. Reuse the startup default-hydration
path for the no-session reload case.

/plan on as the first command with defaultPlanMode=true re-entered plan
mode after the engine had already applied the config default at create,
throwing Already in plan mode. Skip the call when the session is already
in the requested mode.

* test(tui): widen the getConfig mock return type for the reload case

* fix(tui): clear stale lazy defaults when the default model disappears

hydrateLazyConfigDefaults only patched model when the reloaded config
still had a default, so removing defaultModel from config.toml left
appState.model stale and the first lazy-created session passed it
explicitly instead of failing or following the engine's current defaults.
Reset model and context cap when the default is gone.

* fix(tui): reset a removed permission default, don't re-enter plan on --plan

A removed defaultPermissionMode left appState carrying the old elevated
mode, which createSessionFromCurrentState then passed explicitly to the
first lazy-created session; reset to manual when no CLI permission flag
is present.

With both defaultPlanMode and --plan set, the create payload passed
planMode: true even though the engine already entered plan mode from the
config default, throwing Already in plan mode on the first prompt. Track
the config default separately and suppress the --plan override when it is
already active.

* fix(tui): keep read-only and mode commands session-less

Read-only views (/status, /usage, /mcp, bare /title) already degrade
gracefully without a session, so forcing lazy creation made them fail
with LLM-not-set or create an unused persisted session; they no longer
trigger creation, and /title <name> lazy-creates only for the mutation.

/permission, /auto and /yolo are pending-mode choices: with no session on
v2 they now record the mode in appState (which the lazy create path
passes to the engine) instead of failing or creating a session just to
pick a mode. The runtime permission is applied once a session exists.

* fix(tui): wait out lazy assembly in ensureSession, expose workspace MCP

setSession assigned host.session mid-assembly, so a follow-up prompt or
command in that window skipped the shared creation promise and dispatched
against a session whose subscription and runtime sync had not finished.
Check the in-flight promise before the assigned-session fast path.

/mcp required a live session even though the v2 connection set is
workspace-scoped, so it errored until an unrelated prompt created one.
Route it through a workspace-level MCP view (harness passthrough over the
handler's shared connection manager) before the first session exists.

* fix(tui): await workspace MCP readiness in the session-less /mcp list

* fix(tui): hydrate the model default thinking effort for the session-less picker

* style(tui): trim verbose comments in the lazy-default fixes

* fix(tui): hydrate lazy defaults after login, serialize /new with lazy creation

* fix(tui): re-check the busy gate after lazy /add-dir session creation

* fix(tui): let Shift-Tab lazy-create the session on a v2 session-less start

* fix(tui): hydrate session-less defaults on model-less login, refresh workspace commands on reload

* fix(tui): re-check the idle-only gate for /new after waiting out lazy creation

* fix(tui): wait out lazy creation before accepting model/effort switches

* fix(tui): wait out lazy creation before switching sessions from the picker

* fix(node-sdk): read session-less skills from the workspace skill catalog

* feat(tui): show a session-less notice on v2 startup
2026-08-04 16:09:01 +08:00
7Sageer
af43c5226d
feat(agent-core-v2): advertise model capabilities in the subagent model picker (#2591)
* feat(agent-core-v2): advertise model capabilities in the subagent model picker

* chore(agent-core-v2): follow header-only comment convention

* docs(agent-core-v2): note the model catalog collaborator in the AgentSwarmTool header
2026-08-04 15:57:04 +08:00
7Sageer
54c04bf03d
feat: /fork no longer switches to the forked session (#2565)
* feat: /fork no longer switches to the forked session

Forking used to switch to the new session, which closed the source
session and force-stopped its background tasks (and canceled any
in-flight turn). /fork now creates the copy and stays in the current
session; the fork can be opened explicitly via /sessions.

* fix(tui): release fork runtime when staying current
2026-08-04 15:24:12 +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
qer
6cb615cd37
chore(changeset): downgrade token-counting strategy entry to patch (#2590) 2026-08-04 15:06:54 +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
f412e105b3
feat(acp-server): bridge questions via elicitation and support host slash commands (#2583)
* fix(acp): preserve cancels that arrive before the turn id is known

A session/cancel landing between prompt submission and the launch
round-trip found driver.turnId undefined and was dropped entirely; the
turn then ran to completion and the prompt resolved end_turn despite
the client's cancel.

The engine's cancel payload makes turnId optional (an empty call
cancels the active turn — the same contract kap-server's cancel route
relies on), so cancel() now issues an unaddressed cancel in that window
and flags the driver; the launch handler re-issues a precisely
addressed cancel once the id lands, and a no-launch outcome settles
cancelled instead of end_turn.

* fix(agent-core-v2): shut session MCP overlays down on service teardown

The ephemeral per-session MCP overlay was only shut down by the
session handle's dispose wrapper, but the DI container disposes session
scopes directly on workspace/app teardown, bypassing the wrapper — so
overlays of sessions still live at shutdown leaked their MCP
connections and stdio child processes.

Track live overlays in the lifecycle service: the handle wrapper
deletes-then-shuts-down (atomic, so close and service disposal can
never double-shutdown), and the service's own dispose shuts down
whatever is still tracked.

* feat(acp-server): bridge questions via elicitation and support host slash commands

- route AskUserQuestion through `elicitation/create` for form-capable
  clients (native multi-question + multi-select), falling back to the
  `request_permission` bridge on RPC failure
- add a `slashCommands` resolver option so hosts can merge their own
  command palette and skill aliases into `available_commands_update`;
  `/help` now lists the merged palette
- bridge `appendText`/`writeBytes` through client text capabilities
  (read-modify-write append, UTF-8-checked byte writes) with local
  filesystem fallbacks
- defer `available_commands_update` until after the lifecycle response
  settles so clients like Zed do not drop the notification
- propagate plan-toggle errors from `setMode` instead of silently
  reporting the new mode; make server `close()` idempotent

* style(acp-server): satisfy oxlint eqeqeq and await-thenable rules

* test(node-sdk): assert v1-v2 tokenCount parity for imports after eager counting
2026-08-04 10:44:56 +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
7Sageer
c27a9f93a6
feat(agent-core-v2): add AGENTS.md discovery reminder (#2545)
Some checks are pending
CI / typecheck (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 / 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
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core-v2): add AGENTS.md discovery reminder behind experimental flag

When a tool call touches a directory whose AGENTS.md was not part of the
injected instructions (the init-time load only covers the project-root to
cwd chain), append a system reminder to the tool result suggesting the
model read it, at most once per file per agent.

The new agentsMdReminder domain (Agent scope, gated by the
agents-md-reminder experimental flag, default off) hooks
toolExecutor.onDidExecuteTool: Read/Edit/Write contribute their path,
Glob/Grep their search root, and Bash its structured cwd plus literal
directory operands extracted from the command's syntax tree via the
in-repo bash parser (listing commands only, bare names, conservative
skips). Probing walks projectRoot to the touched dir with the same
candidate rules as the init-time load (shared helpers in
profile/context), skipping fully-known directories and blank files.
The known-set is seeded by profileService after each successful
bind/apply/refresh and by sessionInit after /init, claimed
synchronously per discovered file, and published as a whole value only
after the reminder is attached, so parallel calls never duplicate a
reminder and failures leave files eligible for the next touch.

* fix(agent-core-v2): keep the AGENTS.md reminder on visible results and seed restored agents

A same-step duplicate vetoed by toolDedupe carries a placeholder result
that the dedupe hook swaps for the original's deferred result; attaching
the reminder there discarded it while the file was already counted as
reminded, and the telemetry still claimed it was shown. Skip the
placeholder (the call id sits in toolDedupe.syntheticCallIds until the
dedupe hook consumes it) so the reminder, telemetry, and known-set only
advance on results that reach the model; the original call then carries
the reminder for both by the time the deferred resolves.

Session resume and forks commit an already-rendered system prompt
(AGENTS.md content included) without going through bind/apply/refresh,
so no seed point fired and the known-set lagged behind the injected
set, producing false "not part of the injected instructions"
reminders. The first qualifying call of a never-seeded agent now
re-runs the init-time discovery with the same inputs (agent cwd, os
home, brand home) and seeds from it, once per agent; a discovery
failure leaves the agent unseeded so the next touch retries.

* refactor(agent-core-v2): fold agentsMdReminder inline rationale into file headers

The package comment convention keeps rationale in the top-of-file block
only; move the inline blocks' unique increments there (hook-order
fallback mechanics, synthetic-key existence condition, frozen-vs-live
Bash base, operand-less vs failed-resolution listings) and derive
AGENTS_MD_BASENAMES from AGENTS_MD_PLAIN_NAMES so the candidate names
stay single-sourced.

* fix(agent-core-v2): use resolved accesses for AGENTS reminders

* feat(agent-core-v2): drop the agentsMdReminder experimental gate

* fix(agent-core-v2): harden AGENTS reminder tool outcomes

* fix(agent-core-v2): preserve actual tool execution outcomes

* Persist AGENTS.md paths across profile restoration

* test(agent-core-v2): update useProfile snapshot for agentsMdPaths
2026-08-03 22:02:01 +08:00
7Sageer
98ef0f0b2f
fix(agent-core): replay v2 profile.bind records so resumed sessions keep their tools (#2567)
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-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
* fix(agent-core): replay v2 profile.bind records so resumed sessions keep their tools

Sessions created by the v2 engine (CLI 0.31+, wire protocol 1.5) persist
the profile binding, including the tool allowlist, as a profile.bind
record. The v1 replay path had no branch for it and silently dropped the
record, so a session resumed by a v1 host (e.g. the VS Code extension via
kimi-code-sdk) never called setActiveTools and sent requests with no
tools at all (observed server-side as tools_count=0; the model emits
reasoning only and stops with empty content).

v1 replay now maps profile.bind onto config.update + setActiveTools when
activeToolNames is an array, skips the record otherwise so the
resume-time default-profile fallback still applies, and treats
tools.reset_active_tools as a no-op.

* fix(vis): handle v2 profile records in context projection

* fix(agent-core): avoid synthetic replay for v2 profile binds

* fix(vis): render v2 profile wire records
2026-08-03 20:59:46 +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
75395f6abb
feat(agent-core-v2): add lifecycle hook events and enrich hook payloads (#2558)
* feat(agent-core-v2): add lifecycle hook events and enrich hook payloads

New hook events:
- TurnStarted: fired from the turn.started bus event, covering queued
  turns, stop-hook continuations, and background/system turns that
  UserPromptSubmit misses
- UserPromptQueued: fired when a prompt cannot launch immediately,
  carrying the queue length
- TaskStarted: fired from the existing task.started bus event, so
  background tasks no longer only produce a completion-time Notification
- SessionHeartbeat: per-session 60s liveness beat, armed only when the
  event has hooks registered, letting hook consumers distinguish a
  session hanging on a long permission wait from a crashed one

Payload enrichment:
- client_type (host platform identity) on every event
- session_title on every session/agent-scoped event
- model and profile on SessionStart
- SessionEnd reason is now 'exit' or 'archive' instead of a hardcoded
  'exit'
- SubagentStart/SubagentStop now carry session_id/cwd like every other
  event

* fix(agent-core-v2): re-sync SessionHeartbeat timer on hook-index reloads

The heartbeat timer was armed once after the runner's initial load, so a
SessionHeartbeat hook contributed later by a plugin reload never produced
beats for existing sessions. The runner now exposes onDidReload (fired
after every index build), and the session adapter re-syncs on it: arming
when a heartbeat hook appears, disarming when none remains.

* fix(node-sdk): keep the v1 PluginInfo contract assignable with v2-only hook events

The v2 hook-event union is now a superset of v1's, which broke the
node-sdk type projection in two places:

- the klient contract's hookDefSchema rejected plugin manifests using
  the new events (TurnStarted, UserPromptQueued, TaskStarted,
  SessionHeartbeat) at validation time — accept them
- getPluginInfo returned the v2 PluginInfo where the SDK contract
  promises the v1 shape — project manifest.hooks through the v1-known
  event list (read from the legacy HookDefSchema), mirroring how the
  config mapper drops domains v1 does not know
2026-08-03 17:14:34 +08:00
qer
dfc55a5c97
fix(tui): make the /login already-logged-in notice visible (#2559)
The "Already logged in. Model configuration refreshed." confirmation was
rendered with the default dim text color, so users easily missed it and
assumed /login did nothing. Render it with the theme's success color,
matching the success styling used by the login spinner's "✓ Logged in."
line.

Co-authored-by: Mira Bot <mira-bot@moonshot.cn>
2026-08-03 16:37:55 +08:00
7Sageer
29c9e2ab20
docs: clarify secondary model default binding and override precedence (#2553)
The secondary_model section did not state whether spawned subagents are
forced onto the secondary model or only default to it, nor the full
override precedence. Make the semantics explicit in both locales:

- spawning resolves the model in order: explicit tool-call model ->
  profile model_preference -> configured secondary model (default)
- the tool's model parameter accepts only "primary" / "secondary"
- "primary" means the model the main agent is currently running, not
  necessarily default_model
- the user has no per-spawn switch; overriding is the main agent's
  decision or a profile setting

Also unify secondary-model terminology and the [models] alias wording
across the config-files, agents, slash-commands, and env-vars pages.
2026-08-03 15:42:09 +08:00
Haozhe
e6a655e101
feat(agent-core-v2): add lifecycle ledger, dynamic registry, and cascade engine (#2551)
* feat(agent-core-v2): add lifecycle ledger, dynamic registry, and cascade engine

- add `_base/lifecycle` Ledger: ordered registrations with strict reverse
  serial teardown, sync/async dual-track disposers, uninterruptible rollback,
  effect return forms, child ledgers, introspection tree, and teardown-reason
  propagation
- delegate Disposable/DisposableStore/MutableDisposable, Scope.dispose, and
  InstantiationService.dispose to the ledger; retire _constructionOrder
- ServiceCollection: entries carry uid/pinned/recipe, delete(), and a
  per-token availability event; add provide/unprovide to IInstantiationService
- add a persistent dependency graph recording constructor-injection edges
  with affectedSet/topo queries, plus a per-container cascade engine
  (contagion teardown/rebuild transactions, five unit states, pending index,
  abort hook, history ring); TestInstantiationService.set routes via provide

* fix(agent-core-v2): cascade instance replacements, tolerate abort rejections

- provide/unprovide of pre-materialized instances now runs as a cascade
  transaction too, so live dependents are torn down and rebuilt instead of
  holding a stale dependency; re-affirming the live instance stays a no-op
- a rejecting onWillCascade promise is logged and the cascade proceeds
  (best-effort), matching the synchronous-throw handling

* feat(agent-core-v2): propagate cascades across scopes along instance edges

Implements the revised D9 (the firewall design was dropped): instance edges
are scope-tagged on both ends and point child -> parent only, the dependency
graph is shared by the whole scope tree, and each change runs as one
tree-wide transaction orchestrated by the submitting scope's engine —
contagion computed over the global graph, teardown in global reverse
topological order (deepest first), rebuild in global topological order, with
each scope's engine executing its own units. Descendant scopes dying
mid-transaction are skipped idempotently; the request queue, in-flight set,
and settle waiters are tree-shared so cross-tree transactions are serialized
by the orchestrator; shadowed tokens stay outside the contagion set.
2026-08-03 15:40:40 +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
liruifengv
e22479a62e
feat(kap-server): expose effective experimental flags in /meta (#2417)
Some checks failed
CI / build (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test (5) (push) Has been cancelled
CI / test-pi-tui (push) Has been cancelled
CI / test-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
Release / Release (push) Has been cancelled
Nix Build / Check flake.nix workspace sync (push) Has been cancelled
Release / Deploy docs (push) Has been cancelled
Release / Native release artifact (push) Has been cancelled
Release / Publish native release assets (push) Has been cancelled
Nix Build / nix build .#kimi-code (push) Has been cancelled
2026-08-01 10:22:23 +08:00
Sampson
a5960b3905
test(agent-core-v2): disables commit.gpgsign (#2475) 2026-08-01 10:16:51 +08:00
StaR4y
bfa00807c9
fix(web): correct dark monochrome composer styling (#2083)
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-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Nix Build / Check flake.nix workspace sync (push) Waiting to run
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
Co-authored-by: StaR4y <star4y@origin.pw>
Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-31 22:38:45 +08:00
qer
7648874730
docs(changelog): sync 0.31.1 from apps/kimi-code/CHANGELOG.md (#2470) 2026-07-31 20:21:53 +08:00
Mangesh Raut
eaab2b6f28
fix(cli): fall back to built-in models.dev catalog when fetch fails (#2416)
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-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Nix Build / Check flake.nix workspace sync (push) Waiting to run
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
When the remote models.dev catalog cannot be fetched, fall back to the
built-in catalog so CLI/TUI model selection keeps working offline or
under network failure. Import the shared helper via the #/utils alias.
2026-07-31 19:54:09 +08:00
github-actions[bot]
6b56c11697
ci: release packages (#2403)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-31 19:29:29 +08:00
qer
326e1fb6ce
fix(web): restore chat code block styling after the markstream upgrade (#2459)
* fix(web): restore chat code block styling after the markstream upgrade

markstream-vue 1.0.9 renders shiki code blocks through stream-diffs
(code-editor-container) instead of <pre>, and drives font size, line
height, and font family from monacoOptions applied as inline styles.
With only lineNumbers passed, blocks fell back to 12px/18px in the
inherited proportional UI font, and the pre/copy-button/content CSS
overrides in Markdown.vue no longer matched anything.

Pass fontSize/lineHeight/fontFamily/padding through codeBlockProps
monacoOptions (the only channel that reaches the shiki renderer),
retarget the dead overrides to code-block-shell-content and
code-action-btn, and hide the loading fallback's hardcoded line-number
gutter so the highlight upgrade no longer shifts layout.

* fix(web): keep code block metrics in sync

* fix(web): pin chat code font to 13px and align the loading fallback

Tying the code font size to the Appearance UI font size setting changed the
default rendering from the 13px design token to 14px and broke the 15/14/13
type scale; revert to the fixed token size. Keep the fallback ↔ settled
alignment: the restored monaco padding option feeds the fallback pre's
inline padding (12px, ignored by the shadow-root renderer), and a relative
1.65 line-height with !important beats the inline 1.5x default upstream
stamps on the fallback.

* chore(web): pin markstream-vue and its renderer stack to exact versions

The markstream family ships breaking rendering changes in patch releases
(1.0.8 swapped the code-block engine and renamed its DOM classes), so a
floating caret range hands merge control to upstream. Pin markstream-vue,
stream-diffs, stream-markdown and stream-monaco exactly — upgrades become
deliberate actions with a visual check, same posture as @chenglou/pretext
in the same file.
2026-07-31 19:14:02 +08:00
7Sageer
1f3f5dadaa
feat(agent-core-v2): interruption reminder for user-cancelled turns (#2400)
* feat(agent-core-v2): interruption reminder for user-cancelled turns

When the user interrupts a turn with Esc, append a durable
<system-reminder> (origin: injection/interruption) to the agent context
via a new loop aspect watching turn.ended, so the model learns the
previous turn was deliberately cut off. The marker persists to the
wire, replays on resume, stays hidden from transcripts, skips non-user
aborts and steer, and does not stack on repeated cancels.

Two supporting fixes:

- An aborted LLM stream now persists its accumulated partial
  text/thinking as content.part loop events instead of dropping every
  produced token; gated on the turn signal so retried or
  step-cancelled attempts keep their partial output out of the record.
- The turn.cancel wire op carries an optional reason
  ('user_cancelled' | 'aborted') so cold readers can tell deliberate
  interrupts from programmatic aborts. Goal-lifecycle cancels now pass
  an explicit programmatic reason to keep that field honest.

* feat(transcript): mark user-cancelled turns with an interruption marker

Project the deliberate user interrupt onto the transcript timeline: the
live projector emits an 'interruption' marker when a turn ends with
interruptReason 'user_cancelled', and the cold fold consumes the
persisted turn.cancel reason into the same marker. Programmatic aborts
keep surfacing through their own outlets (errors, goal/task state), and
queued cancels that left no visible residue are skipped.

* fix(agent-core-v2): make user-turn cancellation idempotent and reconcile interruption reminders on restore

* fix(transcript): dedupe user-cancelled interruption markers by turn in the cold fold

* chore(agent-core-v2): regenerate state manifest after merging main

* refactor(agent-core-v2): split interruptionReminder out of the loop domain

The loop domain owns turn execution mechanics; whether an interrupted turn
should produce a model-visible reminder is a model-context policy. Move it
into its own L4 domain with its own wire model that cross-reduces the
loop's turn.cancel fact, and rename the op to interruptionReminder.recorded.

---------

Signed-off-by: Haozhe <yanghaozhe@moonshot.ai>
Co-authored-by: Haozhe <yanghaozhe@moonshot.ai>
2026-07-31 18:17:14 +08:00
Rick
302b2cd680
fix(vscode): all AskUserQuestions should be answered and added to the context (#2326)
Co-authored-by: rickgao <rickgao@tencent.com>
Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-31 17:40:12 +08:00
Haozhe
44d34bbd56
refactor(agent-core-v2): move host runtime args onto IBootstrapService (#2460)
* fix(agent-core-v2): resolve package self-references in check-import-boundaries

Imports spelled @moonshot-ai/agent-core-v2/<path> (the legal `./*` export
self-reference) were treated as external packages, letting kosong layer
violations through that spelling pass the checker.

* refactor(agent-core-v2): move host runtime args onto IBootstrapService

- Add HostArgs under BootstrapInput.args / IBootstrapService.args
  (agentFiles, skillDirs, requestHeaders, displayName, replyStyleGuide),
  mirroring VS Code's NativeParsedArgs on the environment service
- Remove the narrow per-domain runtime-options services and their seed
  functions: IAgentCatalogRuntimeOptions, ISkillCatalogRuntimeOptions,
  IHostIdentity
- Reduce IHostRequestHeaders to a pure kosong port contract and bridge it
  from bootstrap args via a new app/kosongConfig adapter, keeping kosong
  free of app-layer imports
- Pass host args through bootstrap() at the composition roots (kap-server,
  v2 print CLI, node-sdk) instead of seeding services
- Persist SDK provider removal as one atomic multi-section config replace

* fix(config): persist provider refresh updates atomically

- expose atomic multi-section config replacement through klient and SDK
- stage provider removals before one atomic write in TUI refresh
- briefly drain startup refresh during shutdown
2026-07-31 16:47:34 +08:00
Haozhe
4c4df1bb05
feat(agent-core-v2): persist the terminal turn.ended wire record (#2457)
* feat(agent-core-v2): persist the terminal turn.ended wire record

- add a persisted turn.ended op (turnId, reason, error, durationMs)
  dispatched from the loop's runTurn finally block, alongside the event
- fold the record back in the transcript cold rebuild: terminal state
  (blocked folded into failed, mirroring the live wire edge), durationMs,
  error message and endedAt; journals without the record keep the
  grouping default
- restrict the test harness's snapshot waiters to emit entries so the
  same-named wire record no longer shadows the turn.ended event

* chore(agent-core-v2): stabilize unique-symbol keys in the state manifest

The checker names a unique symbol key __@name@NNNN, where NNNN is a
compilation-global counter that shifts with unrelated type additions and
churns the generated manifest. Render the stable __@name form instead.

* fix(transcript): map turn.ended around hidden turns in the cold fold

RetryStepRequest opens a real engine turn with origin 'retry' but
contributes no context messages, and a queued-then-cancelled
reservation consumes an engine id without starting. Both make engine
turn ids drift from the grouping ordinals, so matching turn.ended by
ordinal could stamp a later visible turn with the wrong terminal state.
Replay the loop's turn-clock records (turn.prompt / turn.cancel) and
map engine ids to ordinals past the hidden ids; the hidden turns' own
end records map nowhere and are dropped.
2026-07-31 16:25:43 +08:00
liruifengv
32d693f644
feat(tui): ask for workspace trust on startup with the v2 engine (#2453)
* feat(node-sdk): expose workspace trust state and trust grant on the v2 client

* feat(tui): ask for workspace trust on startup with the v2 engine
2026-07-31 14:21:04 +08:00
Haozhe
071d56940f
refactor(agent-core-v2): remove the L0-L7 domain layering and clean up comment conventions (#2451)
* refactor(agent-core-v2): move workspace-domain internals into internal/ dirs

- move sessionLifecycle/addressing into internal/
- move workspaceFs errors/fsProcess/fsSearch/rgLocator/runRg into internal/
- move workspaceMcpConfig/config-loader into internal/
- update import paths in services, index.ts, and tests

* docs(agent-core-v2): strip non-header comments, make headers file-local

- remove all symbol JSDoc and inline // and /* */ comments under src/;
  file-top header comments and directive comments (eslint, @ts-*) are kept
- rewrite file headers to describe only what the file itself does, dropping
  references to other files and see-X / lives-in / consumed-by relationships
- no code changes

* refactor(agent-core-v2): remove the L0-L7 domain layering and its lint guard

- replace check-domain-layers.mjs with check-import-boundaries.mjs,
  keeping only the v1-import ban and the kosong subtree rules
- rename the lint:domain package script to lint:imports
- drop the (Ln) layer label from every file-header identity line
- update AGENTS.md, package docs, and the agent-core-dev skill to match
2026-07-31 13:45:33 +08:00
Bowen Liang
e111c878fd
fix(web): unify permission mode order and risk colors across settings surfaces (#2125)
* web: reorder default permission options in Agent settings

Align the Agent settings default-permission segmented control with the
Composer toolbar order, arranging modes from safest (manual) to most
permissive (auto).

* web: reorder mobile permission cycle from safest to most permissive

The mobile settings sheet still cycled manual → auto → yolo, jumping from
the safest mode straight to the most permissive one on a single tap. Align
the tap-to-cycle order with the Composer menu and Agent settings
(manual → yolo → auto).

* web: align permission risk colors with the Composer's progression

Both the desktop status panel and the mobile settings sheet mapped
yolo → danger and auto → warning, the inverse of the Composer menu
(yolo → warning, auto → danger). Since auto is the most permissive
mode, it should carry the danger color everywhere.

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-31 13:28:05 +08:00
7Sageer
95a656ca61
fix(agent-core): strip the no-op subagent model parameter while the secondary-model experiment is off (#2449)
The Agent/AgentSwarm tool schemas always advertised a \`model\` choice
parameter, so the secondary-model concept entered the prompt even with
the experiment disabled. Gate the advertised JSON schema on the flag in
both engines: off (the default) drops the parameter, on keeps it, and
spawn-time resolution already falls back to the caller's model either
way.

Also scrub ambient KIMI_CODE_EXPERIMENTAL_* env vars in both packages'
vitest setup so flag-dependent tool schemas in llm.tools_snapshot stay
deterministic regardless of the developer shell.
2026-07-31 13:09:56 +08:00
Haozhe
ed7a4cc095
feat(kap-server): add session-less POST /workspace/fs:search route (#2437)
* feat(kap-server): let fs:search resolve a workspace ref for draft sessions

- fs:search accepts a workspace id or absolute root in the session_id slot
  so the @ file mention works before the session exists
- kimi-web searchFiles falls back to the active workspace id in draft state

* fix(agent-core-v2): report empty thinking level for unbound main agent

- sessionLegacyService.status returns thinking_level '' when the main
  agent has no bound model (mirroring model: undefined), so clients
  fall back to the catalog default instead of folding in the wire
  model's 'off' zero value
- add regression test for a never-bound main agent status
- add web changesets: draft @ file mention, new-session thinking level

* perf(minidb): make text index rebuilds async and non-blocking

- TextIndex.build() yields to the event loop during tokenization and
  batches postings writes (~1 MiB), so large rebuilds no longer
  hard-block the host process
- writes landing mid-build are queued and replayed onto the new base at
  swap time, keeping the rebuilt index exact
- PostingsFile.rebuildSync renamed to async rebuild with a synchronous
  commit section (beforeRename hook + atomic rename)
- onCompacted hook is now awaited (sync or async); open-time compaction
  runs in the background so open() returns without blocking on the
  snapshot rewrite and postings rebuild
- compaction skips the postings rebuild when the index's write buffer is
  clean (needsRebuild)
- createTextIndex registers before building so concurrent writes feed
  the build queue; dropTextIndex throws while a build is in flight

* refactor(agent-core-v2): rename workspaceHandler to sessionLifecycle

- rename IWorkspaceHandlerService to ISessionLifecycleService and move
  src/workspace/workspaceHandler/ to src/workspace/sessionLifecycle/;
  update all consumers (gateway, sessionExport, sessionLegacy,
  sessionLookup, kap-server, klient, node-sdk, kimi-inspect, kimi-code)
- rename IStateService to IAppStateService and add the Workspace-scope
  IWorkspaceStateService, so the state domain spans all four scope tiers
- add cascading StateRegistry.inspect(): each tier injects the parent
  tier's registry and folds App to current scope into one StateInspection
  tree; check-domain-layers gains a Rule 2b exemption for state-on-state
  imports

* feat(kap-server): add session-less POST /workspace/fs:search route

Carry the workspace reference (registered id or absolute root) in the
request body and resolve it to the same Workspace-scope fs service the
session route uses, so clients no longer borrow the session route's
{session_id} slot. kimi-web's @ file mention now calls this route with
the workspace ref instead of a session id; the session-route fallback
stays for wire compatibility.

* refactor(agent-core-v2): register workspace-scope service state into IWorkspaceStateService

- move workspaceDirs / workspaceInstructions / workspaceSkillCatalog / workspaceTrust
  runtime state from bare instance fields into the workspace state container
- extend gen-state-manifest.mts to scan app/workspace scopes, emitting
  AppStateSnapshot / WorkspaceStateSnapshot alongside Session/Agent
- regenerate docs/state-manifest.d.ts and update AGENTS.md + agent-core-dev skill
- update affected tests to register the state services and assert the new state keys
2026-07-31 12:11:26 +08:00
liruifengv
bb2919eb81
fix(tui): reduce frequent full-screen redraws (#2442)
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-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-07-31 11:35:39 +08:00
Haozhe
17dfd49768
feat(agent-core-v2): introduce the Workspace domain and the agent-profile registry extension point (#2366)
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-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
* feat(agent-core-v2): insert Workspace lifecycle scope and remove mutable cwd paths

- Insert LifecycleScope.Workspace between App and Session
- Delete session/workspaceCommand domain (addAdditionalDir) and node-sdk RPC
- Remove profile cwd mutation; cwd is fixed at creation
- Make ISessionWorkspaceContext read-only; seed additionalDirs at creation
- Remove TUI and vscode /add-dir commands (to return workspace-scoped)

* feat(agent-core-v2): add Workspace scope with handler-owned session lifecycle

- Add IWorkspaceLifecycleService (App scope): handler registry,
  create-or-get handlerFor with inflight join
- Add workspace/workspaceContext seed and workspace/workspaceHandler
  (session create/resume/fork as handler child scopes)
- Delete App-level ISessionLifecycleService; callers compose
  index -> handlerFor -> handler via sessionLookup helpers
- Slim IBootstrapService; persistence addressing via handler chain
  (disk layout byte-identical)
- kap-server routes rewire internally; /api/v1 wire unchanged,
  debug surface gains workspace addressing
- Pin red line in domain lint: session/agent must not import
  workspace domains

* feat(agent-core-v2): collect workspace resources into the handler scope

- Add Workspace-scope catalogs for skills and agent profiles,
  instructions service, and a shared MCP connection manager
  (built at materialization, refreshed by watch/plugin events)
- Session catalogs keep their APIs but read seeded snapshots and
  refresh via change events; ISessionMcpService removed
- Session create options carry no mcpServers; MCP sources are
  config file (wins on name conflicts) and plugins only
- Agent profile/mcp consume the seeded providers

* feat(agent-core-v2): restore add-dir as a workspace-level capability

- Add workspace/workspaceDirs: shared additional-dir set with
  addDir({path, persist}); persist=true writes .kimi-code/local.toml,
  local.toml watch drives cross-process refresh
- ISessionWorkspaceContext becomes a live read view fed by the
  ISessionWorkspaceInfo seed contract and change events
- Restore Session.addAdditionalDir in kimi-code-sdk 1:1, mapping to
  the workspace service; restore TUI/vscode /add-dir verbatim

* feat(agent-core-v2): collect os-level services into the workspace scope

- Move fs service, fs watch (shared subscription fan-out), process
  runner, and a git facade to Workspace scope; sessionFs domain removed
- Add IWorkspaceToolPolicy with workspace veto wired through tool
  activation, execution guard, composed evaluation, and profile
  prompt projection; injected via ISessionToolPolicyGate seed
- kap-server fs routes and fs.watch bridge remap to the workspace
  services; wire unchanged

* refactor(agent-core-v2): clean up workspace-domain leftovers and docs

- Drop dead code: v2 mergeCallerMcpServers, the transitional
  ISessionContext.additionalDirs field, an unreachable guard
- Fix stale domain references in comments; correct test names
- Give the fs-watch refresh test a realistic wait budget under load
- Document the four-scope model and workspace domain in AGENTS.md,
  agent-core-v2 docs, and the agent-core-dev skill

* test(node-sdk): wait for the initial MCP connect to settle in the parity list test

v1 connects in the background after create resolves while v2 awaits it
inside create, so an immediate list can catch either side still pending
under CI load

* refactor(agent-core-v2): extract git work-tree discovery into the git domain

- add the pure findGitWorkTree probe in app/git/workTree and expose it
  as IGitService.findWorkTree
- switch the git permission policies off the local
  findLocalGitWorkTreeMarker helper to the DI service
- reuse findGitWorkTree for AGENTS.md project-root discovery in
  agent/profile/context.ts
- add findWorkTree coverage to gitService.test.ts

* feat(kimi-inspect): add Workspace Services view

- add WorkspaceServicesView rail view with a workspace picker on top;
  proxies resolve workspace-scope Services on the /workspace/:id route
- extend ChannelScope, ServiceTarget, and ServicePanelDef scope with
  'workspace', routed via client.workspace(id).service
- wire the new view into NavRail and App

* refactor(agent-core-v2): extract mcpCore and workspaceMcpConfig domains

- move the scope-agnostic MCP connection layer (stdio/http/sse clients,
  connection manager, oauth, config schema, tool naming) from agent/mcp
  to the new mcpCore domain
- move the [mcp] config section to app/mcpConfig and OAuth credential
  persistence to app/mcpConfig/oauthStore
- introduce the workspace/workspaceMcpConfig domain owning the effective
  MCP server set (mcp.json files + plugin contributions, refreshed by
  fs watch); workspaceMcp keeps pure connection orchestration
- update the plugin domain, session MCP handle, klient/node-sdk
  contracts, and tests accordingly

* refactor(agent-core-v2): remove the fault-injection experimental feature

- delete the faultInjection domain (flag definition, IFaultInjectionService
  contract, FaultInjectionService implementation)
- drop the requester-side take() injection point and the constructor
  dependency from llmRequester
- remove the flag-gated test cases and the IFlagService stub they needed
- regenerate the state manifest without the faultInjection state keys

* feat(agent-core-v2): gate project-level MCP config behind workspace trust

Add the Workspace-scope IWorkspaceTrust service: an explicit, per-workspace
trust marker persisted under the home (IAtomicDocumentStore, keyed by
encodeWorkDirKey(root)) so a checked-out tree cannot pre-trust itself.
While a workspace is untrusted, workspaceMcpConfig skips the project-level
.mcp.json and .kimi-code/mcp.json files (user-level config and plugin
contributions still load); a trust flip reuses the reload path, so project
servers connect on trust and disconnect on untrust.

Expose the state over kap-server REST: GET /workspaces/{id}/trust,
POST /workspaces/{id}/trust, POST /workspaces/{id}/untrust.

* feat(kimi-inspect): replace the workspace picker with a directory browser

The Workspace Services view now keeps a server-side directory browser in a
left sidebar (over IHostFolderBrowser) instead of a <select> of registered
workspaces. Entries that are registered workspaces carry a workspace badge
plus their IWorkspaceTrust trust state; selecting an unregistered folder
registers it on demand via IWorkspaceService.createOrTouch.

* fix(agent-core-v2): resolve the effective cwd into the profile binding

A default-bound agent recorded no cwd in its profile.bind payload, and no
caller configures ProfileServiceOptions.cwd, so the profile service's cwd
getter fell through to '' and refreshSystemPrompt() rebuilt the prompt
from the server process's cwd: an AGENTS.md edit dropped the workspace
instructions (or swapped in unrelated ones).

bind() now persists the resolved effective cwd (the input's, or the
session's when the input omits it) into profile.bind — the Model's cwd
stays creation-fixed and is always set. The getter's last resort is the
session's own cwd (the value legacy bindings resolved against) instead of
a bare ''.

* refactor(agent-core-v2): introduce the contribution/registry/catalog extension point for agent profiles

- App-scope IAgentProfileRegistry: any scope can register an
  AgentProfileContribution keyed by (sourceId, workspaceKey); dedup per
  source id, change events drive catalog re-projection
- workspaceAgentProfileLoader domain owns agent-file discovery end to end
  (parse / roots / SYSTEM.md / explicit runtime files) with five
  Workspace-scope loaders (workspace / user / plugin / extra / explicit)
  tagged with the handler's workspaceId; internals live under internal/
- SessionAgentProfileCatalog projects the registry directly (name dedup,
  priority adjudication, builtin override rule, inspect()); the
  workspace-catalog + sessionData seed relay is gone
- builtin code contributions register as the 'builtin' entry via
  BuiltinAgentProfileLoader; plugin agent roots are provided by the
  plugin domain as PluginAgentRoot
- remove cwd from the profile binding chain (BindAgentInput /
  ProfileBindingSnapshot / AgentConfigData / ProfileModelState /
  profile.bind op) — it is always the session's frozen cwd; legacy
  wire.jsonl records replay fine (the schema strips the field)
- share markdown frontmatter parsing via _base/text/frontmatter

* fix(agent-core-v2): reconcile the workspace refactor with main

- restore the branch's klient workspaceId scope extension lost to a
  file-level conflict resolution (main had no further changes there)
- stub the plugin system-prompt dependencies main added to the profile
  service in the profileOps / skillCatalog tests
- correct PLUGIN_SKILL_SOURCE_ID to the App skillSource domain (Agent
  scope must not import the Workspace domain)
- kap-server workspaceLayout test supplies the now-required hostIdentity
- regenerate wire/state/config manifests

* fix(agent-core-v2): export the agent-file parse primitives the v2 print CLI consumes

The internal/ split kept parseAgentFileText / resolveAgentPath off the
package entry, but apps/kimi-code's v2 print runner imports them from
@moonshot-ai/agent-core-v2 for --agent-file. Export the two symbols by
name; everything else under internal/ stays domain-private.

* feat(agent-core-v2): return cwd listing for empty fs:search query

An empty fs:search query used to fail request validation (query had a
minimum length of 1), so @-mention pickers had no starting set right
after typing "@". The workspace fs service now answers an empty query
with the workspace root's top-level entries — directories first,
hidden entries excluded, gitignore and exclude_globs honored — mapped
into the search-hit shape (score 1, empty match positions) and capped
by limit. The mirrored protocol wire schema is relaxed in sync.

* test: cover cron-fired steer context and titled session creation

- agent-core-v2: e2e asserting a cron-fired steer turn carries earlier
  tool results (the CronCreate job id) into the provider request
- klient: conformance case creating a titled session through implicit
  workspace materialization
2026-07-31 00:38:11 +08:00
qer
5c0ec2938a
fix(web): upgrade markstream-vue to 1.0.9-beta.1 and enable Monaco code highlighting (#2415)
* fix(web): upgrade markstream-vue to 1.0.9-beta.1 and enable Monaco code highlighting

* fix(nix): update pnpm deps hash for markstream-vue 1.0.9-beta.1
2026-07-30 20:01:57 +08:00
Haozhe
f1a3475ad5
fix(agent-core-v2): write refresh results in one atomic config transition (#2410)
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-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
* fix(agent-core-v2): write refresh results in one atomic config transition

- add IConfigService.replaceSections: applies several domains in a single
  state transition — one disk write, one effective rebuild, change events
  fire only after all domains took effect
- rework ProviderDiscoveryService to absorb the orchestrator's two-phase
  removeProvider/setConfig host contract into one replaceSections write, so
  the kosong registries never pass through a halfway-removed catalog
- stop writing the env-synthesized __kimi_env__ slice to config; the
  bridge's event-driven sync carries it into the registries on its own
- fixes sporadic "model is not configured" errors when starting kimi web,
  caused by the background refresh transiently clearing the model catalog
  while the first session was being created

* fix(agent-core-v2): stage replaceSections writes before mutating raw config

Validate and strip every domain into a staged copy of the raw/memory layer
first, then swap it in only after the whole batch succeeds — previously a
later domain failing validation left earlier domains already applied to
this.raw/this.memory while the call reported failure, exposing a partially
applied user layer to inspect() and future merges.
2026-07-30 18:42:20 +08:00
liruifengv
d8f455d694
docs(changelog): sync 0.31.0 from apps/kimi-code/CHANGELOG.md (#2404)
* docs(changelog): sync 0.31.0 from apps/kimi-code/CHANGELOG.md

* docs(changelog): polish 0.31.0 entries and note secondary model as experimental
2026-07-30 15:39:17 +08:00
qer
479403e701
chore(vscode): release 0.6.6 (#2401)
* chore(vscode): release 0.6.6

* chore(vscode): release 0.6.7

* chore(vscode): fold the sign-in wording fix into 0.6.6

* chore(vscode): backfill the 0.6.5 changelog entry
2026-07-30 15:33:19 +08:00
qer
0f3b106c42
fix(vscode): reword the sign-in waiting message to authentication (#2402) 2026-07-30 15:13:57 +08:00
liruifengv
ea81c9a3c5
feat(kap-server): expose the managed-account profile at GET /oauth/userinfo (#2363)
* feat(kap-server): expose the managed-account profile at GET /oauth/userinfo

* refactor(oauth): serve the userinfo profile as the camelCase domain type end to end

* style(agent-core-v2): drop the method-local comment on getManagedUserInfo

* chore: drop the userinfo endpoint changeset

* test(kap-server): pass the required host identity in the userinfo route test
2026-07-30 15:05:55 +08:00
github-actions[bot]
bc28e9d802
ci: release packages (#2342)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-30 14:56:30 +08:00
qer
6d0a046488
fix(vscode): keep sign-in reachable from the no-models screen (#2393)
* fix(vscode): keep sign-in reachable from the no-models screen

* fix(vscode): stop flagging bundled this.require(...) calls as VSIX runtime dependencies
2026-07-30 14:47:37 +08:00
Haozhe
d36f4c58f6
refactor(agent-core-v2): remove the fault-injection experimental feature (#2399)
- delete the faultInjection domain (IFaultInjectionService, its Agent-scope
  implementation, and the fault-injection experimental flag)
- drop the llmRequester's per-attempt fault consumption point and the
  faultToError helper
- remove the fault-injection test cases and DI wiring from the requester
  service tests, and the domain's layer-registry entry
- regenerate the state manifest without the faultInjection.* state keys
2026-07-30 14:45:31 +08:00
Haozhe
d10b1c1308
fix(agent-core-v2): treat cache entries missing required fields as cold misses (#2395)
* fix(agent-core-v2): treat cache entries missing required fields as cold misses

- normalize `archived` to a boolean when mirroring session metadata to the
  read model, so entries for pre-`archived` sessions no longer lose the key
  during JSON serialization
- add a runtime shape check on read-model cache hits; entries missing
  required fields are rebuilt from disk and overwritten, self-healing
  poisoned entries written before the fix
- log a warning when the TUI session picker fails to fetch sessions instead
  of silently showing "No sessions found."

* chore: add changeset for session index cold-miss fix
2026-07-30 14:28:28 +08:00
liruifengv
40172c7ca9
feat: unify the host identity across OAuth, telemetry, and kap-server (#2382)
* refactor(oauth): make X-Msh-Platform an explicit host identity field

X-Msh-Platform was hardcoded to kimi_code_cli in createKimiDeviceHeaders,
so non-CLI hosts could not state their own platform and the desktop had
to patch the header after the fact. KimiHostIdentity now carries a
required platform (every host declares its own value; the CLI constant
stays the fallback only for direct createKimiDeviceHeaders callers), and
userAgentProduct is renamed to productName so the transport identity
uses one name everywhere.

All in-repo identity constructions pass platform explicitly; the wire
value for CLI and VS Code hosts is unchanged (kimi_code_cli).

* feat(agent-core-v2): carry the host identity in the bootstrap snapshot

Replace the flat clientVersion field with a required clientIdentity
(KimiHostIdentity) so every consumer reads the same host identity
object: OAuthToolkitService now passes it to the OAuth toolkit, which
means the OAuth device-flow endpoints (device authorization, token
polling, refresh) on the kap-server path finally send the full X-Msh-*
device headers instead of none, and the telemetry cloud appender reads
client_version from the same source. A built-in CLI fallback keeps bare
bootstrap() calls in tests working; composition roots must pass their
own identity.

The session export manifest grows an optional desktopVersion field
(payload plumbed through; filled by kap-server in a follow-up).

* feat(agent-core): thread the host identity into the managed auth facades

The v1 managed auth facade constructed its OAuth toolkit without an
identity, so token refreshes from inside the core went out without any
X-Msh-* device headers. createManagedAuthFacade now takes an optional
KimiHostIdentity and every call site supplies one:
CoreProcessService._defaultOAuthTokenResolver forwards the core
process's options.identity (the same source _defaultKimiRequestHeaders
uses), and the DI-held services (oauth / auth summary / model catalog)
read it from a new optional identity field on IEnvironmentService. The
library-level "no identity, no device headers" contract is unchanged.

* feat(kap-server)!: require the host identity and derive request headers from it

ServerStartOptions.hostIdentity is now a required ServerHostIdentity
(KimiHostIdentity + optional prompt display fields), replacing both the
old optional HostIdentityOverrides (renamed to PromptIdentityOverrides,
its productName field now displayName) and the version option (renamed
to serverVersion — it is the engine version reported as server_version,
while the host product version travels in hostIdentity.version).

The server now feeds bootstrap's clientIdentity from hostIdentity and
derives the default outbound headers (User-Agent + X-Msh-*) from it via
createKimiDefaultHeaders, so kap-server-hosted OAuth flows and model /
WebSearch requests carry the real host identity instead of a hardcoded
kimi-code-cli fallback UA. Explicit header seeds still win as an escape
hatch.

Session export manifests record the host product version: kimiCodeVersion
now carries hostIdentity.version (the engine version no longer appears),
and desktop exports (desktop: true) are additionally stamped with a
desktopVersion field. The instance registry keeps its host_version wire
field for compatibility (kimi-inspect reads it); only the in-memory name
changed to serverVersion.

* feat(cli): wire the CLI host identity into the kimi web server

kimi web now passes createKimiCodeHostIdentity(version) as the server's
hostIdentity, so web-UI OAuth flows and the engine's outbound requests
carry the explicit CLI identity (productName + version + platform). The
explicit hostRequestHeadersSeed is dropped — kap-server derives the same
headers from hostIdentity — and buildKimiDefaultHeaders goes away with
its only consumer.

* test(klient): drop clientVersion from the bootstrap contract parity list

* chore: add changesets for the host identity unification

* feat(cli): tag kimi web requests with a (web) User-Agent suffix

kimi web shares the CLI product token and platform, so its outbound
requests were indistinguishable from direct CLI runs upstream. Its host
identity now carries userAgentSuffix 'web', putting web-UI traffic at
kimi-code-cli/<version> (web) while X-Msh-Platform stays kimi_code_cli.

* fix(klient): keep the env() clientVersion wire field after the bootstrap identity switch

The bootstrap snapshot replaced the flat clientVersion scalar with
clientIdentity, which broke klient's env() fan-out (RPCError: method not
found). The wire surface keeps clientVersion — now sourced from
clientIdentity.version — and bootstrapService gains a clientIdentity
read (registered in envContract with an object schema) for consumers
that want the full identity.

* feat(oauth): send the product User-Agent on OAuth requests

The OAuth endpoints used to receive only the X-Msh-* device headers
(undici's default UA otherwise), which left the OAuth host unable to
distinguish runtime surfaces — notably kimi web, whose platform matches
the CLI and whose only distinguishing mark is the (web) UA suffix. The
toolkit now feeds the full identity headers (User-Agent + X-Msh-*) into
every device authorization, token polling, and refresh request; the
request-header type widens from DeviceHeaders to OAuthRequestHeaders.

* feat(vscode): report kimi_code_vscode as the extension's platform

The VS Code extension inherited the CLI's hardcoded X-Msh-Platform value;
with platform now an explicit identity field it declares its own, so the
managed endpoints and OAuth host can tell extension traffic apart from
CLI runs.

* refactor(agent-core-v2)!: require the client identity at the composition root

The bootstrap fallback identity fabricated a kimi-code-cli/unknown host
for any caller that forgot to pass one — the same silent-misreport
pattern this series set out to remove, and it made "required" a lie.
BootstrapInput.clientIdentity is now required, so a missing identity
fails at compile time instead of being papered over. Test and example
callers pass a shared fixture (klient examples and test engines get one
each); the node-sdk v2 client asserts its host identity with the oauth
helper. Also folds DeviceHeaders from an interface into a type alias so
it stays assignable to the widened OAuthRequestHeaders record.

* feat(oauth)!: require and validate the platform in device headers

Drops the quiet CLI fallback in createKimiDeviceHeaders (the same
silent-misreport pattern removed from the bootstrap identity): platform
is now a required option, validated with the same required-ASCII rule as
the version — empty or all-non-ASCII values throw instead of emitting a
blank X-Msh-Platform, and header-unsafe characters are stripped rather
than sent raw.

* fix(node-sdk): seed the host request headers on the v2 client path

The interactive v2 engine path (experimental flag) bootstrapped without
a hostRequestHeaders seed, so managed vendor calls went out with the
SDK's default User-Agent (OpenAI/JS) and no X-Msh-* at all — v1 passes
the full identity headers on the same requests. The v2 client now seeds
the headers from its asserted host identity, and a test pins the seed.

* chore: simplify the CLI changeset wording
2026-07-30 13:45:41 +08:00
Kai
691ec4679e
fix: remove the blocking wait from the TaskOutput tool (#2379)
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-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
* fix: remove the blocking wait from the TaskOutput tool

The block/timeout parameters let a model stall the whole turn waiting
for a background task (up to 3600s), even though completion already
arrives via automatic notification. Remove both parameters from the v1
and v2 engines (kept in model-facing parity), simplify retrieval_status
to success/not_ready, and update the tool, Bash, and Agent prompt
wording plus user docs accordingly. Stale callers passing block are
silently treated as a non-blocking snapshot.

* fix: align background-task prompts with the non-blocking TaskOutput

The compaction reminder promised TaskOutput could fetch a task's result
for tasks that are still running, where it now returns not_ready —
reword it to snapshot semantics and point at the completion
notification. Also list AskUserQuestion(background=true) as a task
source in the TaskOutput description.

* test: exercise stale TaskOutput args through the runtime validator

A stale block/timeout argument never reaches the tool: the executor's
preflight validates args against the closed tool schema and rejects
them immediately, so the old test documented silent-tolerance semantics
the runtime never exhibits. Assert the real behavior through
compileToolArgsValidator/validateToolArgs instead, and drop
statement-adjacent comments to match the package's header-only comment
convention.
2026-07-30 01:26:56 +08:00
7Sageer
fa2c5ce18b
feat: support plugin-contributed custom agents (#2365)
* feat: support plugin-contributed custom agents

* fix: await plugin loading before agent catalog

* fix: refresh plugin agents on v1 reload

* test(agent-core-v2): add enabledSystemPrompts to the plugin service stub
2026-07-29 21:59:48 +08:00
7Sageer
1896d1a13a
Revert "fix(kosong): match Kimi's standalone "Unsupported image." rejection (…" (#2368)
This reverts commit dbb69a2678.
2026-07-29 20:56:41 +08:00
7Sageer
02d77b20d9
feat(agent-core-v2): let plugins contribute system prompt instructions via the manifest systemPrompt field (#2314)
* feat(agent-core-v2): let plugins contribute system prompt instructions via the manifest systemPrompt field

* feat(agent-core-v2): add systemPromptPath to load plugin system prompt from a file

* docs: explain plugin system prompt templates

* fix(agent-core-v2): refresh plugin system prompts after changes

* fix(agent-core-v2): freeze restored profile bindings and converge plugin contributions at session scope

- restore no longer re-renders or re-persists prompts: a resumed agent
  keeps its replayed profile binding (prompt and tool set) as persisted
- a new Session-level convergence point reloads plugin skills into the
  session skill catalog before fanning out to every live agent prompt,
  and every catalog-kind plugin mutation awaits the whole pipeline;
  MCP-only toggles carry a distinct change kind and skip it
- live refreshes after a restart re-resolve the bound profile by name
  and rebind the full slice (prompt, disallowed tools, active tools)
  atomically, warning and keeping the persisted state when the profile
  is gone; renders reuse the first-render timestamp and unchanged
  prompts are not re-persisted, so convergence never churns the wire
- cap plugin system-prompt contributions (32 KB per field/file, 64 KB
  aggregate per prompt build) with manifest diagnostics and warnings
- bump the changeset to minor: this is a new user-facing capability

* fix(agent-core-v2): register the new session domain and dedupe the missing-profile warning

- add sessionPluginContribution to the domain-layer registry so
  lint:domain stays green
- emit system-prompt-refresh-profile-missing once per profile name,
  matching the service's other deduped warnings
- document the convergence timeout escape hatch and the klient
  exclusion of enabledSystemPrompts

* fix(agent-core-v2): dedupe the plugin budget warning and surface section read failures

- emit plugin-sections-oversized once per skipped-plugin signature
- let enabledSystemPrompts failures propagate to the refresh catch
  (keeps the current prompt and warns) instead of silently rendering
  and persisting a prompt without plugin instructions
- cover the convergence timeout cut-off with a fake-timers test
- clarify that the first-render timestamp anchors per process

* fix(agent-core-v2): serialize session convergence and restore onDidReload timing

- run at most one convergence per session and bound each change's wait
  by the timeout, so a fan-out emitter never interleaves deliveries
  after a timed-out convergence
- fire onDidReload as soon as the reload commits again, keeping hook
  reloads independent of prompt convergence
- sign the plugin budget warning with an unambiguous key

* docs(agent-core-v2): align convergence wording with the serialized semantics

- the timeout retry promise only holds once stalled work clears
- note the per-session serial delivery cost model on the plugin change
  contract and the dual-queue invariant on the service

* fix(agent-core-v2): keep empty plugin sections byte-neutral in the prompt template

- place ${plugin_sections} on the same template line as
  ${skills_section} so prompts without either block render exactly as
  before this feature
- note on the change contract that waitUntil work must not call back
  into plugin mutations, and spell out the per-session convergence
  order in the user docs

* fix(agent-core-v2): pin a fork's profile so refresh triggers never rebind it

- applyBindingSnapshot left the fork with no pinned profile, which
  routed in-process forks into the post-restart catalog rebind and
  could reset an inherited tool set; forks now inherit the source
  agent's pinned profile object
- pin the first-render timestamp reuse with a ${now}-embedding test
  and document the anchored ${now} semantics
- tighten the plugin docs budget and resume-refresh wording

* fix(agent-core-v2): join in-flight convergence during agent bootstrap

- an agent created while a plugin convergence is in flight now waits
  for it, and a restored agent refreshes once after it, so a plugin
  mutation never straddles an agent's bootstrap
- warn on a non-string systemPrompt field and strip a UTF-8 BOM from
  systemPromptPath files before trimming
- correct the consumption-surface wording (every CLI surface on the
  experimental flag, not just kimi -p), the per-session queueing note,
  and the single-plugin combined budget clause

* fix(agent-core-v2): bound the bootstrap convergence join by the timeout

A permanently wedged convergence kept convergeTail pending forever,
and the unconditional settled() wait in bindBootstrap would have
blocked every later agent creation in that session; the join now
races the shared convergence timeout and continues (a restored agent
still refreshes once, which never touches the tail), and the timeout
constant moves to the contract for reuse

* fix(agent-core-v2): close the convergence race against in-progress restores

- a convergence fan-out could land while an agent's wire log is still
  replaying, dispatching a replay-visible config record whose effect
  the rest of the replay then overwrites; refreshSystemPrompt now
  skips while the wire restore is in progress
- convergence completion is tracked by a generation counter; bootstrap
  compares it (after a bounded join) and refreshes a restored agent
  exactly once when a round completed after its creation began,
  replacing the wasConverging flag that could miss both windows

* fix(agent-core-v2): bound each convergence so a wedged participant cannot stop the pipeline

- the fan-out now races the convergence timeout, so convergeTail always
  settles: a permanently hung refresh delays its round (blocked entries
  drain oldest-first on later changes) instead of killing the session's
  convergence for good
- warn when agent bootstrap stops waiting on a stalled convergence
- diagnose a blank systemPromptPath and pin the plugin-root escape
  guard with traversal, absolute-path, and symlink tests

* fix(agent-core-v2): bound the skill reload, preserve user-tool overlays, roll the prompt clock daily

- the convergence's skill-reload segment now races the same timeout as
  the fan-out, so no segment of the pipeline can wedge a session for
  good; it continues with the previous catalog and retries next change
- a cold rebind that resets the tool set replays session-added user
  tools onto the new base instead of dropping them for the rest of the
  process
- the rendered timestamp re-anchors when the UTC date rolls over, so
  long-lived processes keep a fresh clock while steady-state renders
  stay byte-stable within a day
- the plugin budget warning dedupes per plugin id, and the docs note
  that systemPromptPath content is frozen until the next reload

* feat(agent-core-v2): converge cold plugin changes on resume through a drift-free gate

- restore replays the persisted binding untouched, then bootstrap
  refreshes only when drift-free inputs changed while the session was
  cold: the catalog profile's tool set/denylist, or the plugin-sections
  baseline persisted alongside the prompt on the existing bind/update
  payloads; directory-listing and date drift wait for live triggers,
  so quiet resumes append no replay-visible records
- the rendered timestamp is day-precision (UTC date at 00:00,
  re-anchored on rollover), keeping steady-state renders byte-stable
  across resumes and sessions on the same day
- consolidate both timeout helpers onto a shared raceOutcome, and drop
  the generation counter the gate supersedes
- align the plugin-sections precedence prose with the AGENTS.md
  disclaimer (no self-granted authority, system instructions win on
  conflict)

* fix(agent-core-v2): bound the restored-prompt gate and land the sections baseline

- the gate's plugin-sections read now races the convergence timeout, so
  agent creation never blocks behind an unrelated plugin mutation
- refreshes serialize per agent through a tail, so overlapping triggers
  cannot write prompts out of order
- when plugin sections change but a plugin-free custom prompt does not,
  the new baseline lands as a sections-only update instead of making
  every later resume re-render in vain
- align the system prompt's Date and Time paragraph with the
  day-precision anchored timestamp

* Update plugin system-prompt instructions in changeset

Live sessions pick up plugin changes, while the default TUI and `kimi -p` paths ignore these fields.

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

* refactor(agent-core-v2): keep plugin skill reload user-driven

Plugin mutations still converge live agent prompts, but the session
skill catalog goes back to refreshing only on explicit plugin reload,
as before: the prompt feature does not need skill convergence, and the
pre-existing manual-reload semantics stay uniform across all plugin
contributions. Removes the convergence-driven skill reload, the
reloadSource de-privatization, and their tests; restores the
PluginSkillSource onDidReload forwarding and its catalog tests.

* refactor(agent-core-v2): apply plugin system-prompt changes only on explicit reload

Drop the live convergence machinery (the plugin onDidChange barrier,
the sessionPluginContribution fan-out, the restored-prompt drift gate,
and the day-precision render clock) so plugin system-prompt sections
take effect at the same point as every other plugin contribution:
/plugins reload or a new session. The profile now refreshes when the
session skill catalog re-pulls its plugin source on reload, reading
both the skill list and the prompt sections fresh.

* feat(agent-core): let plugins contribute system prompt instructions via the manifest systemPrompt field

* chore(agent-core-v2): remove inline implementation comment

* docs: clarify plugin prompt refresh semantics

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-29 20:30:07 +08:00
STAR-QUAKE
dbb69a2678
fix(kosong): match Kimi's standalone "Unsupported image." rejection (#2362)
Some checks are pending
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
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
isImageFormatError missed the production phrasing "Unsupported image.
Please try another one." — the existing pattern requires a url/format/
type suffix, so the deterministic image rejection never triggered the
media-stripped resend, and the session failed on every later request.

Add a standalone-sentence pattern (punctuation- or end-terminated) to
both the kosong and agent-core-v2 classifiers, keeping the deliberate
boundary that count/size phrasings must not match.

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-29 18:54:07 +08:00
wenhua020201-arch
f8ec3d1656
docs: fix dead anchor links in en/zh docs (#2348)
* docs: fix dead anchor links in en/zh docs

- #loop_control -> #loop-control (heading slug uses hyphens)
- #secondary_model -> #secondary-model
- env-vars model section anchors: kimi_model -> kimi-model
- provider credential section anchors: configtoml -> config-toml
- /provider management anchors: point at the renamed heading in each locale
- hooks: point the stale config-files#hooks reference at the local Configuration section
- en files: replace two leftover Chinese anchors with their English targets

* docs: add missing .md extension to themes page links

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-29 15:57:48 +08:00
7Sageer
b850c5f8f5
fix(node-sdk): wire applyPersistedSecondaryModel to agent-core-v2 (#2345)
* test(node-sdk): drop v1-only subagentNames from the resume parity projection

Custom agent files made v1's resumed agent config carry the bound
profile's delegatable subagent roster; v2's resumed agent state has no
equivalent field, so the resume parity cases fail on main. Project the
engine-owned field away instead of pinning it as a resume-data gap.

* fix(node-sdk): wire applyPersistedSecondaryModel to agent-core-v2

On the v2 engine route the /secondary_model command persisted the recipe
but failed to apply it to the current session: the SDK method fell
through to the base class's not_implemented getRpc().

v1 pushes a reloaded config snapshot into the session because its spawn
binding, tool descriptions, and cached startup warning all read that
snapshot. agent-core-v2 resolves the secondary model live against
IConfigService at spawn time and rebuilds the tool description per read,
so the setConfig write already takes effect session-wide. The override
keeps the rest of v1's contract: config reload, the same loud
validations (session lookup, persist-first recipe check, pointed-model
resolution wrapped at [secondary_model].model), and a warning-cache
refresh via a new recheckSecondaryModelWarning on the session warning
service. getSessionWarnings also surfaces the v2 secondary-model warning
next to the AGENTS.md one, matching v1's aggregate.

* fix(agent-core-v2): surface the subagent's bound model on status events

The v2 model slice rides only the bind-time agent.status.updated, which
precedes subagent.spawned and is dropped by clients that key child events
off the spawn, so subagent cards never learned the model — and a
single-step run emits no usage/context slice until it ends, so the model
only appeared at completion. Re-affirm the binding right after the spawn
announcement via a new IAgentProfileService.republishStatus, and fold a
consistent usage/context/model snapshot into every status event at both
v1 edges (kap-server's broadcaster and the in-process SDK session
wiring, resolving the secondary-model derived id to a readable display
name.
EOF
)

* Delete .changeset/subagent-card-model.md

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

* style(agent-core-v2): remove inline implementation comments

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-29 15:40:42 +08:00
liruifengv
37d9bdc585
docs(changelog): sync 0.30.0 from apps/kimi-code/CHANGELOG.md (#2343) 2026-07-29 12:13:20 +08:00
7Sageer
efac96c8a9
feat(agent-core): custom agent files and secondary model on the v1 engine (#2232)
* feat(agent-core): custom agent files and secondary model on the v1 engine

Migrate the custom agentfile and secondary-model capabilities from
agent-core-v2 to the v1 engine so they work in the TUI and plain
kimi -p sessions:

- discover Markdown agent files from user/project/extra/explicit
  directories with the v2 precedence rules, a merged session profile
  catalog replacing the hardcoded builtin profile lookups, SYSTEM.md
  main prompt override, and ${base_prompt} backed by the effective
  default
- --agent/--agent-file now work in print mode on the default engine;
  CreateSessionOptions gains agentProfile/agentFiles
- [secondary_model] config + KIMI_SECONDARY_MODEL/EFFORT bind newly
  spawned subagents to a cheaper model behind the secondary-model
  experiment flag, with primary/secondary model params on Agent and
  AgentSwarm and upfront session warnings
- full disallowedTools deny semantics (exact names + mcp__ globs)
  evaluated by the tool manager and persisted in the agent wire

* fix(cli): guard optional agentFiles in the prompt runner

runPrompt is also driven programmatically (headless goal flow) with
options that never pass through the CLI parser defaults, so agentFiles
can be undefined; mirror the addDirs optional-chaining pattern. Also
extend the SDK experimental-feature assertion with the secondary-model
flag.

* fix(agent-core): preserve custom agent bindings on v1

* fix(agent-core): narrow secondary model error hints

* fix(agent-core): persist custom agent profile bindings

* Delete .changeset/sdk-agent-profile-options.md

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

* Update v1-custom-agent-files.md

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

* Update v1-secondary-model.md

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

* Update v1-custom-agent-files.md

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

* fix(agent-core): keep SYSTEM.md a prompt-only overlay for delegation

* docs: update agent file and secondary model availability wording

* fix(cli): reject --agent-file combined with session resume

The resume path only forwards the agent file's name for the bound-profile
assertion; the file's content is never re-applied (the session keeps its
creation-time catalog snapshot). Previously the combination was silently
accepted, so an edited file (or a same-named one) appeared to apply but did
not. Reject it at option validation and document the constraint.

* refactor(agent-core): share prompt-section prose and note v2 twins in agentfile headers

The Windows notes, additional-dirs and skills prose blocks existed twice:
inline in the builtin default template (system.md) and as constants in the
agent-file renderer (from-file.ts). Extract them to profile/prompt-sections.ts
as the single source: system.md renders them through injected KIMI_* template
variables and from-file.ts imports the same constants. Rendered prompts are
byte-identical for all four builtin profiles across macOS/Windows and
skills/dirs on/off; a new test pins system.md to the shared constants.

Also mark each profile/agentfile file with the path of its agent-core-v2
counterpart so format/semantics changes land in both engines.

* feat(cli): add /secondary_model command for the subagent model

Mirror /model: a picker with a thinking-effort step that persists [secondary_model] and live-applies to the current session via a new Session.setSecondaryModel RPC (node-sdk wrapper included), so newly spawned subagents bind the new model right away. The /model picker now hides the synthesized __secondary__ derived entry; docs and the update-config builtin skill mention the section.

* feat(tui): show the bound model in subagent run stats

Subagents report their model alias via agent.status.updated after spawn; resolve it to a display name and surface it in tool-call subagent stats and agent-group rows.

* fix(agent-core): validate agent profile before session persistence

* fix(agent-core): refresh subagent tools after model switch

* fix(agent-core): show subagent model preferences

* fix(agent-core): preserve secondary model recipe on live apply

* fix(agent-core): make secondary model apply explicit

* fix(tui): refresh secondary model display state

* chore: merge secondary model changesets into one

* Add /secondary_model command for subagent configuration

Show each subagent's model in the subagent card header and agent-group rows. Requires the secondary-model experiment (KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL=1); run /secondary_model to pick a model and thinking effort, applied to the current session immediately.

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

* fix(agent-core): align explicit agent file precedence

* fix(agent-core): let disallowedTools deny select_tools

* chore(cli): drop engine mention from --agent/--agent-file help text

* feat(cli): support --agent/--agent-file in the interactive TUI

Bind the selected agent profile to the startup session when launching
the TUI with --agent/--agent-file, including the session created after
an OAuth login at startup. Sessions created later in the process (/new)
keep the default profile.

Make both flags creation-only in every mode: combining them with
--session/--continue is now rejected in print mode too, since resume
restores the bound agent from the session automatically.

* fix(agent-core): persist new secondary-model selections under env overrides

stripSecondaryModelConfig restored secondary_model.model/default_effort
from raw whenever KIMI_SECONDARY_MODEL/KIMI_SECONDARY_EFFORT was set, so
a /secondary_model pick made under the env vars was silently discarded
on write. Restore from raw only when the value being written still
equals the env value (an overlay round-trip), mirroring the pointer
check in stripEnvModelConfig; a genuinely different selection now
reaches config.toml.

* fix(cli): report the effective secondary model when env overrides the pick

/secondary_model toasted the picked alias even when
KIMI_SECONDARY_MODEL/KIMI_SECONDARY_EFFORT made the session bind a
different model. Read the effective binding back from the reloaded
config (as /model does from session status) and warn with the
env-overridden values instead.

* feat(tui): show the bound model name in the AgentSwarm panel header

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-29 12:06:26 +08:00
github-actions[bot]
16c7189bd5
ci: release packages (#2244)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-29 11:35:59 +08:00
liruifengv
973e2a008c
chore: prune non-user-facing changesets before release (#2335)
* chore: prune non-user-facing changesets before release

- Drop changesets for changes users cannot perceive: internal bash
  parser groundwork, host-identity server start option, and the
  structured managed-usage refactor
- Drop the duplicate agent-core-v2 repeat-breaker entry and remap the
  v1 fix to @moonshot-ai/kimi-code with user-facing wording
- Remove the untouched @moonshot-ai/kimi-code-sdk entry from the
  quota-exhausted fail-fast changeset
- Downgrade the upload size-cap removal to patch and drop the
  implementation detail from its wording

* chore: simplify verbose changeset entries

* chore: shorten changeset entries to single statements
2026-07-29 11:27:45 +08:00
Yufeng He
67dd03149f
feat(tui): customizable footer status line via status_line config (#2255)
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-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
* feat(tui): customizable footer status line via status_line config

The bottom status bar was a fixed layout. Add a [status_line] section
to tui.toml covering the two established models:

- items: codex-style composition. Pick and order the built-in slots
  (mode, goal, model, tasks, cwd, git, tips); unset keeps today's
  layout, unknown ids are skipped with a warning, and an empty list
  blanks line 1.
- command: claude-code-style custom line. The footer runs the command
  with a JSON snapshot on stdin (model, cwd, git branch, permission
  and plan mode, context usage, session id, version) and renders the
  first stdout line. Runs are throttled to one per second and capped
  at 300ms; nonzero exit, empty output, or a timeout falls back to the
  built-in layout.

Line 2 (context readout) stays built-in in every mode. Resolve #2116.

* feat(tui): apply status_line on /reload-tui and document it

The reload command pushes reloaded tui.toml fields into AppState; the
new statusLine field joins that list so edits go live without a
restart. The config files reference (EN/ZH) documents the new section.

* fix(tui): round-trip active status_line on save and harden the runner

Codex review on #2255 caught a real one: saveTuiConfig rewrites the
whole tui.toml, so changing any other preference dropped an active
[status_line] section. Render it live when set (items and command),
commented-out guide when unset.

Also: spawn the command through ComSpec/cmd.exe on Windows instead of
assuming sh.exe, and take the whole process tree down on timeout
(process-group kill on POSIX, taskkill /T on Windows) so a script that
spawned children cannot leak them.

* fix(tui): address status_line review: runner lifecycle, capture cap, tips slot

- recreate the command runner when a reload swaps status_line.command;
  the old runner kept executing the previous script until restart
- schedule a trailing refresh instead of dropping updates that arrive
  inside the throttle window, so the last state change always lands
- stop accumulating stdout once the first line is complete (and cap a
  missing-newline stream at 64KB); only the first line is ever rendered
- honor the configured position of the tips slot in items instead of
  always pinning tips to the far right
- route unknown status_line.items warnings through the TUI status area
  on reload instead of raw stderr, which could corrupt the display

Changeset text tightened per maintainer note.

---------

Co-authored-by: Kai <me@kaiyi.cool>
2026-07-28 22:37:52 +08:00
Haozhe
ceaa96942b
feat(kap-server): add global message search with literal and live-session modes (#2321)
* feat(kap-server): add the /api/v1/search global message search endpoint

Cross-session full-text search over user messages, assistant text and
session titles, backed by a minidb index under <home>/search-index with a
single-writer lock election and read-only WAL catch-up for other
processes. Hits carry transcript anchors (turn ordinal and step id) so
clients can jump straight to the matching turn or step.

* feat(kimi-inspect): add a search view with chat-timeline navigation

The left rail gains a Search view over the global search endpoint, with
role and sort filters and cursor pagination. Clicking a hit switches to
the chat view and navigates to its session, agent, turn and step — the
channel pages the turn into the loaded window, scrolls it into view and
flashes the target briefly.

* chore(kimi-code): start the dev server without built web assets

The repo's dev server scripts (dev:server, dev:kap-server,
dev:kap-server:multi, dev:server:restart) now set KIMI_CODE_DEV_SERVER=1.
When it is set and dist-web/index.html is missing, kimi web starts the
API server without the bundled web UI instead of failing at startup,
so backend dev no longer requires a kimi-web build.

* feat(kap-server): add literal substring search and a live session route

- minidb: text indexes accept an injectable tokenizer/queryTokenizer,
  and a hashed 2/3-gram tokenizer (NFKC + lowercase, code-point
  windows) backs substring search; tokenizer names persist in
  db.textindexes.json with backward-compatible defaults
- /api/v1/search gains mode: 'literal' — n-gram candidates confirmed
  against the original text (zero false positives), with an
  'candidate_cap' incomplete flag when the candidate set truncates
- container.session_id queries against a session live in this process
  scan the in-memory transcript store instead of the index (both
  modes); the response's source: live|index field names the serving
  route and rides in the page-token fingerprint
- kimi-inspect: exact-match toggle and source badge in the search
  view, plus an in-chat session search bar with jump-to-hit navigation

* test(kimi-inspect): avoid stringifying BodyInit in search api tests
2026-07-28 21:47:50 +08:00
Haozhe
f79fde2b90
feat(node-sdk): migrate the SDK method surface to agent-core-v2 (#2262)
* feat(node-sdk): add agent-core-v2 backed SDKRpcClientV2 harness

- add SDKRpcClientV2 wiring the v2 engine (DI x Scope) in-process via the
  klient memory transport, with getExperimentalFeatures migrated to
  klient.global.flags.list() and unmigrated methods failing fast
- export createKimiHarnessV2 / SDKRpcClientV2 from the SDK index
- wire the experimental v2 gate into the CLI interactive shell (run-shell)
- extend build-dts to bundle agent-core-v2 and klient declarations

* feat(node-sdk): migrate listWorkspaceSkills to agent-core-v2

- add engineAccessor escape hatch exposing the in-process engine's app-scope
  service accessor for SDK methods the klient facade does not cover yet
- implement listWorkspaceSkills via ISkillDiscovery plus the v2 user/project
  root helpers and BUILTIN_SKILLS (plugin skills and skillDirs still gaps)
- add a v1-v2 parity test pinning identical return values per migrated
  method, with understood gaps listed explicitly in KNOWN_DIFFS

* feat(node-sdk): migrate the SDK method surface to agent-core-v2

- implement the remaining SDKRpcClientBase methods on SDKRpcClientV2,
  routed through the klient facade where covered, the engineAccessor
  escape hatch where the engine has a service, or SDK-side rebuilds on
  v2 primitives where only primitives exist (config shape mapping,
  global mcp.json store, MCP OAuth flows, importContext, session
  warnings, print background policy)
- translate the v2 event stream into the v1 Event union and bridge
  approval/question/user_tool interactions per live session
- rebuild resume replay by folding the v2 wire.jsonl through the v1
  agent restore pipeline, so resumed sessions render history again
- keep deleteSession as not_implemented; the v2 engine has no delete
  capability
- extend the v1-v2 parity suite to every migrated method, pinning
  understood engine differences in KNOWN_DIFFS
- add the dev:cli:v2 root script to launch the TUI on the v2 engine

* fix(node-sdk): await the v2 undo and compaction-cancel agent calls

* test(cli): spread the real oauth module in the telemetry test mock

* feat(node-sdk): forward v2 engine telemetry to the host telemetry client

* feat(cli): gate the v2 TUI route behind a dedicated KIMI_CODE_TUI_V2 switch

* fix(node-sdk): honor skillDirs on the agent-core-v2 SDK route

The v2 SDK client accepted KimiHarnessOptions.skillDirs (the CLI's
--skills-dir) but never seeded it into the engine, so explicit skill
dirs were silently dropped on the v2 TUI route and the Skill tool
could not find skills from them. Seed skillCatalogRuntimeOptions at
bootstrap and let listWorkspaceSkills resolve the explicit dirs as
the user source, matching the engine's session skill catalog.

* refactor(cli): gate the v2 TUI route behind the master experimental flag again

Drop the dedicated KIMI_CODE_TUI_V2 switch: the TUI v2 harness route is
gated by KIMI_CODE_EXPERIMENTAL_FLAG, the same master switch as the
kimi -p v2 route. The gate tests are kept with updated assertions, and
dev:cli:v2 sets the master flag again.
2026-07-28 20:29:45 +08:00
7Sageer
d88b3775c9
fix(agent-core-v2): count validation-rejected tool calls toward the repeat breaker (#2317)
* fix(agent-core-v2): count validation-rejected tool calls toward the repeat breaker

* style(agent-core-v2): remove inline implementation comments
2026-07-28 19:32:46 +08:00
STAR-QUAKE
de0ba9d065
fix(agent-core): count validation-rejected tool calls toward the repeat breaker (#2313)
* fix(agent-core): count validation-rejected tool calls toward the repeat breaker

Args-rejected calls returned before prepareToolExecution, so the breaker
never counted them and the model could re-issue the same invalid call
until maxSteps. Register them in finalizeToolResult so reminders fire at
3/5/8 and the turn force-stops at 12.

* fix(agent-core): key parse-failed repeats on raw argument text

Malformed JSON arguments normalize to {} on parse failure, which keyed
every malformed-but-different attempt identically and could force-stop a
turn whose calls were evolving rather than identical. Register skipped
calls on the raw arguments text when parsing failed.

---------

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-28 18:39:26 +08:00
Haozhe
d03a4886fd
feat(server): remove the 50 MiB upload size cap and stream uploads to disk (#2312)
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-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
- add writeStream/putStream to the storage and blob store interfaces so
  large values are written incrementally (tmp + fsync + rename) instead of
  being buffered in memory
- FileService.save now streams the request body straight to the blob store
  and only counts bytes for FileMeta.size
- drop the multipart fileSize limit and the file.too_large error from the
  /files upload path (41301 stays in the wire protocol for session export)
2026-07-28 17:01:45 +08:00
liruifengv
b0f43aea28
feat(oauth): return structured managed usage rows (#2300)
* feat(oauth): return structured managed usage rows

Stop formatting plan-usage labels and reset hints into English strings
at the oauth layer. The parser still absorbs backend field drift, but
now emits a stable structured row (name / window{duration,unit} / used
/ limit / resetAt) that kap-server passes through and clients localize
themselves:

- window: normalized from duration/timeUnit (minute/hour/day/week),
  whole-hour minute windows fold to hours, unnamed summaries are the
  weekly limit
- resetAt: absolute ISO timestamp; relative reset_in/ttl seconds are
  converted at parse time
- TUI /usage panel formats labels and reset hints locally

* refactor(oauth): parse managed usage strictly to the current payload shape

Drop the defensive drift tolerance (alternate reset-time spellings,
reset_in/ttl seconds, remaining-derived used, title/scope names,
top-level duration/timeUnit, fuzzy time-unit matching) and parse only
what the platform actually sends: numeric strings, resetTime, nested
detail/window records, TIME_UNIT_* enums.

* fix(node-sdk): update managed usage smoke example and facade tests for structured rows
2026-07-28 15:03:25 +08:00
Petrichor
cdbd33c13c
fix(kosong): fail fast on quota-exhausted 429 instead of retrying (#1857)
* fix(kosong): fail fast on quota-exhausted 429 instead of retrying

A 429 caused by an exhausted account quota or insufficient balance
(Moonshot error.type "exceeded_current_quota_error", OpenAI
"insufficient_quota") can never succeed on retry, yet it was classified
as APIProviderRateLimitError and silently retried for the whole budget
(10 attempts, ~3 minutes of backoff) with no UI feedback — the session
appeared frozen on every request.

Introduce APIProviderQuotaExhaustedError, minted in
normalizeAPIStatusError from the structured body error.type/error.code
forwarded by convertOpenAIError, with billing-anchored message patterns
as a fallback for gateways that flatten the body to text. The new class
is excluded from isRetryableGenerateError (fail fast, even when a
retry-after header is present) and from isProviderRateLimitError (no
swarm requeue/suspend). toKimiErrorPayload and translateProviderError
map it to provider.api_error (retryable: false) instead of
provider.rate_limit, and classifyApiError reports it as
quota_exhausted in telemetry. agent-core-v2 mirrors the same fix.

Transient rate-limit 429s keep the existing retry, backoff, and
Retry-After behavior (verified end-to-end against a mock provider:
quota body fails after attempt 1/10; rate-limit body still walks the
full 10-attempt ladder).

Behavior changes to note: quota-failed swarm subagents now fail
instead of suspending indefinitely as "Rate limited...", and quota
errors cross the wire as provider.api_error rather than
provider.rate_limit.

* fix(kosong): classify quota exhaustion in OpenAI Responses stream errors

Responses response.failed / error SSE events carry no HTTP status and
were minted by errorFromOpenAIResponsesEvent as either a rate-limit
error (rate_limit_exceeded / embedded status_code=429) or a base
ChatProviderError — and the base class falls into the retryable
unclassified-failure fallback, so an insufficient_quota event still
burned the whole retry budget on the openai_responses path. Route the
event code and message through the same quota-exhausted check before
the rate-limit branch, in kosong and the agent-core-v2 mirror. Covers
all three entry paths (error events, response.failed, nested gateway
frames) since they share the single converter.

* style(agent-core-v2): drop inline comments per AGENTS.md header-only rule

agent-core-v2 comments live solely in the top-of-file block, never
beside functions or statements; the kosong twins keep the full
rationale.

* refactor(kosong,agent-core-v2): move quota-429 checks to vendor hook

Per review on #1857: the knowledge of how a backend signals quota
exhaustion is vendor-specific and must not run for every
OpenAI-compatible provider from the shared conversion layer.

- Add a convertError hook: ProtocolTrait.convertError in agent-core-v2
  (single-value, last-declarer-wins, bound by composeOpenAIChatHooks /
  composeAnthropicHooks / traitConvertError) and an equivalent optional
  hook parameter on convertOpenAIError / convertAnthropicError. Bases
  consult it with the raw failure (SDK error on HTTP paths, raw event on
  the Responses in-stream path) after the abort guard, before their own
  rules.
- Declare Moonshot's quota signals (exceeded_current_quota_error,
  billing wordings) on the Kimi side: kimiOpenAITrait and
  kimiAnthropicTrait in v2, the KimiChatProvider and KimiFiles catch
  sites in kosong, all through the new classifyKimiQuotaError.
- Drop the options parameter from normalizeAPIStatusError and the
  shared quota code/pattern tables: the contract layer keeps only the
  vendor-neutral APIProviderQuotaExhaustedError type and its retry /
  rate-limit / wire-mapping semantics.
- The OpenAI bases keep recognizing only OpenAI's own documented
  insufficient_quota code (HTTP and Responses stream events) as
  protocol knowledge of that wire.

Behavior: kimi and openai provider types classify exactly as before;
an unregistered vendor speaking Moonshot billing wordings through a
plain openai transport now stays a retryable rate limit by design.

* fix(kosong,agent-core,agent-core-v2): wire kimi quota hook fully

Follow-up to the second review round on #1857, all four findings:

- Kimi-over-Anthropic (legacy engine): AnthropicOptions gains the same
  optional convertError hook as the OpenAI bases, threaded through
  AnthropicStreamedMessage and every catch site, and the provider
  manager's anthropic route now passes classifyKimiQuotaError for
  provider type kimi — a quota-exhausted 429 over this transport
  previously still burned the retry budget. classifyKimiQuotaError now
  also walks error -> .error -> .error.error for the code/type, since
  the Anthropic SDK keeps the full body on .error instead of hoisting.
- v2 telemetry: ApiErrorKind gains 'quota_exhausted' and
  classifyApiError checks APIProviderQuotaExhaustedError before the
  generic 429 branch, matching the legacy engine's reporting.
- Hook contract: converted ChatProviderErrors now pass through before
  the vendor hook is consulted in convertOpenAIError /
  convertAnthropicError (both engines), so the hook sees each raw
  failure exactly once even when a stream-minted error crosses an
  outer catch; tests assert the single consult.
- protocolTrait: the convertError member doc shrinks to the concise
  style and the consult contract moves into the file header's
  composition rules.

* test(kosong,agent-core,agent-core-v2): lock quota hook assembly paths

Third review round on #1857:

- Fix the v2 anthropic base header and AnthropicHooks doc still claiming
  withThinking is the only hook.
- Drop the two remaining non-header JSDoc blocks in protocolTrait.ts per
  the AGENTS.md header-only rule; the consult contract already lives in
  the file header.
- Update the ProtocolTrait contract test to the seventeen-hook shape
  (convertError included) and cover the traitConvertError binding.
- Add real-assembly regression probes: the v2 registry composes a
  (kimi, anthropic) provider whose mocked SDK client throws a Moonshot
  quota 429 and generate rejects with the non-retryable
  APIProviderQuotaExhaustedError (a plain anthropic composition keeps
  the same 429 retryable); the legacy ProviderManager routing test
  asserts convertError is classifyKimiQuotaError on the kimi-anthropic
  route and absent for plain anthropic; the legacy provider threads
  options.convertError to its generate catch.

* test(kosong,agent-core-v2): cover KimiFiles quota 429 and drop stale docs

Fourth review round on #1857:

- Drop the AnthropicHooks member JSDoc (its content already lives in the
  anthropic.ts and anthropicHooks.ts file headers) and fix the anthropic
  contrib header still calling the hook set single-hook.
- Add the missing KimiFiles regression in both engines: a mocked files
  client rejecting with a Moonshot quota 429 makes uploadVideo reject
  with the non-retryable APIProviderQuotaExhaustedError, locking the
  classifyKimiQuotaError argument at the upload catch sites.
2026-07-28 14:35:12 +08:00
Haozhe
e556088458
feat(tree-sitter-bash): add a pure-TypeScript bash parser and an agent-core-v2 bashParser service (#2016)
* feat(tree-sitter-bash): scaffold pure-TypeScript bash parser package

Add packages/tree-sitter-bash with the SyntaxNode tree model (UTF-16
code-unit offsets, tree-sitter-bash named-node type names), a parse
budget (deadline + node cap) with an aborted ParseResult variant, and
a placeholder parse() to be replaced by the real lexer/parser.

* feat(tree-sitter-bash): add lexer and core recursive-descent parser

Cover the permission-analysis grammar subset: lists, pipelines,
commands, words, quotes, expansions, command/process substitution,
subshells, the full redirect operator set, heredocs and comments,
with tree-sitter-bash named-node type names. Long scan loops check
the parse deadline via budget.progress() so large literals cannot
exhaust the node cap; parse depth is bounded on all recursion
chains.

* feat(tree-sitter-bash): support the full bash grammar

Add compound commands (if/while/until/for/c-style-for/select/case/
function/compound/do_group), test commands with reference-exact
extglob/regex right-hand-side rules, a Pratt expression engine for
arithmetic expansion shared across arith/c-for/test modes, arrays
and subscripts, declaration/unset commands, ansi-c/translated
strings and brace expressions. Reserved words are recognized only
in statement position. Case-aware balanced scanning keeps command
substitutions intact around case items, expression leftovers are
kept as ERROR nodes instead of dropped, and recursion depth is
bounded per chain (substitution 150, parse 500, lexer scan 1024).

* test(tree-sitter-bash): add differential fixtures, corpus, fuzz and perf suites

Turn the ad-hoc wasm comparison work into permanent infrastructure:
a differential helper pinning reference-equivalent and
known-difference samples against the real tree-sitter-bash wasm
(478 match / 79 known-diff fixtures plus the official v0.25.0
corpus), a three-way consistency check between fixtures, the
known-difference registry and the README, deterministic seeded
fuzz with tree-integrity assertions, and performance smoke tests.
Converges 15 further divergence groups found by systematic probing
and documents the rest; the full suite is 853 tests in ~4s.

* feat(agent-core-v2): add App-scope bashParser service

Wrap the pure @moonshot-ai/tree-sitter-bash package as
IBashParserService (L1, no dependencies): parse(source, options)
returns a wire-safe BashParseResult whose nodes drop the cyclic
parent link so trees can cross the RPC boundary. Budget exhaustion
surfaces as { ok: false, reason: 'aborted' }, malformed input as
hasError — never a throw.

* chore: add changeset for the bash parser service

* chore: update pnpmDeps hash after adding tree-sitter-bash package

* fix(agent-core-v2): register bashParser service with ScopeActivation

The registration was written against the removed InstantiationType API
(#/_base/di/extensions); switch to ScopeActivation.OnDemand from
#/_base/di/scope so the package typechecks and the service registers.

* feat(kimi-inspect): add Bash Parser view

Add a fourth icon-rail tab that exercises the App-scope bash parser
service over the debug RPC surface: a source textarea with a parse
budget (timeoutMs / maxNodes), a dropdown of curated examples adapted
from the tree-sitter-bash differential fixtures, and an expandable
syntax tree with per-node type, UTF-16 range and leaf text, plus
hasError / aborted / node-count badges.

* fix(agent-core-v2): snapshot bash syntax trees iteratively

A long left-associative chain (e.g. an arithmetic expression with a
few thousand operands) parses into a tree thousands of levels deep
while still within budget; the recursive DTO conversion then overflowed
the JS call stack and made parse throw RangeError, breaking the
never-throws contract. Convert the tree with an explicit stack, the
same approach as the parser's own materialize.

* feat(kimi-inspect): add a deep-arithmetic example to the Bash Parser view

A thousand-operand left-associative chain fills the textarea with a
thousand-level binary_expression tree — the shape that once overflowed
the DTO conversion. Deeper chains still parse in-process but cannot
cross the JSON RPC transport (V8 call-stack limit in serialization),
so the example stays within the wire limit.

* chore: update pnpmDeps hash for the rebased lockfile

The rebase onto main merged pnpm-lock.yaml, invalidating the recorded
fetchPnpmDeps hash; use the hash CI computed for the merged lockfile.
2026-07-28 14:29:08 +08:00
qer
425cfdf53f
chore(web): upgrade markstream-vue to 1.0.7 (#2294)
* chore(web): upgrade markstream-vue to 1.0.7

Fix garbled code-block line numbers reported on 0.29.2: the async
code-block loading fallback rendered unstyled (proportional font,
code overlapping the line-number gutter) and with an over-estimated
reserved height that clipped leading lines. markstream-vue 1.0.6 adds
self-contained fallback styles and 1.0.7 fixes the height estimate.

* chore(nix): update pnpmDeps hash for markstream-vue 1.0.7

* chore(web): scope lockfile update to the markstream-vue graph

Regenerate pnpm-lock.yaml with a plain install so only markstream-vue
and its transitive deps move (markdown-it-ts, stream-markdown-parser,
etc.); unrelated toolchains (e.g. lightningcss in kimi-inspect) stay
pinned as before. Addresses review feedback.

* chore(nix): update pnpmDeps hash after lockfile scoping

* chore(changeset): shorten release note to one user-facing sentence
2026-07-28 13:25:38 +08:00
Haozhe
7e30add445
feat(kap-server): add global fs:mkdir endpoint (#2281)
* feat(kap-server): add global fs:mkdir endpoint

Add POST /api/v1/fs:mkdir to create a directory on the host filesystem
by absolute path, backing the folder picker's "new folder" action.
Implemented directly on node:fs/promises.mkdir in the transport layer
for now, non-recursive by design, with wire errors mapped to the
existing fs.* codes (40001/40409/40411/40919).

* test(kap-server): update api surface snapshot for fs:mkdir

* test(kap-server): stop export tests from holding server.close() open

The export download tests reused pooled undici keep-alive connections,
so afterEach's server.close() could wait out fastify's 72s default
keepAliveTimeout and die on the 10s hook timeout (flaky on CI). Send
connection: close on the streamed export requests, matching the
fs:content tests.
2026-07-28 10:50:39 +08:00
7Sageer
77618e38c3
feat(kap-server): wire cloud telemetry for engine events (#2230)
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-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
* feat(kap-server): wire cloud telemetry for engine events

The v2 engine registers a full telemetry event catalog, but kap-server
never attached an appender, so events from web-hosted sessions were
dropped to the null appender. Add an opt-in `telemetry` start option
that attaches a CloudAppender (app_name kimi-code-cli, ui_mode web,
matching the v1 `kimi web` host conventions), still gated by the
config `telemetry` toggle, with periodic flush and a bounded flush on
close. The option defaults off so tests never post to the real
endpoint; the CLI's `kimi web` host enables it.

* fix(kap-server): honor telemetry disable environment

* fix(agent-core-v2): isolate session telemetry context

* fix(kap-server): seed telemetry client version

* fix(cli): share telemetry shutdown deadline

* fix(telemetry): make shutdown ownership durable

* fix(kap-server): keep telemetry shutdown best-effort
2026-07-27 17:18:16 +08:00
qer
29783e471a
feat(cli): add plugin quota and update notices (#2147)
* feat(cli): add plugin quota and update notices

- Show "Note: This plugin consumes your quota." after installing
  quota-consuming official plugins (currently Kimi Datasource).
- Show a one-time update notice after invoking an outdated plugin (a
  plugin MCP tool call or a /<plugin>:<command> turn); the last
  notified version is persisted so each new marketplace version
  reminds once.
- Skip the third-party trust prompt for loopback sources that mirror
  the official plugin CDN path, so the dev plugin marketplace no
  longer prompts when installing official plugins.

* feat(cli): report plugin update notices at turn end

Buffer plugin MCP tool usage during the turn and report it together
with plugin command usage when the turn's output has fully ended,
instead of firing the check mid-turn at tool result time. Cancelled
turns no longer trigger the notice.

* fix(cli): refresh plugin MCP map on miss and serialize notice writes

Address review findings on the plugin update notifier:

- The memoized MCP server-to-plugin map is reused across /reload,
  /new, and session switches, so plugins installed or enabled later in
  the same app run never resolved. Refresh the map once on a lookup
  miss, and never pin an empty map when there is no session.
- Concurrent notices (a turn that used two outdated plugins) raced on
  the read-modify-write cycle of the notice state file and could drop
  each other's entries. Serialize checks through a promise queue so
  each notified version is persisted exactly once.

* fix(cli): gate plugin notices on official provenance and survive tool-name truncation

Address review findings:

- The quota note and the update notice keyed on the plugin id alone,
  so a local/GitHub fork reusing a billed plugin's manifest id was
  treated as the official build. Both now require official provenance
  (a zip install from the official CDN plugin path or its loopback dev
  mirror) via a shared isOfficialPluginInstall check.
- Resolving plugin MCP tools by splitting on the '__' separator broke
  for qualified names core truncates to 64 chars, which can cut the
  separator. Match known server names by longest prefix with a name
  boundary instead, which survives truncation as long as the server
  part itself is intact.

* revert(cli): drop the dev marketplace trust relaxation

The loopback carve-out let any local service bypass the third-party
trust prompt by serving a zip under the official path shape, which
does not prove official provenance (review P1). Revert to the single
rule — only https://code.kimi.com/kimi-code/plugins/official/* is a
trusted official source — and restore the stock dev marketplace
server. Installing official plugins from the dev marketplace shows
the trust prompt again.

* fix(cli): restrict update notices to the official catalog and settle tests

- Skip the update check when the loaded marketplace is not the default
  official catalog, so a custom KIMI_CODE_PLUGIN_MARKETPLACE_URL can
  no longer produce a notice that claims to come from the Official
  Marketplace.
- Return a never-rejecting promise from the notifier entry points so
  tests await the serialized queue directly instead of relying on
  zero-delay timers for ordering.
2026-07-27 17:00:42 +08:00
Haozhe
a77ee03829
feat(agent-core-v2): add hostIdentity domain for host-overridable system prompt identity (#2144)
* feat(agent-core-v2): add hostIdentity domain for host-overridable system prompt identity

- add app-scope hostIdentity domain (L3) with productName / replyStyleGuide
  overrides and hostIdentitySeed for composition roots
- render ${product_name} and ${reply_style_guide} in the base system prompt,
  falling back to CLI defaults when the host provides no override
- seed the variables from AgentProfileService via IHostIdentity
- expose hostIdentity on kap-server's ServerStartOptions

* chore: add changeset for host identity system prompt

* fix(agent-core-v2): adapt hostIdentity registration to ScopeActivation

The DI refactor on main removed _base/di/extensions (InstantiationType);
register with ScopeActivation.OnScopeCreated instead, and regenerate the
state manifest for the new SystemPromptContext fields.

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-07-27 16:55:41 +08:00
7Sageer
086769bfad
chore: drop the changelog entry for the experimental secondary-model API (#2243)
The secondary-model feature is still experimental (gated by
KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL and ignored by the interactive
TUI), so the config API support merged in #2228 should not produce a
user-facing changelog entry yet.
2026-07-27 16:47:43 +08:00
7Sageer
3b017821cf
feat(kap-server): accept secondary_model in the config API (#2228)
* feat(kap-server): accept secondary_model in the config API

POST /api/v1/config now accepts secondary_model, persisted to the
[secondary_model] config section via the generic per-domain dispatch.
GET /config also hides the synthesized __secondary__ derived entry
from the models view, matching the GET /models listing.

* Update config-api-secondary-model.md

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

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-27 16:45:18 +08:00
liruifengv
a9af42e698
docs(changelog): sync 0.29.2 from apps/kimi-code/CHANGELOG.md (#2236) 2026-07-27 15:04:24 +08:00
github-actions[bot]
8a45f10edd
ci: release packages (#2145)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-27 14:28:12 +08:00
liruifengv
48bf3d4c28
feat(kap-server): bundle the desktop app log into session exports on request (#2223)
* feat(kap-server): bundle the desktop app log into session exports on request

* refactor(agent-core-v2): align the desktop log export with repo conventions
2026-07-27 12:07:58 +08:00
7Sageer
d40d0d305d
refactor(agent-core-v2): make undo domain-owned (#2055)
* refactor(agent-core-v2): rebuild undo as wire-level journal rewind

Replace the compensating context.undo op with a wire-layer rewind
primitive: a log.cut control record with a persisted target, applied
uniformly by the wire during fold. Turn boundaries become first-class
(TurnIndexModel indexing turn.prompt record positions), models declare
a temporal classification (rewindable), and a single
IAgentRewindService owns the undo pipeline (quiesce -> precheck ->
cut -> reconcile) with all entry points converged.

- wire: log.cut record, rewindable model flag, re-fold rebuild;
  OpApplyContext.recordIndex for position-aware reducers
- rewind service: aborts the active turn, cancels in-flight
  compaction, preserves the pending queue, rebases measured tokens,
  reconciles lastPrompt, tracks conversation_undo
- todo list, plan mode, task-notification delivery and the turn index
  now rewind together with the undone turns
- transcript reducer applies cut ranges so snapshot/messages surfaces
  stay consistent with the model context
- REST/RPC/debug undo entry points converge on the rewind service;
  TUI parses the v2 undo-unavailable error shape
- legacy context.undo records keep replaying for old journals

* refactor(agent-core-v2): keep undo domain-owned

* refactor: enhance /undo functionality for consistency and safety, including todo list rollback and improved event handling

* chore: clean up undo changeset artifacts

* refactor: rebuild rewind consistency

* fix: make conversation undo durable and consistent

* fix(agent-core-v2): stabilize undo restoration

* fix: keep TUI undo on legacy error contract

* refactor(agent-core-v2): drop unused full compaction cancel API

Undo now rejects with session.busy while compaction runs instead of
cancelling it, so the awaitable cancel() added for the earlier rewind
semantics has no callers left. Remove it from the interface and
implementation; the RPC cancel path keeps using the task abort
controller directly.

* fix(agent-core-v2): remove injected context on undo

* chore(agent-core-v2): regenerate wire manifest

* docs(agent-core-dev): rename rewind to undo in layer table

* fix(agent-core-v2): undo prompt-owned image reminders

* refactor: remove transcript undo reconciliation

* fix(kap-server): map undo busy errors

* refactor(agent-core-v2): rename undo participant registry and attribute checkpoint depth

- Rename IAgentConversationUndoReconciliationRegistry to
  IAgentConversationUndoParticipantRegistry (conversationUndoParticipants).
- Return the limiting model from checkpointDepth and include it in the
  SESSION_UNDO_UNAVAILABLE details; report checkpoint_lost instead of
  compaction_boundary when no compaction explains the missing depth.
- Add a registry invariant test: every model reacting to context.* ops
  must be registered via defineCheckpointedModel or explicitly exempt.

* Delete .changeset/fix-undo-injections.md

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

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-27 11:09:26 +08:00
Haozhe
cc9b25e132
fix(kap-server): flush public and control WS frames immediately (#2221)
Only subscription events now enter the timed coalescing buffer.
server_hello, acks, resync_required, and global session events join the
outbound FIFO and flush it right away, so they no longer wait behind the
flush window and cannot overtake earlier buffered subscription frames.
The broadcaster marks global fan-out with the new BroadcastDelivery
'immediate' lane.
2026-07-27 10:58:38 +08:00
Luyu Cheng
0cef160c4b
fix(agent-core): continue goal pursuit when a goal turn hits the per-turn step limit (#2210)
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
Release / Native release artifact (push) Blocked by required conditions
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
Release / Publish native release assets (push) Blocked by required conditions
* fix(agent-core): continue goal pursuit when a goal turn hits the per-turn step limit

* chore(agent-core-v2): regenerate state manifest

* fix(agent-core): start goal pursuit when the goal-creating turn hits the step limit

* refactor(agent-core-v2): drop step-cap narration from goal module and test
2026-07-27 00:30:33 +08:00
Haozhe
bf8e967d5c
refactor(agent-core-v2): register tools as scoped services (#2196)
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-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
* refactor(agent-core-v2): register agent tools as DI services with profile-aware activation

- replace module-level registerTool + Eager AgentBuiltinToolsRegistrar with registerAgentTool double registration (Agent-scope DI service + contribution table)
- add toolActivation domain: AgentToolActivationService filters contributions by the bound Profile's tool policy using declared names, resolves instances lazily via accessor.get, and re-activates on agent.status.updated
- rename BuiltinTool to AgentTool service interface; tools become Agent-scope services with decorator-injected dependencies (e.g. AgentTool -> SubagentTool/ISubagentTool)
- AgentLifecycleService.create runs one activation pass after restore and profile binding so tools reflect the Profile before the first turn
- update tool registrations, scripts, and tests; add toolActivationService tests

* refactor(agent-core-v2): centralize builtin tools under agent/tools

- move builtin tools from scattered domain folders (plan/tools,
  goal/tools, os/backends/node-local/tools, task/tools, etc.) into a
  unified agent/tools/ directory
- split each tool into a kebab-case definition file (bash.ts) and a
  registration file (bashTool.ts) pairing with its prompt markdown
- update imports across src, tests, kap-server, and TUI comments

* fix(agent-core-v2): keep agent tools out of scope-creation instantiation

Main's instantiateAll constructs every registered service at scope
creation, but agent tool constructors may legitimately throw when their
host capability is absent (e.g. WebSearchTool without a configured
provider), and profile-aware activation must stay the only resolution
path so the runtime registry holds real instances, never proxies.

- add SyncDescriptor.instantiateWithScope (default true) and let
  registerScopedService opt registrations out of the instantiateAll
  sweep
- registerAgentTool passes the opt-out, restoring lazy
  activation-driven construction on top of the eager-scope semantics
- regenerate docs/state-manifest.d.ts

* refactor(agent-core-v2): replace delayed DI with scope activation

- add explicit OnScopeCreated and OnDemand activation modes
- remove delayed proxy and idle initialization support
- migrate service registrations, tests, and DI guidance
2026-07-26 14:32:59 +08:00
Haozhe
7799bd7346
feat(agent-core-v2): add per-scope keyed state container (#2192)
* refactor(agent-core-v2): instantiate all registered services eagerly at scope creation

- add instantiateAll to scope creation: every service registered for a
  scope tier is constructed when the scope is created, following the
  static dependency graph; a failing constructor fails scope creation
- drop the hand-maintained eager-resolution lists: igniteEagerServices
  in AgentLifecycleService, the force-instantiated session services in
  SessionLifecycleService, and the kosong config bridge get in bootstrap
- update DI docs and agent-core-dev skill guidance for the new semantics
- adjust affected tests (registry hygiene, timer draining, listener
  ordering) and add scope-tree coverage for eager instantiation

* feat(agent-core-v2): add per-scope keyed state container (state domain)

- add _base StateRegistry: typed StateKey/defineState descriptors with
  register/get/set, per-key onDidChange and global onDidChangeAny events,
  and BugIndicatingError on duplicate or unregistered key access
- bind thin per-scope services at each tier: IStateService (App),
  ISessionStateService (Session), IAgentStateService (Agent), so scoped
  plain-data state lives in one observable container that dies with the scope
- register the state domain in the layer check script and export the new
  services from the package index
- add StateRegistry unit tests and scoped resolution tests

* refactor(agent-core-v2): move session-scope service state into ISessionStateService

- add StateRegistry.snapshot() with JSON-safe serialization for Map/Set/Date/circular values
- migrate plain-data fields of 13 session-scope services (cron, interaction, sessionActivity, agentProfileCatalog, fs, fsWatch, log, metadata, skillCatalog, toolPolicy, workspaceCommand, workspaceContext) to defineState keys registered in sessionState
- register SessionStateService in affected tests and add test/state/stubs.ts helper

* feat(kimi-inspect): rework chat layout with session pane and tabbed right dock

- add SessionPane column next to the sidebar: Services tab (pending
  interactions + session Service panels) and State tab polling
  ISessionStateService.snapshot() every second, rendered as a live
  diff tree
- merge the transcript audit panel and the agent inspector into one
  RightPanel with Audit/Agent tabs that keep panel state across switches
- extract InteractionsCard from Inspector into its own component
- collapse multiline strings in StateTree into a compact hover-preview
  button with a viewport-clamped fixed popup
- test: cover StateRegistry.snapshot() conversions (Map/Set/circular);
  pass a session state service to SessionInteractionService in kap-server
  test fakes
- update the AGENTS.md project map for the new layout

* refactor(agent-core-v2): move agent-scope service mutable state into agentState

Register each Agent-scope service's mutable fields into the agent-state
container (IAgentStateService) via defineState keys, and access them
through get/set accessors backed by states.get/set. Promise locks,
disposables, and other mechanism-only fields stay plain instance fields.

- define per-service state keys (e.g. activityView.*, goal.*, toolDedupe.*)
  and register them in each service constructor
- replace direct field reads/writes with states-backed accessors across
  the touched services
- update the affected unit tests for the new IAgentStateService dependency

* fix(agent-core-v2): collapse class instances in state snapshots

- StateRegistry.snapshot() now stops at custom-prototype boundaries and
  emits '(ClassName)' markers, so resource graphs reachable from registered
  values (e.g. tool instances holding service references) can no longer
  exhaust the heap during export; plain data keeps recursing
- add agent-scope state test covering the full assembled agent scope
- kimi-inspect: extract the session State card into a shared StateCard and
  add an agent State tab in RightPanel polling IAgentStateService.snapshot()
- document the per-scope state container pattern in the agent-core-dev skill

* refactor(agent-core-v2): keep resource-holding state out of the state container

- move fields holding live resources (tool entries, managed tasks, turn
  jobs, prompt records, MCP registrations, profile callbacks) back to
  plain private fields so the agent state service only holds
  snapshot-safe plain data
- add gen:state-manifest script that statically collects defineState
  keys and their register call sites into docs/state-manifest.d.ts
- add state manifest freshness test and update agent-state docs

* chore: add changeset for session-scope state container

* docs(agent-core-v2): move eager-instantiation notes into the scope.ts header

* fix(kimi-inspect): suppress stale state tree while switching state owner
2026-07-26 10:10:15 +08:00
Luyu Cheng
c497af60e6
fix(tui): steer user messages into the running turn while a goal is active (#2153)
Some checks failed
CI / build (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test (5) (push) Has been cancelled
CI / test-pi-tui (push) Has been cancelled
CI / test-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
Nix Build / Check flake.nix workspace sync (push) Has been cancelled
Release / Release (push) Has been cancelled
Nix Build / nix build .#kimi-code (push) Has been cancelled
Release / Deploy docs (push) Has been cancelled
Release / Native release artifact (push) Has been cancelled
Release / Publish native release assets (push) Has been cancelled
* fix(tui): steer user messages into the running turn while a goal is active

* fix(tui): reset request state when steering mid-goal input fails

* fix(agent-core): update session prompt metadata on steer
2026-07-25 02:12:24 +08:00
Liu Zhongnuo
f06eb5c60e
feat(agent-core): defer registered user tools (#2119)
Some checks are pending
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
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 / build (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
* feat(agent-core): defer registered user tools

Allow hosts to mark registered user tools as deferred so select_tools can discover and load their schemas on demand in both agent-core implementations.

* fix(agent-core): hide unregistered deferred tools

Filter loaded deferred user-tool schemas from provider history and loaded-state checks after unregister while preserving canonical history and disconnected MCP behavior.

* fix(agent-core-v2): drop stale inline schemas

Stop treating previously loaded user-tool schemas as active after the same tool is re-registered for inline disclosure.
2026-07-24 17:48:36 +08:00
Chen, Yicun
0d00a07c02
fix(web): preserve selected text when copying over HTTP (#2120)
Co-authored-by: chenyicun <chenyicun@msh.team>
2026-07-24 16:38:36 +08:00
Haozhe
3615b5da9f
refactor(agent-core-v2): drop definition-level capability resolution (#2142)
Some checks are pending
CI / lint (push) Waiting to run
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 / 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
Capability resolution now falls back from trait hooks (last declarer
wins) to the protocol base catalog to UNKNOWN_CAPABILITY; the
definition layer is removed from the chain.

- remove ProviderDefinition.capability and the definition branch in
  ProtocolAdapterRegistry.explainCapability
- kimi contrib drops its UNKNOWN_CAPABILITY declarations, so base-known
  model ids resolve through the base catalog instead of being
  suppressed by a vendor-level UNKNOWN
- update composition tests for the new resolution order
2026-07-24 16:04:02 +08:00
liruifengv
a2401cc1ed
feat(kap-server): add provider write endpoints and models.dev/registry import (#2110)
* fix(agent-core-v2): honor explicit undefined field deletes in modelsToToml

The models TOML transform merged each entry as { ...oldRaw, ...converted },
so a field the new record carried with an explicit undefined — deleted from
'converted' by setDefined — was put right back from the old on-disk raw.
Field-level clears issued through config.replace (e.g. the provider write
routes dropping base_url, display_name, capabilities) only took effect in
memory and resurrected on the next boot. Merge via setDefined onto the old
raw directly, matching providerEntryToToml.

* feat(kap-server): add provider write endpoints and models.dev/registry import

Add the provider management write surface plus server-proxied catalogs so
API clients (the desktop app first) can manage providers without editing
config.toml by hand:

- POST/PUT/DELETE /providers: manual create, replace-style edit (new_id
  rename with pointer migration, tri-state api_key, form-unknown fields
  merged through), and delete with real alias removal.
- GET /providers/{id} additionally reveals the stored api_key for edit
  prefill on the loopback+bearer transport; the list stays redacted.
- GET /catalog/providers[{id}]: pruned models.dev directory with import
  eligibility resolved server-side (10-min cache, stale fallback, built-in
  snapshot, shared in-flight fetch).
- POST /providers:import_catalog and :import_registry (collection actions
  next to :refresh): catalog/registry imports with TUI-aligned
  remove-then-apply refresh semantics, OAuth-managed guards, and the
  registry source blob that scheduled refreshes rediscover.

Write-path consistency: field-level clears assign explicit undefined (the
TOML transforms only drop keys that way), import refreshes swap aliases in
two passes so stale on-disk fields cannot ride along, re-imports keep the
stored credential when api_key is omitted, and multi-step writes serialize
through a process-local chain. Global default_provider/default_model are
never modified by provider writes.

* fix(kap-server): cache the built-in catalog fallback and guard alias-key collisions on replace

- Cache the built-in snapshot on upstream failure, so an offline install no
  longer pays the full upstream timeout on every catalog call.
- Reject a PUT whose rebuilt alias keys collide with an alias owned by
  another provider (foreign-prefix keys are global), checked before any
  write so a collision never lands half the edit.

* fix(kap-server): make toCatalogProviderItem's switch explicit for older TS control-flow

* chore(agent-core-v2): drop inline rationale per package comment conventions

* feat(kap-server): seed default_model on provider create/import when none is configured

A fresh setup has no global default model, so the first provider added
through the write endpoints left GET /auth permanently unready and new
sessions without a selected model. Seed default_model from the created
provider's default (or first) model on POST /providers, :import_catalog
and :import_registry when — and only when — no default is configured;
an existing pointer is never moved, not even a dangling one.

* refactor(agent-core-v2): own the models.dev provider import in the engine

kap-server's provider write endpoints imported @moonshot-ai/kosong and
@moonshot-ai/kimi-code-oauth directly for the models.dev browse/import
and custom-registry import. Move that capability behind a new App-scope
IModelsDevImportService so the server only talks to agent-core-v2:

- app/kosongConfig/modelsDev.ts mirrors the third-party api.json schema
  and hosts the pure translation (wire inference, endpoint resolution,
  model pruning, thinking/gateway overrides) ported from kosong's
  catalog.ts; kosong's own type surface stays limited to built-in
  vocabulary
- modelsDevUpstream.ts owns the directory fetch, 10-minute cache, and
  built-in snapshot fallback; modelsDevImportService.ts owns the import
  orchestration (tri-state api_key, two-pass alias swap, OAuth-managed
  rejection, serialized writes) with coded modelsDev.* errors
- kap-server routes shrink to wire mapping (zod schemas + numeric error
  code mapping) and drop the kosong/oauth dependencies

The /api/v1 surface is byte-identical: the apiSurface snapshot and all
existing modelCatalog tests pass unchanged.

* chore: drop the branch's changesets

* fix(agent-core-v2): route the models.dev always-thinking exemption through kosong

The zero-tolerance vendor-name gate forbids a 'kimi' compare outside the
kosong layer, and the engine-side models.dev port carried one inline.
Kosong (the vendor layer, owner of thinking semantics) now answers the
verdict — wireHasProtocolThinkingDisable — and kosongConfig/modelsDev.ts
asks it instead of comparing wire ids.

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-07-24 14:00:16 +08:00
liruifengv
c2b2c4eb49
docs(changelog): sync 0.29.1 from apps/kimi-code/CHANGELOG.md (#2136) 2026-07-24 13:56:33 +08:00
github-actions[bot]
f4c3967a41
ci: release packages (#2061)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-24 13:23:37 +08:00
liruifengv
dad11ed44e
chore: prune non-user-facing changesets (#2134)
* chore: prune non-user-facing changesets

Remove entries covering only agent-core-v2 internals, kap-server
protocol/endpoint changes, and experimental-engine behavior; the
underlying changes still ship with the next user-facing release.
Downgrade the MCP timeout and web service env var additions from
minor to patch, as both extend existing configuration surfaces.

* chore: document user-facing criteria in gen-changesets skill

Add Core Rule 6 to skip changesets for changes users cannot
perceive, note pre-release pruning in the workflow, and classify
configuration additions to existing features as patch.
2026-07-24 13:17:36 +08:00
7Sageer
7b62ed5b2c
feat: support a configurable secondary model for subagents (#2064)
* feat: support a configurable secondary model for subagents

* refactor: move the subagent model config to a consumer-neutral [secondary_model]

The secondary model becomes a model-domain concept next to default_model so
future consumers beyond subagents can share it: [secondary_model] model /
effort in config.toml, KIMI_SECONDARY_MODEL / KIMI_SECONDARY_EFFORT env
overrides, and the Agent / AgentSwarm per-spawn choice renamed from
"subagent" to "secondary".

* docs: replace "v2 engine only" notes with the concrete effective surfaces

State that [secondary_model], its env overrides, the Agent/AgentSwarm
model parameter, and SYSTEM.md take effect only under kimi web and
experimental kimi -p (the TUI ignores them), and that --agent /
--agent-file are available only under experimental kimi -p. Also drop
the SYSTEM.md claim of parity with --agent/--agent-file, which was
inaccurate: SYSTEM.md is an agent-core-v2 app-domain feature and also
applies under kimi web, while the flags are gated at the CLI.

* fix: review follow-ups for the subagent secondary model

- Drop the "cheaper" claim from the Agent/AgentSwarm model parameter
  descriptions and the advertised model list — the secondary model is
  not necessarily the cheaper one.
- Downgrade the changeset to patch, note the kimi web / experimental
  kimi -p effective surface, and tighten the wording.
- Remove the onWillRestore stub fields from two lifecycle stubs; they
  belong to upcoming lifecycle work, not to this change.

* fix(agent-core-v2): prevent ghost agents from invalid model bindings

* fix: narrow the secondary-model error hint to missing-alias failures

The model catalog's not-configured throw now carries details.model, and
wrapSubagentModelError only decorates errors whose details.model matches
the bound model. Malformed [models.*] entries and unrelated config.invalid
failures during agent creation pass through untouched instead of being
misattributed to an invalid secondary-model alias.

* fix: mark subagent resume semantics as breaking

* chore(agent-core-v2): follow header-only comment convention

* fix(agent-core-v2): await agent restore preparation

* feat(agent-core-v2): support agent model preferences

* Update subagent-secondary-model.md

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>

* fix: validate secondary models before agent creation

* Delete .changeset/secondary-model-startup-warning.md

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>

* Add secondary_model config section for subagents

Individual agents can override this via the new `model_preference` field in their agent file.

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

* feat(agent-core-v2): support override patches in [secondary_model]

The recipe is now `model` plus the flattened ModelOverride field set.
With any patch field set, a config overlay synthesizes a derived
registry entry (base copy, patch merged into overrides, aliases
dropped) so subagent spawning rides the standard effectiveModelConfig
merge; with none, subagents bind the pointed entry directly.
`default_effort` replaces `effort` (KIMI_SECONDARY_EFFORT rebinds)
and doubles as the explicit subagent thinking; unset, thinking
resolves naturally instead of inheriting the caller. The overlay
strips the derived entry (and any defaultModel pointer to it) from
writes, and the kap-server GET /models route hides it from pickers.

* fix(agent-core-v2): fire section events for overlay-rewritten domains

rebuildEffective only committed the caller-named domains, so a
ConfigEffectiveOverlay or section env binding that rewrote a sibling
domain (setting [secondary_model] synthesizes a derived models entry;
removing the recipe retracts it) left consumers of the models section
stale. Widen the commit candidates with every domain the recompute
actually changed; commit() deepEqual-guards each candidate, so the
widening costs nothing.

* feat(agent-core-v2): gate secondary model behind experimental flag

* Update subagent-secondary-model.md

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

---------

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>
Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-24 12:16:53 +08:00
Kai
66f611aae9
fix: echo thinking under the reasoning field the endpoint actually uses (#2104)
Some checks are pending
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 / 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 / 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
OpenAI-compatible endpoints disagree on the wire field for reasoning
content: `reasoning_content` (DeepSeek/Moonshot convention, older vLLM)
vs `reasoning` (OpenAI GPT-OSS guidance, current vLLM — which also only
accepts `reasoning` on the request side, vllm-project/vllm#38488).
Reading only `reasoning_content` silently dropped thinking, and the lost
thinking then never made it back into later requests.

Add per-endpoint dialect detection: inbound responses are scanned in
priority order (reasoning_content, reasoning_details, reasoning; first
string value wins), the carrying key is remembered, and outbound
messages echo thinking under the same key (default reasoning_content).
An explicit `reasoningKey` / `reasoning_key` config still pins the
dialect.

- kosong: shared reasoning-key module; wire the kimi and openai-legacy
  providers; the dialect cell is shared across per-step provider clones.
- agent-core-v2: same mechanism in the vendored openai-legacy base; the
  kimi trait no longer pins reasoning_content (the default already is),
  keeping Moonshot wire behavior byte-identical while adapting to vLLM.
- agent-core: memoize the base provider behind ConfigState.provider so
  the detected dialect survives across turns instead of being rebuilt
  per access.
2026-07-23 21:15:00 +08:00
Haozhe
d751b6796c
feat(kap-server): global session work status, transcript subscribe_v2, and plan endpoint (#2094)
* feat(session): add core work aggregate and consume it at WS/REST edges

- agent-core-v2: add the sessionActivity domain (ISessionActivityView,
  Session scope) folding each agent's activity view and the interaction
  kernel into busy / main_turn_active / pending_interaction /
  last_turn_reason, firing cause-tagged change events only on real
  tuple changes
- kap-server: resolveSessionFacts reads the core view; the WS
  broadcaster drops its per-agent fold/dedup and delegates to the
  view, deferring turn_ended emissions until after the turn.ended
  frame so busy:false never precedes it; global events now fan out
  to every established connection via addGlobalTarget without any
  subscription
- kimi-inspect: add src/activity (GlobalEventsWs + SessionActivityHub
  + useSessionActivities) consuming the global push; the Sidebar
  renders running / approval / question / failed badges per session

* feat(kap-server): decouple transcript stream from agent_filter

- transcript frames (transcript.reset/ops) are governed by the per-agent
  transcript grades alone and bypass the legacy agent allowlist; the filter
  still gates session_event delivery
- client_hello is handshake-only in code: hello and subscribe share one
  attach path, and hello's inline subscription fields are deprecated
  (subscriptions made optional) in both protocol packages
- flush deferred work_changed(busy:false) from a microtask so it always
  lands after the matching turn.ended frame

* fix(kimi-inspect): restore session activity hub in the browser

- bind the default fetch in SessionActivityHub: a member call hits the
  browser's Illegal invocation (receiver is not the global object), and
  the swallowed error left the REST seed never firing, so the Sidebar
  badges never populated on refresh
- create the hub in useEffect + useState instead of useMemo: under
  StrictMode the first mount's cleanup closes the memo-created hub for
  the rest of the page's life

* chore: add changeset for the global session work status push

* feat(kap-server): add transcript plan endpoint for ExitPlanMode calls

- add GET /sessions/{id}/transcript/plan projecting one ExitPlanMode
  call's plan info (content, path, options, review outcome) from the
  first available fact: the linked approval interaction's request
  display, the live tool frame's display, or the tool result output
- add TOOL_CALL_NOT_FOUND (40416) error code
- add transcriptPlanResponseSchema to the transcript contract
- cover live, auto-mode, cold-rebuild, and error paths in tests
- update the API surface snapshot and AGENTS.md

* feat: move transcript subscriptions to subscribe_v2 and extend the plan endpoint

- add subscribe_v2 / unsubscribe_v2 WS control frames as the only
  carrier of per-agent transcript grades and the transcript_since
  cursor; client_hello / subscribe no longer accept transcript fields
- serialize control-frame handling per connection so interleaved
  attaches cannot overwrite fresher subscription state
- make tool_call_id optional on GET /sessions/{id}/transcript/plan:
  omitted lists every ExitPlanMode call with recoverable plan content
  as a plans array
- add a Plan lookup card to the kimi-inspect agent inspector via
  fetchTranscriptPlan
- add TOOL_CALL_NOT_FOUND (40416) to the shared protocol error codes
- update AGENTS.md and add changesets
2026-07-23 18:48:25 +08:00
7Sageer
5fdbdb4a22
feat: configure web search/fetch services via KIMI_WEB_* env vars (#2096)
* feat: configure web search/fetch services via KIMI_WEB_* env vars

KIMI_WEB_SEARCH_BASE_URL / KIMI_WEB_SEARCH_API_KEY and
KIMI_WEB_FETCH_BASE_URL / KIMI_WEB_FETCH_API_KEY overlay the
[services] config section field by field in both engines (env wins
over config.toml), so the WebSearch and FetchURL backends can be
pointed at a Moonshot service without OAuth login. The v2 engine now
also honors the explicit [services.moonshot_fetch] section
(config > managed OAuth > local), which it previously parsed but
never consumed.

* fix(agent-core-v2): guard stripServicesEnv against clearing the services section

config.replace(SERVICES_SECTION, undefined) — the logout deprovisioning
path in authService — passes undefined straight into the section strip,
and the composed stripServicesEnv dereferenced it (value[key]), throwing
TypeError and aborting logout mid-cleanup. Add the same isPlainObject
guard the other strip implementations (stripProvidersEnv,
stripEnvBoundFields) already have, plus a regression test that also
locks in the env overlay staying effective after the file value is
cleared.

* style(agent-core-v2): follow header-only comment convention

* fix: isolate env web service credentials

* Add env vars for web search and fetch services

The new environment variables `KIMI_WEB_SEARCH_BASE_URL`, `KIMI_WEB_SEARCH_API_KEY`, `KIMI_WEB_FETCH_BASE_URL`, and `KIMI_WEB_FETCH_API_KEY` take priority over the corresponding fields in `config.toml`. The `kimi web` backend now honors the `[services.moonshot_fetch]` config section.

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

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-23 17:36:16 +08:00
7Sageer
527d485d92
feat: add global default MCP server timeout configs (#2065)
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-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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: add global default MCP server startup timeout config

Add a `[mcp] startup_timeout_ms` config.toml section with a
`KIMI_MCP_STARTUP_TIMEOUT_MS` env override as the global default MCP
server connection (startup + tool discovery) timeout. Precedence:
per-server `startupTimeoutMs` in mcp.json > env var > config.toml >
built-in 30s default.

* feat: add global default MCP tool call timeout config

Extend the `[mcp]` section with `tool_timeout_ms` and the
`KIMI_MCP_TOOL_TIMEOUT_MS` env override as the global default for
single MCP tool calls, mirroring the startup timeout: a per-server
`toolTimeoutMs` in mcp.json still wins, and unset entries fall back to
the SDK built-in 60s default.

* chore: shorten the mcp timeouts changeset

* feat(agent-core): add global default MCP server timeout configs

Port the `[mcp]` section (`startup_timeout_ms` / `tool_timeout_ms`)
and the `KIMI_MCP_STARTUP_TIMEOUT_MS` / `KIMI_MCP_TOOL_TIMEOUT_MS` env
overrides to agent-core (v1), mirroring the v2 semantics: per-server
fields in mcp.json > env vars > config.toml > built-in defaults. The
v1 TOML loader gains explicit `mcp` read/write mappings, and both
connection-manager construction sites (Session, testGlobalMcpServer RPC)
pass the resolved defaults through.

* fix(agent-core): validate and apply MCP timeout defaults

* fix: propagate MCP startup timeout to SDK requests

* refactor(agent-core-v2): resolve MCP default timeouts at connect time

Keep the session connection manager synchronously lazy instead of gating
its existence on config readiness: resolveDefaultTimeouts is read from the
mcp config section at each (re)connect, so AgentMcpService's eager
construction stays unconditionally safe and reconnects pick up changed
preferences. The initial connect still awaits config.ready for a
deterministic snapshot. Also restore the ISessionMcpService method docs,
bump the changeset to minor, and fix the v1 env-parse comment.

* chore(agent-core-v2): regenerate config manifest for the mcp section

* Add global default MCP server timeouts configuration

Specify the new global default MCP server timeouts in both the config file and environment variables.

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

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-07-23 12:41:08 +08:00
liruifengv
ca38b7ed86
chore: remove superpowers toolbar tip (#2089) 2026-07-23 12:34:14 +08:00
Haozhe
5240b5c83c
feat(agent-core-v2): add generated config and wire-protocol manifests (#2086)
* feat(agent-core-v2): add generated config section manifest

- add scripts/gen-config-manifest.mts: drains the live
  registerConfigSection / registerConfigOverlay contributions and renders
  docs/config-manifest.toml in the on-disk config.toml shape (owner,
  scope, registered defaults, env bindings, schema fields)
- add a gen:config-manifest package script (--check mode included) and a
  freshness test that rebuilds the manifest and compares byte-for-byte
- point the agent-core-dev config skill and the package AGENTS.md at the
  generated manifest instead of the stale hand-maintained ownership map

* feat(agent-core-v2): add generated wire-protocol manifest

- add scripts/gen-wire-manifest.mts to generate docs/wire-manifest.d.ts
  from defineOp registrations (payload interfaces, persist policy,
  toEvent, cross-reducers) plus a WirePayloadMap
- extract shared JSON Schema helpers from gen-config-manifest.mts into
  scripts/lib/jsonSchema.mts
- add gen:wire-manifest script and wireManifest.test.ts freshness check
- document the manifest in packages/agent-core-v2/AGENTS.md

* fix(agent-core-v2): keep array-of-tables manifest sections fully commented

A bare `[hooks]` header parses as a plain table, which array sections
reject on load; emit only the commented `[[hooks]]` shape so the
manifest matches the on-disk config.toml shape it documents.

Addresses a Codex review comment on PR #2086.
2026-07-23 11:17:10 +08:00
Haozhe
188c0fcbf7
refactor(agent-core-v2): decouple kosong from config persistence (#2068)
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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* refactor(agent-core-v2): decouple kosong from config persistence

- keep the kosong provider/model registries in memory and sync them with
  config.toml through a new app/kosongConfig two-way bridge: hydrate on
  startup, push config section changes into the registries, and persist
  runtime mutations (discovery refresh, OAuth provisioning, default-model
  pointer changes) back to disk
- declare the providers/models/thinking section constants, zod schemas,
  env bindings, and TOML transforms in a single app/kosongConfig
  configSection.ts; kosong keeps hand-written types only
- pin every schema to its kosong type at compile time via
  AssertExact<Equal<...>> (_base/utils/typeEquality)
- register a transitional auth>kosongConfig exception in the domain-layer
  checker for the OAuth provisioning flows

* chore(agent-core-v2): drop unused IConfigService import from authLegacy

* fix(agent-core-v2): reconcile registry with env-pinned default pointers

A registry-originated default-model/provider write lands only in the
config user layer when an effective overlay pins the section
(KIMI_MODEL_NAME pins defaultModel to the reserved env model): the
effective value does not move and no change event fires, so the registry
diverged from the effective config view — catalog/auth reads reported the
user pick while profile resolution kept the pinned model. After
persisting, the bridge now re-asserts the effective value into the
registry, restoring the pre-refactor behavior where every default-pointer
write was arbitrated by the effective view.

* fix(agent-core-v2): await persistence before resolving kosong registry mutations

ProviderService/ModelService mutations resolved as soon as the in-memory
registry updated, while the kosongConfig bridge persisted the change on an
unawaited private chain — callers (klient kosong.*, set_default route,
OAuth provision, discovery refresh) could observe success before the write
reached config.toml, and a restart right after could lose it.

- fire registry change events through AsyncEmitter and await delivery in
  set/delete/replaceAll/setDefaultX, so a mutation resolves only after
  listeners' waitUntil work completes; loadAll keeps synchronous timing
- the bridge hooks its persists into waitUntil, hoists the equality guards
  into the listeners so config-originated echoes stay fully synchronous,
  and serializes persists on the chain as before
- retry a failed persist with bounded backoff (3 attempts); failures are
  logged, never rejected to callers, and the in-memory change stands
- default-pointer change events now carry an { id } payload (fireAsync
  requires object events)
- add the klient kosong-config stress example covering read-after-write,
  concurrent bursts, and restart durability
2026-07-23 00:32:10 +08:00
qer
e0f2a41769
feat(datasource): add wind, imf, gildata, sec_edgar, and sp_data sources (#2029)
Some checks are pending
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 / test (4) (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 / Publish native release assets (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
* feat(datasource): add wind, imf, gildata, sec_edgar, and sp_data sources

* feat(datasource): strengthen source routing in skill and tool schema

* feat(datasource): retry on credential rotation and enforce single-source routing contract

* feat(datasource): neutral source selection with objective capability boundaries

* feat(datasource): note wind_search_fields field-name mapping in skill and schema
2026-07-22 22:45:55 +08:00
Haozhe
64f053cf46
feat: agent-core-v2 permission/workspace refactors and transcript durability (#2021)
* refactor(agent-core-v2): extract toolApproval domain from permissionGate

- add agent-scoped `toolApproval` domain owning the approval round-trip:
  builds approval requests, drives the session/approval broker, publishes
  permission.approval.* events, records session approval rules, and
  resolves ask continuations
- slim permissionPolicy down to the static risk-adjudication chain; drop
  the dynamic `registerPolicy` mechanism
- move harness constraints out of the policy chain into their owning
  domains as toolExecutor hooks ordered before 'permission': plan-mode
  guard (plan), swarm batch exclusivity and AgentSwarm approve (swarm),
  goal-start review (goal), exit-plan review (plan/exitPlanModeReview),
  and btw deny (session/btw)
- delete the now-unused policies (deny-all, plan-mode-guard-deny,
  plan-mode-tool-approve, goal-start-review-ask, swarm-mode-agent-swarm-
  approve, agent-swarm-exclusive-deny)
- update Permission.md, AGENTS.md, and check-domain-layers for the new
  domain layout

* feat(kimi-inspect): add App Services view for app-scope service reflection

- add `services` top-level view (`AppServicesView`) on the NavRail, showing
  the full-width app-scope Service panel grid; session/agent scopes stay in
  the Chat view's Inspector
- extract shared `ServicePanels` from Inspector and add `methodArgs` helper
  to build per-method argument editors from channel metadata
- support variadic service calls in `panels.ts` (`call(svc, method, ...args)`)
- update agent-core-dev skill docs for the toolApproval extraction and the
  guard/review-off-chain permission design

* refactor(agent-core-v2): restructure workspace domains

- rename workspaceRegistry to workspace and workspaceLocalConfig to
  projectLocalConfig
- extract id-spelling resolution into the workspaceAliases domain
  (IWorkspaceAliases)
- extract workspace-centric session queries into workspaceSessions
  (IWorkspaceSessions)
- update kap-server routes, klient contracts, and related tests to the
  new services

* feat(transcript): add op-batch sequencing and point-to-point catch-up

- wire: transcriptSeqSchema with per-agent batch seq watermark on
  transcript.reset/ops and the REST transcript response, the
  transcript_since subscription cursor, and the GET transcript/ops
  catch-up shape; seq stays optional everywhere so legacy peers fall
  back to loss-signal-driven refreshes
- wire: drop interactionFrame, interactions live on the interaction
  item in the ops stream
- kap-server: TranscriptService assigns consecutive per-agent batch
  seqs and retains them in a bounded in-memory journal; the
  transcript_since cursor replays journaled batches instead of a
  baseline reset, and the baseline reset is now items-empty because
  history always pages in over REST
- kimi-inspect: transcript REST/WS clients track the op-batch
  watermark and run seq-gap/reconnect catch-up with full-refresh
  fallback; add the Transcript audit panel (AuditTrail timeline,
  structural diff, state tree) docked right of the chat
- docs: sync AGENTS.md and agent-core-dev skill notes with the new
  transcript contract

* feat(transcript): persist plan revisions and task/interaction facts, add user-messages endpoint

- record each ExitPlanMode submission as a versioned plan blob via a reference-only plan.revision op, projected live and cold as a plan.revision marker plus the plan badge (reviewPath, version)
- persist task.started/task.terminated (with a bounded output tail) and interaction.request/interaction.resolved ops, and add foldFacts so a cold transcript rebuilds tasks, interactions, todos and goal/plan/swarm meta from the wire journal
- add GET /sessions/{session_id}/transcript/user-messages returning all turn-opening prompts grouped per agent

* fix(agent-core-v2): anchor external PreToolUse hooks before the permission gate

The toolApproval extraction forces the gate to construct early (planService injects it to anchor plan-guard), so the 'permission' hook registered ahead of 'externalHooks' and a policy ask waited on the approval broker before PreToolUse could block, hanging the turn. Fetch the gate first in registerListeners and register the PreToolUse hook with before: 'permission', falling back to appending when the gate is stubbed without its hook.

* refactor(agent-core-v2): replace ordered onBeforeExecuteTool hook with veto-event pattern

- introduce BeforeToolExecuteEvent with veto/allow/pass/waitUntil statements
- add BeforeToolExecuteEmitter with two-pass fire (immediate then deferred)
- split readiness work into separate onWillExecuteTool participation event
- migrate all domain listeners: permissionGate, plan, goal, swarm, btw,
  externalHooks, toolDedupe, mcp
- remove IAgentPermissionGate force-injection for hook ordering
- update docs, tests, and domain-layer check to match

* refactor(agent-core-v2): unify veto payload on ExecutableToolResult

- veto() and waitUntil factories now carry a plain ExecutableToolResult:
  isError reads as a denial, anything else as a short-circuit;
  the block/reason/syntheticResult weak union is gone
- add denyToolExecution(reason) helper for the common denial shape, and
  narrow the fire/authorize return to BeforeExecuteDecision ({ veto } or
  { executionMetadata })
- narrow the policy 'result' resolution to { kind: 'result'; result }
- settle vetoed calls through a single normalize/merge path in the executor

* fix(agent-core-v2): pull up IAgentPermissionGate in agent activation

The permission gate only subscribes `onBeforeExecuteTool` from its
constructor. The veto-event refactor removed the ordering-driven
force-injections that used to pull it up, so without an explicit
resolution tool execution would run without policy adjudication.

* refactor(agent-core-v2): fold systemReminder domain into contextMemory appendTagged

- add `appendTagged(content, tag, origin)` to `IAgentContextMemoryService`,
  storing content pure with a `tag` field on `ContextMessage`
- apply the XML tag at projection time in `contextProjector` via the new
  `tag.ts` helpers (`wrapTag` / `applyTagToContent`)
- delete the `systemReminder` domain and migrate all call sites
  (contextInjector, goal, plugin, prompt, swarm, btw, sessionInit,
  toolSelectAnnouncements) to `appendTagged`
- build toolDedupe reminder strings with `wrapTag`

* refactor(klient): merge providers/models/catalog into global.kosong facade

Converge three separate facade namespaces (global.providers,
global.models, global.catalog) into a single global.kosong facade
that exposes two domain concepts: provider (CRUD) and model
(read-only view). Add streaming generate() method for direct
LLM calls through the facade.

- Define ProviderAuth (api-key | oauth), ProviderInput,
  AnonymousProviderInput, GenerateInput, GenerateParams,
  GenerateEvent as klient-owned public types
- Extend KlientChannel with stream() for AsyncIterable transport
- Add streaming IPC protocol (stream/stream_data/stream_end/
  stream_error/stream_cancel frame types)
- Add StreamingProcedureContract, ScopedStreamCaller, and
  per-chunk zod validation in the contract layer
- Implement generate via dispatcher special-case routing to
  IModelCatalog.getRequester().request()
- Rename events: providers.changed -> kosong.providers.changed,
  models.changed -> kosong.models.changed,
  catalog.changed -> kosong.changed
- Remove GlobalProvidersFacade, GlobalModelsFacade,
  GlobalCatalogFacade, and ModelRecord from public exports
- Update all tests, examples, and README

BREAKING CHANGE: global.providers, global.models, and
global.catalog replaced by global.kosong; event names changed;
ModelRecord no longer exported.

* feat(transcript,kimi-inspect): add tag field to text frames and improve session creation

transcript:
- add optional `tag` field to TextFrame, textFrameSchema, and HistoryMessage
- propagate tag through contextTranscript MutableMessage

kimi-inspect:
- render tagged frames with violet badge and distinct styling in ChatView
- skip cwd prompt for workspace-based session creation in Sidebar
- auto-bind default model on new sessions via resolveDefaultModel

* refactor(transcript): rename wire/ directory to contract/

The transcript package's REST/WS schemas and event types lived in
src/wire/, which collided with the engine's persisted wire.jsonl
record vocabulary. Rename it to src/contract/ so "wire" unambiguously
refers to wire.jsonl records (foldWireRecordFacts, HistoryWireRecord
stay unchanged).

- rename src/wire/{schema,events}.ts to src/contract/
- update index exports and test imports accordingly
- reword comments: "wire shape" -> "contract shape", "on the wire" ->
  "in ops" / "in transit" / "on the WS channel" / "transcript API"
- events.ts: "transcript frame" -> "transcript event" for WS envelope
  messages, avoiding confusion with TranscriptFrame
- kap-server tests: TranscriptWire/TurnWire/FrameWire/OpsCatchupWire/
  UserMessagesWire -> *Contract
- update AGENTS.md references

* refactor(transcript): rename wire/ directory to contract/

The transcript package's REST/WS schemas and event types lived in
src/wire/, which collided with the engine's persisted wire.jsonl
record vocabulary. Rename it to src/contract/ so "wire" unambiguously
refers to wire.jsonl records (foldWireRecordFacts, HistoryWireRecord
stay unchanged).

- rename src/wire/{schema,events}.ts to src/contract/
- update index exports and test imports accordingly
- reword comments: "wire shape" -> "contract shape", "on the wire" ->
  "in ops" / "in transit" / "on the WS channel" / "transcript API"
- kap-server tests: TranscriptWire/TurnWire/FrameWire/OpsCatchupWire/
  UserMessagesWire -> *Contract
- update AGENTS.md references

* Revert "refactor(agent-core-v2): fold systemReminder domain into contextMemory appendTagged"

This reverts commit 55afaa3d96f729c4f73a71f1fbd23d3f6087453b.

Restore the systemReminder domain: reminders go back to being baked
into message text at write time, and ContextMessage loses the `tag`
field (projection-time wrapping is removed with it).

* feat(transcript): add wire-equivalent detail, dedupe session events

- transcript: add step usage/timing/retry, turn durationMs/error/usage,
  tool inputText/progress, task resultSummary/error, meta.agent status,
  a global prompts entity, and the 'hook' marker
- kap-server: project the new fields in coreEventMap and suppress
  transcript-projected session events on connections subscribed to the
  transcript protocol (live fan-out and cursor replay)
- kimi-inspect: mechanical type sync for the new snapshot prompts field

* fix(klient): resolve lint errors in ipc channel stream and e2e matrix test
2026-07-22 19:21:56 +08:00
Haozhe
c6291c3ad7
feat(v2-print): align kimi -p run lifecycle with the default engine (#2017)
* feat(v2-print): align kimi -p run lifecycle with the default engine (13 files)

- add applyPrintModeConfigDefaults in agent-core-v2: fill print-mode config
  defaults (bash task timeout / max steps per turn / subagent timeout all
  unbounded) into the memory layer, respecting user-set keys
- applyPrintBackgroundPolicy: keep the run alive while cron tasks have
  future fires so their steered turns can run, with an anti-spin guard for
  wedged ticks; goal/cron waiting shares the steer ceiling budget
- default print background mode changed from exit to steer
- add task.bashTaskTimeoutS config key and wire it into BashTool's
  detachTimeoutMs
- v2 print runner now forwards turn.step.retrying events to writeRetrying
- allow subagent timeoutMs = 0 (no timeout)

* test(agent-core-v2): remove duplicate loop config imports in config test
2026-07-22 19:21:39 +08:00
liruifengv
b32170b018
docs(changelog): sync 0.29.0 from apps/kimi-code/CHANGELOG.md (#2054) 2026-07-22 18:23:48 +08:00
github-actions[bot]
8bf5bacba9
ci: release packages (#1989)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-22 17:24:39 +08:00
Haozhe
8250e590f3
docs(cron): drop references to the non-existent kimi resume command (#2050)
* docs(cron): drop references to the non-existent `kimi resume` command

- point user docs at the real resume command `kimi --session`
- reword cron tool descriptions and code comments in agent-core and
  agent-core-v2 to describe session resume without naming a subcommand

* chore: add changeset for cron docs wording fix
2026-07-22 16:59:08 +08:00
Kai
4c763f6763
feat: send prompt-attached videos directly with the prompt (#1999)
Some checks are pending
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 / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Release / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
CI / test-windows (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: send prompt-attached videos directly with the prompt

Videos attached to a prompt (pasted in the TUI, uploaded in the web UI)
previously reached the model only after it opened the file with
ReadMediaFile — an extra tool round trip that could leave the video
unseen when the model never made that call. They are now uploaded
through the model provider's file channel and embedded directly in the
user message as the provider-issued reference; ReadMediaFile stays as
the fallback and as the model's own way to open video files.

- agent-core: new uploadVideo agent RPC and Session.uploadVideo in the
  SDK; the TUI uploads pasted videos at submit time, falling back to
  the file-tag form on failure
- kap-server: inline file-source video prompt parts at the REST edge
  and map provider file ids back to local uploads behind
  GET /files/llm/{llm_id}
- kimi-web: play provider-referenced prompt videos after reload/resume

* fix: fall back to inline video when the upload channel fails

ReadMediaFile only used the provider's video upload channel when one
was bound, and surfaced a hard tool error when the upload itself
failed — on providers without a files endpoint (or a transient upload
failure) the model was told the video could not be read at all. Now a
missing or failing upload falls back to delivering the video inline
(base64), the same shape providers without an upload channel already
get. Both engines (agent-core and agent-core-v2) are fixed.

* test: cover prompt video edge cases and failure paths

Stress coverage for the prompt video pipeline:

- agent-core uploadVideo RPC: extension/magic classification
  (.txt with video bytes accepted, extension trusted when magic is
  absent), exact 100MB boundary, directory and nonexistent paths
- TUI: mixed per-video outcomes (one inlined, one tag fallback),
  submission order behind an in-flight upload, queued video messages
  carrying final uploaded parts
- kap-server: per-video provider-id mappings, Range requests through
  the llm redirect, mapping persistence across a server restart

* fix: surface auth rejections from the video upload channel

The base64 fallback for a failing video upload must not mask auth
rejections: a 401/403 (surfaced as provider.auth_error) drives the
credential force-refresh and a clear auth error, while an inline
payload would just be rejected again by the next request. Only a
missing or broken upload channel (no files endpoint, network/server
errors) falls back to inline delivery.

* fix: keep the by-design no-hook video error from degrading to inline

Main's contract for a provider with no video upload hook is an honest
"does not support video upload" tool error — an inline payload would
be dropped on that protocol's wire anyway. The base64 fallback now only
applies to an upload channel that exists but failed at runtime. The
no-hook throw gets a stable type (VideoUploadUnsupportedError) so the
two cases are told apart without matching message text.

* fix: constrain the llm video route param to a safe alphabet

The provider file id is used as a blob-store key, and the node-fs
backend joins scope and key into a storage path. An id containing an
encoded path separator (%2F) could address a different storage path
than the intended llm-video mapping; the route now only accepts the
provider-id alphabet.

* style(agent-core-v2): move added explanations into top-of-file comment blocks

The package's comment convention keeps comments solely in the top-of-file
block; relocate the new notes (url-source id pairing, video delivery
fallback, VideoUploadUnsupportedError) from functions and schema fields
into their module headers.

* fix: reject prompt video uploads when the model lacks video input

The uploadVideo RPC only checked the provider's upload channel, so an
SDK caller on a text-only or unknown-capability model could obtain a
valid-looking video_url part the current model is not supposed to
accept. The TUI capability guard does not cover this public path, so
the agent now gates on video_in itself.

* fix: sniff uploaded video bytes before inlining them

The inline branch trusted the upload-time content type; now the bytes
are sniffed first, and anything magic-confirmed as a non-video kind
(e.g. an image mislabeled as video/mp4) falls back to the file-tag
form instead of being uploaded. Like the image gate, bytes are
authoritative where the container format allows it — an MPEG-PS
lookalike still rides the extension, matching ReadMediaFile.

* test: keep the generation stub pending until the abort lands

An immediately-answered 404 ends the turn (non-retryable) before the
test's abort call, racing the cleanup into a 409; the stub now hangs
generation until the abort cancels it.

* fix: validate prompt file references before mutating session controls

A stale file_id failed inside media resolution — after the model,
thinking and permission overrides had already been applied — so a
rejected prompt still changed the session's controls. File references
are now checked up front, keeping failed submits side-effect free.

* fix: serialize prompt submissions per session

A slow provider video upload let a later text-only request reach the
queue ahead of an earlier video one, silently reordering the
conversation for REST clients and multiple tabs. Submissions to the
same session now chain, matching the ordering the TUI already
guarantees locally.

* fix: play reloaded provider videos through the authenticated fetch path

A video recovered from an ms:// reference carried only the bare redirect
URL, which 401s under daemon auth when loaded natively. The attachment
now keeps the provider file id (llmFileId) end to end, and AuthMedia
fetches the bytes with the Bearer credential through the daemon's llm
redirect — the same blob-URL path uploaded files already use.

* fix: fence pending video submits against session and model switches

A slow paste-upload left the TUI idle, so /new, the session picker or
/model could fire mid-upload; the continuation then dispatched the old
session's provider reference into the newly selected session or a
model that cannot resolve it. The dispatch now re-checks that the
session and model are unchanged and asks for a resend instead.

* fix: preserve the caller's video path verbatim in uploadVideo

Trimming the path changed the filesystem target before validation and
upload, so a name that legitimately starts or ends with whitespace
resolved to the wrong file; the trim is now only the emptiness check.

* fix: forward provider-issued ids on image URL prompt parts too

The shared url-source schema accepts id for image and video parts, but
only the video path forwarded it, dropping provider-keyed image ids
between prompt acceptance and the model request.

* fix(web): reconcile inlined video echoes into the optimistic user message

The loose user-message matcher counted media parts and <video path>
tags but not the [video:ms://…] text shape, so a racing server echo of
an inlined upload slipped through as a duplicate user bubble.

* fix: queue bash submits behind a pending video upload

A bash-mode submit could start while a pasted video was still
uploading, recording shell context before the earlier prompt was
dispatched and reordering user actions; it now chains behind the
upload like normal submits.

* test: cast the driver through unknown for the session-switch fence test

* fix: validate prompt file references before resolving the prompt agent

A stale file_id posted to a fresh or cold session materialized the main
agent (registering it in session metadata and igniting agent-scoped
services) before the request was rejected. The file check now runs
first, so failed submits create nothing and mutate nothing.

* fix: key the playback mapping by the id embedded in the reference URL

Projections and clients read the provider id from the ms:// URL, not
the id field, so an upload that returns an id-less or mismatched
reference would inline fine but 404 on playback. The mapping now
derives its key from the URL, falling back to the explicit id.

* fix: sanitize provider video ids on the write side of the playback map

The read route got the safe-alphabet guard, but recordLlmVideoRef still
used the provider-returned id verbatim as a blob-store key; a crafted
provider response could write the mapping outside the llm-video
namespace. Out-of-alphabet ids are now dropped on both put and get.

* fix(web): keep recovered provider videos resendable through the edit path

The composer reload path only honored fileId and fell back to a
bare fetch(url) without the Bearer token, so editing a reloaded
provider-video turn dropped the chip after a 401. llmFileId is now
threaded through and the bytes are re-uploaded via the authenticated
llm redirect.

* fix: fall back to inline video for no-hook providers whose wire carries it

The by-design no-hook error is only the honest answer when the wire
would drop an inline payload anyway (the OpenAI family). Protocols
that convert video_url (kimi, anthropic, google-genai, vertex) now
take the base64 fallback instead of failing every video read; the
registrar computes the flag from the model's protocol.

* test: satisfy the full IBlobStore shape in the playback-map stub

The tsgo typecheck job rejects a structural stub missing _serviceBrand
and list even though plain tsc accepted it.

* fix: queue prompt-producing slash commands behind pending uploads

Skill activations and plugin commands started their turn immediately
while a pasted video was still uploading, so the earlier video prompt
queued behind them and user actions ran out of order. Both paths now
chain behind inputSubmitChain (and re-check session/model at dispatch)
like normal and bash submits.

* fix: validate media kinds in the prompt file-reference preflight

The preflight only proved a referenced file exists; a real upload used
with the wrong kind (e.g. a PDF submitted as video) still passed it and
mutated session controls before assertMediaFile rejected the request.
The kind assertion now runs up front with the existence check.

* fix(web): play sent and recovered videos in the file preview

The media preview returned early for every kind except image, so a
user-turn video chip's play action was a no-op even with llmFileId
threaded through. The preview now handles video: bytes come from the
authenticated file/llm fetch into a blob URL and render in a native
player.

* fix(web): preview recovered videos with the authenticated blob URL

The llm re-upload branch fetched bytes with auth but kept the protected
redirect URL as the chip preview, which 401s as a native video src; the
fetched blob now becomes the preview URL, mirroring the fileId branch.

* style(agent-core-v2): move the inline-fallback note into the module header

Same package comment convention as before: rationale lives in the
top-of-file block, not beside the registration call.

* fix: fence delayed bash submits to the originating session

The chained bash callback ran runShellCommandFromInput against whatever
session was active at dispatch time, so a command submitted in session
A could execute in session B's workspace and be recorded there after a
mid-upload switch. The originating session is now captured at submit
and re-verified at dispatch, like the prompt and skill paths.

* fix: emit prompt video telemetry from the agent scope

video_upload is an agent-level event requiring ambient agent identity,
but the uploader was built with the Core-scoped session view, leaving
prompt-upload events unattributable. The route now resolves telemetry
from the target agent for the uploader while image compression keeps
the session-scoped view.

* fix: preserve provider image ids through legacy projections and web mappers

The url-source id accepted by the prompt schema was dropped again by
the legacy message projection and the web wire mapper, so provider-
keyed image references lost their id across messages, snapshots, and
undo responses. It now flows through both directions.

* fix(web): revoke recovered video blob URLs before dropping attachments

A failed llm re-upload removed the attachment without revoking the
freshly created preview blob URL, pinning the whole video in the
browser blob store until page unload on every failed edit attempt.

* fix: serialize foreground slash commands behind pending uploads

/compact and /init started their turn while a pasted video was still
uploading, so the earlier message landed after them — and compaction
summarized the context without it. Prompt-producing builtin commands
now share the same queueBehindPendingUploads chain (with the
session/model dispatch fence) as skill, plugin, and bash submits.

* fix: defer session controls until media preparation succeeds

Media resolution now runs before any profile/model/thinking/permission/
denylist mutation, with the uploader resolved transiently from the
requested (or currently bound) model — a failed submission leaves the
session's controls untouched. A concurrent model switch during
preparation is rejected with session.busy instead of enqueueing a
reference uploaded for the previous model.

* chore: bump the new SDK video upload API as a minor release

Session.uploadVideo is new public API surface, not a patch-level tweak.

* fix: re-check busy state before draining upload-queued commands

A slash command deferred behind a video upload ran the moment the video
prompt dispatched, landing on an already-running turn: beginSessionRequest
wiped the active turn's live pane, and /init or /compact started on top
of it. The deferred callbacks for skills, plugin commands, /compact and
/init now re-run the resolver's busy check at dispatch and show the same
blocked message the user would get when typing while streaming.

* fix: resolve profile-bound models before choosing the uploader

A first prompt carrying "profile" without "model" resolved the upload
model from the still-unbound alias, so no uploader was installed and
every attached video fell back to a tool-read tag even when the
configured default model supports provider upload. The transient
resolution now mirrors AgentProfileService.bind: an explicit body model
wins, a profile bind falls back to the configured default model, and
only otherwise does the currently bound alias apply.

* refactor: resolve prompt videos at request time inside the engine

Move prompt-video delivery out of the submission edge: the TUI submits
synchronously with a local file:// part the v1 turn resolves before the
message enters history, and kap-server carries an internal kimi-file://
reference the v2 requester resolves against the effective model with an
app-scoped upload cache. History keeps the durable local file id, the
/messages projection emits structured video parts, and the web plays
videos back through the authenticated /files channel - deleting the
submit fences, per-session serialization, provider-id reverse mapping,
redirect endpoint, and the unreleased SDK upload API.

* fix: propagate abort through video upload delivery instead of degrading

A turn cancelled mid-upload used to be treated as an ordinary upload
failure: v1 fell back to an inline base64 part and appended the degraded
message to history, and the v2 resolver memoized the tag fallback for the
rest of the agent's lifetime. Both catch sites now check the delivery
signal itself - abort rejections vary in shape by provider - and re-throw
so cancellation ends the turn (v1, classified as cancelled via the abort
reason) or the request (v2, not memoized, so the next turn uploads).

* fix: keep the tag form for no-upload providers whose wire drops inline video

An OpenAI-family model configured with video_in but no provider upload
channel used to receive prompt videos as an inline base64 part - which
chat completions rejects and the Responses adapter degrades to an
omitted-video placeholder, persisting ~4/3x the file size in history for
bytes the model never sees. The prompt path now mirrors the v2 resolver's
protocol gate and degrades to the <video path> tag instead; ReadMediaFile's
own delivery is unchanged. Also merges the prompt-video changesets into a
single user-facing entry.

* fix: escape the NUL separator in the video upload cache key

The cache-key template literal contained a literal NUL byte instead of
the \0 escape, which made Git classify the whole source file as binary
- no inline diffs, unreliable text tooling. The escape produces the
byte-identical runtime string, so hashed cache keys are unchanged.

* fix: check the abort signal before the inline video fallback

The no-uploader inline path (and the post-upload-failure fall-through)
never consulted the delivery signal, so cancelling a turn while the video
bytes were being read still base64-encoded the file and appended the
degraded message to history. The inline branch now re-throws the abort
reason first, matching the upload catch.

* fix: retry transient prompt video upload failures on later steps

A generic upload failure used to memoize its tag fallback for the rest of
the agent's lifetime, freezing a transient files-endpoint error into a
permanently degraded video. The resolver now marks failure-born fallbacks
as non-memoizable: the current request keeps the lightweight tag form and
the next step retries the upload. Structural outcomes (successful uploads,
capability and sniff fallbacks, no-hook inline) stay memoized for
step-retry stability.
2026-07-22 13:46:00 +08:00
Kai
ba921ca531
fix: gate always-thinking inference to OpenAI wires, plus catalog review follow-ups (#2036)
* fix: gate always-thinking inference to OpenAI wires, plus review follow-ups

- catalog: strip the inferred alwaysThinking marker on non-OpenAI wires so
  Claude/Gemini keep their native off; verified against live models.dev
- v1 provider-manager: honor per-alias baseUrl on kimi/google-genai/vertexai
- thinking: rewrite the PHASE-6 contract comment, drop three dead
  cannot-disable warning branches, normalize requested effort in v1 to
  match v2
- tests: input-cap compaction preference in both engines, kap-server WS
  status cap, v2 legacy status input cap
- docs + changeset: new model fields, catalog-refresh behavior, kap-server

* fix: strip always-thinking only where the wire encodes a true off

The previous gate kept the marker only on the OpenAI wires, which wrongly
stripped it from Gemini 3 on the Google wires: its floor is
thinkingLevel MINIMAL with suppressed thoughts — still reasoning — so an
Off option there would be a lie. The criterion is now the wire's encoding,
not its family: strip only on anthropic and kimi, the two wires with a
protocol-level `thinking: {type: 'disabled'}` that the catalog's effort
list can never show. Verified against live models.dev data: google now
marks exactly the gemini-3 family (10), anthropic and moonshotai stay 0.

* docs(agent-core-v2): move the strict-validation contract into the thinking.ts file header

The scoped guide keeps comments in the top-of-file block only; the
function JSDoc shrinks to a short what-it-answers note matching its
neighbors. No behavior change.
2026-07-22 13:43:10 +08:00
Haozhe
430cd382a8
refactor(agent-core-v2): drop pass-through methods from AgentRPCService (#2042)
- trim AgentRPCService/AgentAPI to the 10 methods with real logic
  (prompt, steer, cancel, undoHistory, setPermission, cancelCompaction,
  activateSkill, activatePluginCommand, getContext, getTools)
- route the klient agent facade to the domain services directly
  (shellCommand, profile, usage, plan, task) with new wire contracts
- keep the test harness ctx.rpc surface unchanged via passthrough adapters
- rewire kap-server rpc tests and the kimi-inspect RPC panel
2026-07-22 13:42:01 +08:00
Kai
ec88d352e8
fix: five correctness follow-ups to the catalog metadata work (#2030)
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-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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix: five correctness follow-ups to the catalog metadata work

- Normalize a configured effort value (case/whitespace) before thinking
  resolution on both engines, so "OFF" is read as off instead of being
  sent upstream as an invalid effort.
- Clamp a declared input cap to the effective context window in the
  effective model resolution, so an override lowering max_context_size
  cannot leave a stale larger cap behind.
- Report context usage against the same effective cap
  (max_input_tokens ?? max_context_tokens) in the SDK getStatus and the
  v2 legacy status projections, matching the session-event and service
  surfaces.
- Preserve concretely declared per-model endpoints when the override
  npm is unrecognized, via the same OpenAI-compatible fallback used for
  top-level entries.
- Attribute resolved.maxInputSize and resolved.capabilities
  .max_input_tokens in the model inspector to their config, override,
  or clamp provenance.

* fix: honor observed context caps and publish the effective cap in v2 status

- The overflow-learned provider window is now written into both
  max_context_tokens and max_input_tokens of the effective compaction
  context, so the strategy cannot bypass it by re-selecting the raw
  catalog input cap during overflow recovery.
- agent.status.updated publishes max_input_tokens ?? max_context_tokens,
  matching the other v1/v2 status surfaces.

* fix: never mutate config on clamp, cap usage ratio, and refuse proprietary override SDKs

- effectiveModelAlias / effectiveModelConfig now build a copy before
  clamping maxInputSize to the effective window instead of rewriting
  the caller's config record in place.
- context_usage is clamped to 1 in the v2 legacy status, the v1
  session service, and the SDK getStatus; the default-model status
  fallback also resolves the input cap.
- Overrides naming known proprietary SDKs (Bedrock, Cohere) are
  refused before the OpenAI-compatible fallback, matching top-level
  import behavior.
- The inspector attributes a clamped maxInputSize to the clamp even
  when the raw value came from models.*.overrides.

* fix: normalize forced efforts, prefer input cap in WS status, keep raw event ratio

- Whitespace-only configured efforts now read as absent, and the
  KIMI_MODEL_THINKING_EFFORT override is lowercased on both engines.
- kap-server's WS legacy status publishes max_input_tokens ??
  max_context_tokens like the other status surfaces.
- SDK getStatus keeps the ratio unclamped on purpose (>100% is the
  documented overflow signal on that path); schema-bounded REST status
  stays clamped to 1.
- Pin the provider-observed window beating a declared input cap with a
  v1 full-compaction regression test.

* fix: attribute resolved.maxInputSize provenance and drop test-body comments in v2
2026-07-22 03:13:10 +08:00
Kai
b5efba7abc
fix: consume the model metadata declared by the models.dev catalog (#2015)
* fix: stop advertising Claude thinking efforts for non-Claude models

Models served over the Anthropic protocol whose names carry no Claude
marker (e.g. a catalog-imported Kimi K3) no longer inherit the latest
Opus effort list, so the model selector stops offering levels the model
does not accept. The models.dev catalog import now also parses
reasoning_options and records the declared effort levels on the model
alias, so K3 offers its real levels (low / high / max).

* fix: consume deprecated, override, and input-limit metadata from the models.dev catalog

- Models declared status=deprecated in the catalog are no longer
  offered for import.
- Per-model provider overrides on gateway providers (an npm package
  targeting an Anthropic SDK plus a usable endpoint) now land as alias
  protocol and base_url, so those models are served over the right
  protocol and endpoint; overrides without a usable URL are skipped.
- A declared limit.input now sizes the context budget instead of the
  larger total context window (e.g. gpt-5: 272k instead of 400k).

The model alias schema gains an optional base_url field (not accepted
in overrides) that Anthropic wire resolution prefers over the
provider-level base URL.

* fix: honor thinking-disable semantics and the OpenAI-compatible fallback in catalog imports

- reasoning_options 'none' is the model's off encoding: off_effort flows
  from the catalog through the model alias to the OpenAI wire providers,
  so turning thinking off sends 'none' instead of omitting the effort
  field; models with effort levels but no way to disable thinking are
  imported as always_thinking and no longer offer an Off option.
- Bare Claude family aliases (e.g. sonnet-latest) recover the inferred
  Anthropic effort profile; v2 comment conventions restored.
- Providers whose SDK the catalog does not type now fall back to the
  OpenAI-compatible wire (with a visible "guessed" note) instead of
  being refused; imports lacking a usable endpoint ask for one
  (--base-url on the CLI, a prompt in the TUI). Proprietary SDKs
  (Amazon Bedrock), unrecognized explicit types, and env-placeholder
  URLs are refused with a clear reason.

* fix: align catalog imports with the reference models.dev consumer

- A JSON null tier in declared effort values is now read as the 'none'
  off-encoding (previously such models were wrongly imported as
  always-thinking with no way to turn reasoning off).
- Alpha-status models are filtered out alongside deprecated ones.
- Models whose per-model provider override targets a wire that cannot be
  expressed per-model (e.g. Claude on google-vertex, whose wire here is
  Gemini-mode Vertex, or gpt entries on an Anthropic provider) are
  skipped instead of being imported under the silently wrong protocol.
- interleaved: true no longer pins reasoning_content: the provider's
  default three-field scan is wider and the pinned key only narrowed
  reasoning parsing for gateways answering with another field name.

* fix: require endpoints for Anthropic-compatible catalog imports and honor --base-url

- catalogProviderNeedsBaseUrl now covers the Anthropic wire: a
  non-official Anthropic-compatible vendor without a concrete catalog
  endpoint (e.g. google-vertex-anthropic) must supply --base-url / the
  TUI prompt instead of silently falling back to the default Anthropic
  endpoint.
- --base-url now takes precedence over the catalog-declared endpoint,
  and an empty --base-url is rejected instead of persisting a blank
  endpoint.

* fix: enforce always-on thinking on every wire and refuse Cohere at import

A model that declares always_thinking (e.g. a catalog-imported gpt-5)
no longer resolves to a dishonest off state via thinking.enabled=false
or an SDK/ACP off request: resolution clamps to the model's default
effort on every wire instead of letting upstream keep reasoning while
the UI reports Off. The Anthropic warn-and-send path for unlisted
effort levels is unchanged. Cohere's proprietary SDK joins Amazon
Bedrock on the import-refusal list instead of being guessed as
OpenAI-compatible.

* fix: harden catalog import edge cases

- An explicit but unrecognized catalog type is now refused before
  npm/id inference, so a future catalog protocol is never silently
  miswired through the OpenAI fallback.
- User-supplied --base-url values for Anthropic-wire providers get the
  same trailing-/v1 normalization as catalog endpoints, avoiding
  /v1/v1/messages requests.
- The TUI import prompt rejects env-placeholder base URLs like the CLI
  does.

* fix: await the floating assertion promise in the catalog add CLI test

* refactor: unify catalog import resolution into a single decision function

Wire-type inference, the OpenAI-compatible fallback, proprietary-SDK
refusal, endpoint adaptation, and the base-URL requirement are now
produced together by resolveCatalogImport, one pure resolver consumed
by both the CLI and the TUI — replacing the cooperating predicates
(inferWireType, isGuessedWireType, catalogProviderNeedsBaseUrl) whose
permutations kept producing edge cases. No behavior change.

* fix: close configured-off clamp hole, keep inferWireType compat, carry same-wire override endpoints

- A configured thinking.effort = "off" no longer bypasses the
  always-on clamp: it is treated as absent and the model default
  applies, mirrored on both engines.
- The previously public inferWireType stays as a deprecated
  compatibility wrapper over resolveCatalogImport so existing SDK
  consumers do not break on a patch release.
- Catalog model overrides that stay on the provider's wire but declare
  their own endpoint now persist it on the alias (and the v1 OpenAI
  wire branches honor alias-level base URLs like the Anthropic branch).

* fix: split total window from input cap and close override/endpoint gaps

- max_context_tokens once again means the total context window (used by
  completion budgeting); a model's declared input limit is tracked as
  max_input_tokens, which compaction, context-splice and usage-ratio
  checks prefer — fixing the over-clamping introduced when the input
  cap was stored as the context budget.
- A catalog endpoint declared only as an env placeholder now always
  produces needs-base-url (official SDK included), so credentials are
  never sent to the public vendor host by default.
- api-only per-model overrides are honored as same-wire endpoint
  changes; overrides targeting another known but inexpressible wire
  (e.g. google-genai on an OpenAI gateway) are skipped; same-wire
  models whose declared endpoint is an unusable placeholder are
  skipped instead of silently rerouted.

* style: drop a function-level comment from the v2 thinking resolver

* chore: consolidate the PR's changesets into two user-facing entries
2026-07-22 01:23:31 +08:00
liruifengv
8e0dcf3049
feat(kap-server): add GET /api/v1/oauth/usage for managed account plan usage (#2027) 2026-07-22 00:51:28 +08:00
liruifengv
0a1b5fa00f
fix(agent-core-v2): clear dangling defaultModel and thinking on managed logout (#2026) 2026-07-21 23:50:09 +08:00
qer
154e082488
fix(web): keep transparent images readable over a checkerboard canvas (#2022)
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-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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
White-on-transparent images disappeared on light surfaces and
black-on-transparent ones on dark surfaces (markdown inline images, tool
result media, and the file preview's large view). Add a shared
--media-alpha-canvas token — a checkerboard of two mid-gray squares mixed
from --color-bg/--color-text, each >=3:1 against both white and black —
and paint it directly on <img> so opaque images stay pixel-identical and
only transparent pixels reveal the canvas.

Also let check-style's no-gradient-text skip custom-property definitions
(a token is never rendered text), and register the new tokens in the
design system view.
2026-07-21 22:20:21 +08:00
liruifengv
576d650380
feat(cli): add third-party source note to update prompt (#2014)
* feat(cli): add third-party source note to update prompt

* fix(cli): adjust third-party source note wording

* refactor(cli): move official install URL into app constants

* chore: simplify changeset wording

* chore: adjust changeset wording
2026-07-21 20:42:23 +08:00
Kai
37eda4e59a
feat(config): add env overrides for loop control and background task limits (#1993)
* feat(config): add env overrides for loop control and background task limits

Add three operational environment overrides, resolved as env > config.toml
> default in both engines (agent-core and agent-core-v2), matching the
existing KIMI_IMAGE_MAX_EDGE_PX / KIMI_SUBAGENT_TIMEOUT_MS pattern:

- KIMI_LOOP_MAX_STEPS_PER_TURN overrides loop_control.max_steps_per_turn
- KIMI_LOOP_MAX_RETRIES_PER_STEP overrides loop_control.max_retries_per_step
- KIMI_CODE_BACKGROUND_MAX_RUNNING_TASKS overrides background.max_running_tasks

Invalid values are ignored and fall back to the config value. The v2 engine
resolves them through config section env bindings (effective-only, never
persisted); the v1 engine resolves them at the consumption point.

* fix(config): strip env-bound fields before persisting config writes

Environment overrides resolved into the effective config could be echoed
back through IConfigService.set/replace (e.g. GET then POST
/api/v1/config) and persisted into config.toml, outliving the env var.
This affected the pre-existing image / subagent / keep-alive bindings as
well as the new loop control and max-running-tasks bindings.

Extend ConfigStripEnv with a getEnv parameter and add a shared
stripEnvBoundFields helper: while a field's env var is set, writes
restore the field's raw on-disk value (or drop it) instead of persisting
an echoed env value; when unset, normal writes persist. Register it for
the loopControl, task/background, image, and subagent sections.

Also fix ConfigService.stripEnv looking up rawSnake by the camelCase
domain key; on-disk sections are keyed snake_case.

* fix(config): honor binding parsers when stripping env fields on persist

An invalid env value (e.g. KIMI_LOOP_MAX_STEPS_PER_TURN=abc) is ignored
on the read path but still marked the field env-owned on the write path,
so a config write for that field was silently dropped.
stripEnvBoundFields now derives the guard from the section's envBindings
and skips fields whose env value fails the binding's parse, so invalid
env values are ignored on both paths — and the duplicated field/env
descriptor list is gone.

Also drop function-level comments added beside helpers; agent-core-v2
keeps comments solely in the top-of-file block, so the strip semantics
now live in the config.ts / configService.ts headers.

* fix(config): re-apply env overlays from the env-free base on every read

Two follow-ups from review:

- ConfigService.get()/getAll() re-applied section env bindings on the
  already-overlaid effective cache (and get() mutated it in place), so a
  valid override degraded to invalid or unset kept serving the stale
  value until the next reload — and an echoed stale value could then be
  persisted by a config write. Reads now recompute from a cached
  env-free validated base, so degraded or removed env values fall back
  to the file immediately.
- stripEnvBoundFields restored env-owned fields from the raw snake
  sub-object, missing values persisted under legacy keys (e.g.
  max_steps_per_run). Section stripEnv now receives the env-free,
  fromToml-normalized raw base, so legacy aliases are honored.

* test(kap-server): retry temp-home cleanup in auth tests

auth.test.ts removed its temp home with a plain recursive rm, which races
late async writers in the server shutdown path and flakes with ENOTEMPTY
(seen on main CI and locally on main). Harden the cleanup with the same
maxRetries/retryDelay options sessions.test.ts already uses.

* chore(changeset): consolidate the env-override changesets into two entries

One feature entry (loop/background env overrides, both engines plus the
CLI) and one fix entry (env values persisting on writes and sticking
after degrade/unset, agent-core-v2 plus the CLI).

* fix(config): clear fully-stripped sections and refresh overlay domains on get()

Two follow-ups from review:

- stripEnvBoundFields returned an empty object when every written field
  was env-owned, so a section with a non-empty default (e.g. subagent)
  stored raw = {} and the default stopped applying until the next
  reload. A fully stripped result now clears the raw section instead.
- get(domain) only recomputed env overlays for sections with env
  bindings, so domains written solely by a ConfigEffectiveOverlay
  (models / defaultModel from KIMI_MODEL_NAME) kept serving stale cached
  values after the env changed. get() now derives every non-memory
  domain from the fresh env-free base, matching getAll().

* test(config): drive the get() overlay freshness test with an inline overlay

The previous version imported the model env overlay to activate it, which
the CI runners failed to resolve from this test file (both tsgo and
vitest, while the same specifier resolves elsewhere — not reproducible
locally). An inline ConfigEffectiveOverlay double exercises the same
ConfigService contract with a tighter seam and no module dependency.

* fix(config): preserve unknown fields on full strip and clone nested env targets

Two follow-ups from review:

- stripEnvBoundFields cleared the raw section whenever the stripped
  result was empty, so an env echo write could drop unknown
  forward-compatible fields from the TOML table. An emptied section now
  keeps its raw table while the env-free base still holds other fields,
  and is cleared only when nothing remains (defaults keep applying).
- applyEnvBindings reused nested child objects in place, so a nested
  env binding on a persistable key would mutate the env-free validated
  base and serve stale values after the env var is removed. Children
  are now cloned before descending.

* fix(config): keep the env-free base when a strip leaves nothing to persist

Returning {} for a fully stripped write stored the empty object into the
raw layer while the TOML table survived via rawSnake, so the bases
diverged; a second echo write then saw an empty base, cleared the
section, and deleted the table — dropping unknown forward-compatible
fields. When nothing persistable remains, the write is now a no-op for
the section (the env-free base is kept as-is), and the section is
cleared only when the base is empty.

* fix(config): revalidate stripped results so restored raw values never persist

A strip may restore on-disk values from the unvalidated raw base (e.g.
an env-masked invalid field), smuggling them past the merge-time
validation into the stored config: the section would then fail the next
buildValidated pass, dropping accompanying valid edits from the runtime
while the invalid value stayed persisted. set() now revalidates the
stripped result — discarding the parse output so unknown fields survive
— and rejects the write instead, matching replace().
2026-07-21 19:49:00 +08:00
7Sageer
a3699dd6aa
feat(kap-server): expose per-tool active state in GET /api/v1/tools (#2005)
* feat(kap-server): expose per-tool active state in GET /api/v1/tools

* Update tools-list-active-flag.md

Signed-off-by: 7Sageer <7sageer@djwcb.cn>

---------

Signed-off-by: 7Sageer <7sageer@djwcb.cn>
2026-07-21 19:23:15 +08:00
Haozhe
d67a2003ab
feat(kap-server): add GET /api/v1/fs:content endpoint for host files (#2012)
* feat(kap-server): add GET /api/v1/fs:content endpoint for host files

- serve any host file by absolute path as raw content with Content-Type, ETag caching, and single-range support
- extract shared file metadata helpers (binary detection, line counting, etag, mime/language guessing) into agent-core-v2 _base/utils/fileMeta
- extract pickHeader/parseRangeHeader into kap-server lib/httpRange for reuse across download/content routes

* fix(kap-server): resolve lint and typecheck errors in fs:content route

Return the reply.send(stream) result through the handler promise instead of
using return-await, matching the session fs download route pattern.

* fix(kap-server): reject non-regular files in fs:content

Device nodes, FIFOs, sockets, and zero-size virtual files (/proc, /sys)
would otherwise hang the request or stream unbounded data. Addresses
review feedback; also folds fileMeta symbol comments into the header
block per the agent-core-v2 header-only comment convention.
2026-07-21 18:53:27 +08:00
7Sageer
92576e4d85
fix(agent-core-v2): reconnect dropped MCP servers on tool call failure (#1991)
* fix(agent-core-v2): reconnect dropped MCP servers on tool call failure

When an MCP transport dies mid-session (e.g. the stdio process exits),
the wrapped tool now reconnects the server once and retries the call on
the fresh client, so a dropped connection surfaces as a slow call
instead of a failed turn. Concurrent failing calls on the same server
share a single reconnect, and abort errors bypass the retry.

* fix(agent-core-v2): constrain MCP reconnect retries

* fix(agent-core-v2): recover MCP tool calls on any transport failure

Widen the tool-call recovery trigger from the ConnectionClosed error code
to any transport-level failure: an McpError means the server answered (or
the request timed out) and is not retried, while raw transport errors
(fetch failures, "Not connected" on an already-closed client) now trigger
one reconnect-and-retry. This covers remote HTTP/SSE servers restarting
and batched calls dispatched after the close was processed.

Reconnect through the session-scoped connection manager with join
semantics (reconnectAndJoin) instead of a per-agent dedupe map: parallel
failing calls across agents now share one reconnect instead of preempting
each other through attemptId, and a call that fails after the server was
already healed retries on the resolved client without restarting it again.

* fix(agent-core-v2): probe MCP liveness before reconnecting on tool call failure

Classify an ambiguous tool-call failure by probing the client with a
short-timeout ping instead of guessing from the error type:

- The server answered (a JSON-RPC error, or a result that failed
  client-side CallToolResultSchema validation) is rethrown as-is;
  reconnecting would not change the answer and previously restarted the
  stdio server on every such call.
- The probe succeeding means the failure was transient, so the call is
  retried once in place instead of paying a full reconnect.
- Only a measured-dead transport (the SDK's onclose, or a failed probe)
  goes through reconnectAndJoin and retries on the fresh client, still
  capped at one reconnect per call.

MCPClient gains ping() for the probe; retries remain at-least-once,
now documented in the adapter's module header.

* fix(agent-core-v2): keep MCP tools registered on connection failure

A server whose transport dies between turns no longer loses its tools:
failed/pending status events no longer unregister them, so the next
call reaches the adapter and its reconnect-and-retry path heals the
connection instead of failing with "tool not found".

* Improve MCP server connection handling on tool calls

A server whose connection dies between turns no longer loses its tools from the tool list — they stay available and the next call heals the connection instead of failing with 'tool not found'.

Signed-off-by: 7Sageer <7sageer@djwcb.cn>

---------

Signed-off-by: 7Sageer <7sageer@djwcb.cn>
2026-07-21 17:56:06 +08:00
liruifengv
e45832398d
fix(tui): keep long sessions responsive and bound resumed history (#1976)
* fix(pi-tui): reuse processed lines across frames in the renderer

Each frame previously re-truncated, re-normalized, and re-compared every
transcript line, so steady-state frames (spinner ticks, streaming flushes)
cost O(total lines x chars) and pegged one core in long sessions. Keep the
previous frame's raw lines, processed output, and per-line kitty image ids;
a line whose raw string reference is unchanged reuses its processed output
verbatim, and image-id consumers read the cache instead of re-scanning text.

* fix(tui): stop tree-wide transcript invalidation on structural updates

Grouping a second Read/Agent call, removing swarm progress, finalizing an
MCP status row, replay tool-call removal, and /undo each invalidated the
entire transcript tree, forcing every mounted message to re-render (markdown
lexing + code highlighting) on the next frame. The container's render cache
already validates per child by reference, so structural child-list changes
are picked up without a tree-wide invalidate; reserve it for global style
changes such as theme switches.

* fix(tui): fold older assistant messages into the turn step summary

Step merging only collapsed thinking/tool steps, so assistant text blocks
accumulated without bound inside a turn (hundreds of markdown components in
a single long turn, all re-processed every frame). A running turn now keeps
its last 20 assistant messages (KIMI_CODE_TUI_KEEP_RECENT_ASSISTANT), and a
finished turn folds down to its conclusion tail of 2
(KIMI_CODE_TUI_KEEP_RECENT_ASSISTANT_COMPLETED); older ones collapse into
the step summary line with a message count. Entries are kept, so expand
behavior is unchanged.

* fix(session): bound resumed history to the most recent user turns

Resume used to return every agent's full replay over the RPC boundary (a
long session can reach ~100MB, serialized and parsed several times on an
in-process call), while the TUI only renders the last 10 user turns. The
resume payload now accepts an optional replayTurnLimit, the turn-boundary
predicate moves into agent-core as the single source of truth
(limitAgentReplayByTurns, re-exported through the SDK), and the CLI passes
its existing 10-turn limit so resume transfers just the tail.

* fix(session): count goal continuation rounds as replay turns

Replay turn boundaries only matched real user input, so a 100-round goal
(a handful of user prompts plus 100 system-trigger continuations) fell
entirely inside the 10-turn replay window and resumed by rendering the whole
run from the start. The goal driver already fires one synthetic prompt per
goal turn and counts those as turns itself, so replay trimming now treats
goal_continuation prompts as turn boundaries and keeps the most recent 10
rounds. The continuation prompt is model-facing and hidden live; replay no
longer renders it as a user bubble either, while still advancing the replay
turn so each round groups separately.

* fix(tui): keep replay turn folding when goal continuations are hidden

Suppressing goal continuation bubbles removed every turn-boundary
component from goal-session replays, so mergeAllTurnSteps found no turn
edges and skipped step/assistant folding entirely — a single oversized
goal round still mounted all of its tool cards and assistant messages.
Mount an invisible ReplayTurnBoundaryComponent for each hidden
continuation: it renders zero lines but keeps the turn edges discoverable
to folding and window trimming, matching replay trimming's per-round turn
semantics.

* chore: consolidate and simplify changesets
2026-07-21 15:20:12 +08:00
Haozhe
74da87a457
feat(kap-server): broadcast agent.created/agent.disposed session events (#1997)
* feat(kap-server): broadcast agent.created/agent.disposed session events

- emit durable agent.created / agent.disposed facts from the
  SessionEventBroadcaster lifecycle callbacks, ahead of the agent's own
  events, and let them bypass per-subscription agent allowlists
- add the agent.created / agent.disposed wire types and zod schemas
- stamp disposedAt on the transcript roster entry via
  TranscriptStore.markDisposed so REST consumers can tell a dead agent
  from a live one

* chore: add changeset for agent lifecycle events
2026-07-21 15:18:59 +08:00
7Sageer
ce0e3ceb04
feat: support custom agent files (#1735)
Some checks are pending
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
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
* feat: support custom agent files

Discover Markdown+frontmatter agent definitions from user, project, and configured directories; merge them into a session-level profile catalog (priority: builtin < user < extra < project < explicit). Custom agents work as subagents via the Agent tool and as the main agent.

- agent-core-v2: new agentFileCatalog domain (discovery/parsing/profile factory, mirroring skillRoots/parseFrontmatter) and sessionAgentProfileCatalog merged view; AgentProfile.tools becomes optional (undefined = all tools) and gains disallowedTools deny list, evaluated in profileService.isToolActive and persisted in session wire records so resume keeps the gate even if the file is gone
- CLI: restore --agent/--agent-file for the v2 print runner (KIMI_CODE_EXPERIMENTAL_FLAG); the v1 TUI rejects them with a clear v2-only error
- kap-server/protocol: optional profile field on prompt submission with first-bind semantics (same-name no-op, different name rejected)
- docs: custom agents section (en/zh) + config/CLI references + changeset

* chore: shorten custom agents changeset

* fix(agent-core-v2): stringify caught errors in agent catalog log calls

* docs: drop v2 engine notes from custom agents docs

* fix: align custom agent binding semantics across engine and edges

- agent-core-v2: bind now owns the first-bind invariant — switching
  profiles after bind throws profile.already_bound (checked again in the
  synchronous segment before the first wire dispatch, so concurrent binds
  cannot both pass); unknown names throw profile.unknown; same-name
  rebinds keep the persisted thinking effort.
- kap-server / CLI: edges degrade to error mapping / same-name no-op
  instead of their own divergent guards.
- agent files: reject non-string mode values, honor disallowedTools in
  the append-mode Skill probe, pass --agent-file through unresolved so
  the engine can expand ~, reject empty --agent-file values.
- session catalog: ready is recoverable via reload() after a fatal
  source failure, and agent-file discovery is kicked at session
  materialize so resumed sessions see file agents from the first turn.
- docs: first-bind semantics, name: agent override, tools: [] meaning,
  --agent-file last-wins.

* fix: tighten custom agent behavior

* fix: address custom agent review findings

* fix(agent-core,agent-core-v2): keep active-tool wire records replayable by v1

Binding a profile without a tool allowlist (the default for file-defined
custom agents) persisted a tools.set_active_tools record with no `names`
key. v1 clients discover v2 sessions through the shared session index and
replay newer wire versions without migration, so the record crashed v1
resume with a TypeError and wedged the session permanently.

The v2 engine no longer writes the record when the base set is already
"every tool active" (its absence encodes the same state), and v1 replay
now skips records that lack `names` as defense in depth for wires already
written by preview builds. A same-name rebind that resets an allowlist to
"all tools" has no v1-safe encoding and is left as a documented gap (no
production caller today); a future tools.reset_active_tools Op is safe
because v1 silently no-ops unknown record types.

* fix(agent-core-v2): tolerate unreadable directories in agent-file discovery

A single unreadable subdirectory (EACCES anywhere in the scanned tree)
previously aborted the whole discovery pass, zeroing every agent of that
source on every session start with one path-less warning. The walker now
skips-and-warns per directory below the root (mirroring the skill
discovery it parallels), root-level failures are isolated per root so one
bad root no longer takes its siblings down, and only a genuinely transient
whole-fs outage (os.fs.unavailable) still propagates so the session
catalog keeps its previous contribution. Source-level warnings now name
the offending path, and repeated skip warnings are capped with a summary
that samples the suppressed paths.

Also consolidates the path primitives (~ expansion, base-relative
resolution, realpath type probes) shared by the root resolvers, the
walker, and the explicit-file source into agentFileCatalog/paths.ts, and
tightens parser diagnostics: frontmatter null is treated as absent, and a
present-but-wrong-typed name/description reports a type error instead of
"missing".

* fix(agent-core-v2): warn when a same-name builtin suppresses a file profile

A directory-discovered agent file colliding with a builtin profile
without override: true was silently dropped at merge time. The suppression
now logs a warning naming the profile and the opt-in.

* refactor(agent-core-v2): pass skillActive explicitly to renderSystemPrompt

The third parameter was a full tool list used only for includes('Skill'),
which forced the agent-file profile factory to answer a boolean question
with sentinel lists. The template now takes an explicit skillActive flag;
a skillActiveFor helper keeps builtin call sites derived from their tool
arrays.

* refactor(agent-core-v2): fall back to the configured default model in bind

BindAgentInput.model is now optional: the engine resolves a missing model
against the configured defaultModel and throws model.not_configured when
neither is set, so edges no longer each re-implement the fallback.

* fix(agent-core-v2,kap-server): reject unsupported thinking atomically at first bind

A REST prompt carrying profile + an unsupported thinking effort bound the
session first and failed setThinking after, wedging the session on an
identity the user never successfully used. The effort is now validated up
front when the caller marks it as an explicit request (strictThinking):
the bind rejects before any await or state mutation, and the requested
effort rides along in the bind instead of a separate setThinking. Internal
spawn/fork paths pass inherited thinking without the flag and keep the
previous clamp behavior — a persisted effort that drifted out of the
model's support list must not break subagent spawning. The route's
now-redundant model fallback is dropped in favor of the engine-side
default.

* fix(agent-core-v2): await the agent profile catalog at session materialize

The catalog's ready promise was only kicked, so a resumed session's first
turn could render the Agent tool description without the file-defined
agent types. Discovery is local-fs and cheap, so materialize now awaits
it; ready only rejects for a fatal explicit-source error, which is exactly
the case that should fail fast. A failure there now also removes and
disposes the half-materialized handle instead of leaving it registered in
the session cache.

* test: cover the --agent-file fatal path and tidy profile registration hygiene

The v2 print CLI now has a test asserting an invalid --agent-file fails
before any turn. The denylist profiles in binding.test.ts register in a
beforeAll (idempotent, scoped to the describe's run window) instead of at
module scope during collection.

* docs: align custom agent docs with v2-engine gating

--agent/--agent-file are rejected without the v2 engine, so restore the
requirement in the Agents and Command Reference pages (and use
KIMI_CODE_EXPERIMENTAL_FLAG=1 in the examples), note that tool lists only
shape model-visible disclosure (permission rules are the enforcement
layer), and remind authors of delegation-bound agents to state the
handoff contract in the prompt body.

* test(agent-core-v2): revert unrelated style churn in fs/workspace tests

Keep these files' diff limited to the realpath fakes the feature needs;
the lint-preference rewrites belong to a separate cleanup.

* feat(agent-core-v2): add permanent system prompt override via SYSTEM.md

Read $KIMI_CODE_HOME/SYSTEM.md on every startup and inject it as the default main-agent profile (name "agent", override: true), replacing the builtin default system prompt while inheriting builtin tools and description. Missing or empty files are ignored; unreadable files warn and fall back to the builtin profile.

The body supports variable substitution (${skills}, ${agents_md}, ${cwd}, ${cwd_listing}, ${os}, ${shell}, ${now}); unknown variables pass through verbatim.

Priority: --agent-file / --agent / project override > SYSTEM.md > same-name user-scope scan files.

* feat(agent-core-v2): gate tools globally and accept session disabledTools

Add a [tools] config section: "enabled" acts as a global allowlist (empty = unconstrained), "disabled" as a denylist applied on top, both intersected with the active profile's policy in isToolActive (mcp glob supported).

Plumb a session-persistent disabledTools parameter through the stack: v2 RPC PromptPayload, REST "disabled_tools" (protocol and kap-server parallel schemas), klient contract/facade, and node-sdk. The server applies it via profileService.setSessionDisabledTools, which replaces the client-owned denylist, keeps the profile's own deny, persists across resume, and rejects calls before a profile is bound with profile.not_bound (mapped to 40001). v1 core-api gains a type-only field and ignores it.

* fix: enforce session tool policy across agents

* fix(agent-core-v2): enforce tool policy at execution

* fix(agent-core-v2): align subagent tool descriptions with policy

* fix(agent-core-v2): harden custom agent policy state

* fix(agent-core-v2): harden custom agent lifecycle

* refactor(agent-core-v2): persist profile binding in a single profile.bind record

* fix(agent-core-v2): skip unreadable paths during agent file discovery

* fix(agent-core-v2): exempt select_tools from the executor policy guard

- share one composed profile/global/session tool-policy evaluation between
  the executor gate and prompt rendering instead of two verbatim copies
- tolerate context-build failure in system prompt refresh instead of
  rejecting callers (config watcher void-fire, session policy fan-out)

* test(agent-core-v2): resolve profile and tool-policy SUTs by interface

- drop the Object.assign patching of tool-policy methods onto the shared
  profile service; rename describes so the SUT ownership is accurate
- classify profile.bind as v2-only with the accepted v1-replay tradeoff
  documented, un-red the wire vocabulary guard test
- cover the select_tools guard exemption with an executor-level test

* fix(agent-core-v2): enforce explicit select_tools policy

* feat(agent-core-v2): accept Claude-style tool lists and rename agent-file mode to promptMode

* feat(agent-core-v2): unify prompt templating on ${var}

- Replace the nunjucks renderer with a single ${var} regex renderer
  (unknown placeholders pass through verbatim) and drop nunjucks from
  agent-core-v2.
- Merge the variable tables into one catalog shared by the builtin
  system.md, SYSTEM.md, and agent file bodies; adds additional_dirs_info
  plus code-composed blocks (windows_notes, additional_dirs_section,
  skills_section).
- Replace the agent-file promptMode field with ${base_prompt}: bodies
  are always rendered as templates, and ${base_prompt} expands to the
  effective default profile prompt (honoring the SYSTEM.md override).
- Migrate the builtin system.md, goal reminders, compaction instruction,
  and tool description templates to the same syntax.

* docs: complete agent priority chain and link SYSTEM.md precedence

* test: fix invalid custom agent fixture

* fix(cli): reject multiple agent selectors

* feat(agent-core-v2): add subagents allowlist to agent files

* fix(agent-core-v2): persist the subagent allowlist in the profile binding

The delegation allowlist now rides the profile.bind record like the tool
denylist, so a resumed session keeps enforcing it even when the source
agent file was deleted or changed. Agent/AgentSwarm resolve the caller's
allowlist from the persisted binding data instead of looking the profile
up in the live catalog.

* feat(agent-core-v2): warn on tool patterns that never match

Profile bind/apply and [tools] config changes now statically flag
entries that can never activate anything — wildcards without the mcp__
prefix (a bare * in an allowlist disables everything, in a denylist
nothing), incomplete mcp__ literals, and names no registered or
builtin-profile tool has — via a tool-pattern-no-match warning event,
once per pattern, instead of letting the tool set silently shrink. The
known-name vocabulary is the live registry plus literal names from the
builtin profiles, so flag-gated tools stay known and a typo in one agent
file cannot legitimize the same typo in another.

* docs: align --agent-file docs with the single-selector CLI

The flag accepts exactly one file and conflicts with --agent, but the
docs still described the earlier repeatable, composable design.

* docs: note the agent-file trust model and never-matching tool patterns

Spell out that project-scoped agent files can replace the default main
agent's whole system prompt (unlike AGENTS.md reference injection), and
list the three tool-pattern shapes that never match and now raise a
warning.

* chore: slim changeset wording to user-facing language

Drop wire record names, enforcement mechanics, and template syntax from
the entries; split the v1 resume fix into its own patch changeset; add a
patch entry for the tool-pattern warnings.

* chore: shorten changeset entries to one-line summaries

* Delete .changeset/v1-resume-v2-sessions.md

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>

* test: drop class-instance spread in sessionLifecycle test stub

* chore: clear the comments

---------

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>
2026-07-21 15:01:43 +08:00
7Sageer
e070a580f0
feat(telemetry): emit turn_id and agent_id on turn and tool events (#1675)
* fix: emit turn_id on turn_started/ended/interrupted telemetry

The turn lifecycle telemetry events (turn_started, turn_ended,
turn_interrupted) never carried the turn id, while tool_call and
tool_call_dedup_detected did. Any analysis correlating a turn's start,
end, or interruption back to its tool calls had nothing to join on.

Add turn_id (already in scope) to all three track() calls, matching the
existing key/value convention used by tool_call_dedup_detected.

Update the strict turn_started/turn_interrupted assertion to cover it.

* fix(agent-core-v2): emit turn_id on turn_started/ended/interrupted telemetry

Port the v1 fix to agent-core-v2: turn lifecycle telemetry events
(turn_started, turn_ended, turn_interrupted) carried no turn id while
tool_call did, leaving nothing to correlate a turn's start, end, or
interruption back to its tool calls.

Add turn_id to the three event interfaces, the telemetry registry
property docs, and the three track2() calls in AgentLoopService,
matching the existing ToolCallEvent key convention. Extend the
turn telemetry assertions in loop.test.ts to cover it.

* fix: emit turn_id on tool_call telemetry

* docs(agent-core-v2): clarify turn_id is a per-agent index in the telemetry registry

* feat(telemetry): add agent_id to turn and tool events

turn_id is a per-agent counter, so it collides across the main agent and subagents within a session. Emit each agent's scope id as agent_id on turn_*, tool_call, tool_call_dedup_detected, api_error and subagent_created (plus parent_agent_id) so events become attributable via (session_id, agent_id, turn_id).

* docs(changeset): cover turn_id emission alongside agent_id

* feat(telemetry): add agent_id to agent-level settings events

* feat(telemetry): link v2 events across agents, turns, and tool calls

- subagent_created: parent_tool_call_id, so a child run joins to the tool call that launched it
- permission_policy_decision / permission_approval_result: agent_id, turn_id, tool_call_id
- plan_submitted / plan_resolved / plan_enter_resolved, context_projection_repaired: agent_id
- compaction_finished / compaction_failed: agent_id + optional turn_id
- cron_scheduled / cron_deleted: optional agent_id of the scheduling agent
- api_error: turn_id + request_kind, so compaction request failures are distinguishable from turn requests
- tool_call_repeat: agent_id + optional turn_id; tool_call_dedup_detected stops fabricating turn_id: 0 outside a turn
- background_task_created/completed: task_id on both, unified kind vocabulary ('process' replaces the legacy 'bash' alias on created)
- agent lifecycle: auto-assigned agent-N ids now skip ids persisted by previous runs, so a resumed session cannot reissue agent-0 and collide with earlier telemetry

* fix(agent-core-v2): preserve telemetry turn attribution

* chore: split telemetry changeset into two logical changes

* refactor(agent-core-v2): bind agent telemetry context at scope

* test(agent-core-v2): fix telemetry assertions for scope-bound agent_id

- undoHistory/goal: include the injected agent_id in exact property assertions
- rpc-events: assert on the shared records array instead of a track2 spy,
  which the scoped telemetry view bypasses

* refactor(agent-core-v2): bind agent identity ambiently in telemetry

- TelemetryService.withContext now returns a lightweight forwarding view:
  transport state (appenders, enabled flag) stays with the App-scoped root,
  so views created before an appender attaches no longer silently drop
  events, and enablement changes apply to every live view.
- Agent-scope events register with defineAgentTelemetryEvent and compose the
  centrally declared AgentTelemetryEventContext (agent_id) into their wire
  schema; business payloads and call sites stay free of agent_id, enforced
  at compile time and by the registry convention test.
- image_compress/image_crop stay plain events: the kap-server prompt routes
  emit them through a session-scoped view without agent identity.
- v1 subagent_created gains parent_tool_call_id for parity with v2.
- Add a lifecycle test asserting each agent scope's telemetry view binds its
  own agent id.

* Delete .changeset/subagent-id-reuse.md

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>

---------

Signed-off-by: 7Sageer <12210216@mail.sustech.edu.cn>
2026-07-21 14:48:09 +08:00
Kai
a8f1ca3f10
feat(acp): support selecting thinking effort levels (#1992)
* feat(acp): support selecting thinking effort levels

The ACP thinking config option was a binary on/off toggle mapped to the
model's default effort, so ACP clients (e.g. Zed) could not pick a
concrete level even though the engine and the SDK fully support
support_efforts granularity.

- Advertise one select row per declared effort level (off + levels);
  boolean models keep the legacy off/on rows, always-thinking models
  drop the off row.
- Track the session thinking state as an effort string and reconcile it
  with the engine-normalized value read back from session status after
  model/thinking changes.
- Validate incoming levels against the current model's catalog and
  reject unknown ones with invalid_params before any SDK call; keep the
  legacy 'on' value as an alias for the model default effort.

* test(acp): avoid spreading the Session stub in the reconciliation test
2026-07-21 13:48:50 +08:00
Kai
beeb964393
fix(vscode): reduce webview streaming re-render churn (#1994)
Chat components subscribed to the entire chat store, so every
streaming delta re-rendered every assistant message. Subscribe to
narrow store slices and memoize ChatMessage so settled messages no
longer re-render on each delta. Cap getImageDataUri at 10MB, matching
the media picker, so oversized local images are not inlined into the
webview DOM.
2026-07-21 13:10:59 +08:00
Haozhe
6dd4fd3368
refactor(agent-core-v2): rebuild the model wire layer on the kosong architecture (#1970)
* refactor(v2): land kosong contract layer (L0 wire contract)

* refactor(v2): land kosong protocol layer (L1 traits and base registry)

* refactor(v2): land kosong provider layer (bases, trait composers, kimi definition)

* refactor(v2): land kosong model and catalog layers

* refactor(v2): migrate engine callers to kosong, drop old llmProtocol layer

* test(v2): migrate agent-core-v2 tests and harness to kosong

* refactor: sync peripheral packages to kosong architecture

* refactor(v2): replace provider dialects with per-protocol definitions

* refactor(v2): merge the vertexai protocol into google-genai via providerOptions

* refactor(v2): remove vendor-name gates outside the kosong layer

* feat(v2): add model resolution inspection and connectivity ping

* refactor(v2): remove the unused platform config layer

* test(klient): pin invalid-input behavior across providers in e2e

* refactor(agent-core-v2): merge kimi traits, split bases by protocol

- merge the seven kimi trait modules into kimi.contrib.ts as two trait
  objects: kimiOpenAITrait (all native-transport hooks) and
  kimiAnthropicTrait (thinking only)
- move bases/* implementations into per-protocol directories (openai/,
  anthropic/, google-genai/) and rename openai.contrib.ts to
  openai-legacy.contrib.ts
- add per-directory index.ts registration barrels (import = registration)
  and exempt them in check-domain-layers.mjs alongside *.contrib.ts

* refactor(agent-core-v2): reorganize model and request-layer types

- consolidate shared model types (ModelOverrides, CompletionBudgetConfig/Params,
  ResolvedModelAuthMaterial, ThinkingDefaults/ModelThinkingMetadata) into
  kosong/model/model.types.ts and drop modelOverrides.ts
- rename L2 request types to the ModelRequest* prefix: LLMEvent ->
  ModelRequestEvent, LLMRequestInput -> ModelRequestInput,
  LLMCallParams -> ModelRequestParams
- extract ModelRequestTiming to replace three duplicated copies of the
  stream-timing shape
- rename L3 llmRequester types with the Agent prefix to match
  IAgentLLMRequesterService (AgentLLMRequestOverrides/Finish/Task/Source/
  PartHandler/LogFields)
- delete the unused LLMRequestParams type

* refactor(agent-core-v2): fold kosong/catalog into kosong/model

- merge IModelCatalogService's enumeration surface (listModels /
  listProviders / getProvider / setDefaultModel and the wire shapes) into
  IModelCatalog; delete kosong/catalog/modelCatalog.ts
- move the remote refresh path to the new IProviderDiscoveryService
  (discovery.ts + discoveryService.ts, renamed from
  catalog/modelCatalogService.ts)
- relocate configSection / errors to discoveryConfigSection.ts / errors.ts;
  DEFAULT_MODEL_SECTION now lives in kosong/model/model.ts
- drop the L3 catalog layer from check-domain-layers.mjs
- kap-server routes / refresh scheduler / channelRegistry follow the split;
  klient renames the contract modelCatalogService to modelResolver and adds
  providerDiscovery; kimi-inspect reads via IModelCatalog
- modelRequesterImpl: drop the streamedAnyPart backfill, onMessagePart
  already delivers every part
- tests: remove the app/modelCatalog and kosong/catalog suites, add the
  kosong/model catalog and discovery suites

* feat(klient): trim trailing undefined args and add boundary smoke probes

- add trimTrailingUndefined helper so optional trailing args no longer
  cross the wire as null in http/ipc transports, which defeated
  server-side default parameters
- add model-requester-boundary smoke probe for ChatProvider error
  wrapping behavior against real config and a local stub
- add kimi-select-tools smoke probe verifying the kimi-only wire
  encoding of dynamic tool declarations
- extend smoke.ts with a models set/get/delete round-trip, catalog
  list assertions, and update AGENTS.md with the new scripts

* fix(agent-core-v2): declare openai chat hooks as function properties

Method-shorthand members on OpenAIChatCompletionsHooks tripped
typescript-eslint(unbound-method) at every extraction site
(`const hook = this._hooks?.convertMessage` and friends), failing the
repo-wide lint job. Every implementation is a plain closure composed by
openaiHooks.ts, so declare the members as function-typed properties,
which matches the actual semantics and clears the four errors.

* fix: repair stale references surfaced by the origin/main rebase

- agent-core-v2: point vacuousContent's ContentPart import at kosong/contract
- kap-server: rewrite the transcript test seed as IModelCatalog (IModelResolver is gone)
- klient: inline onceEvent/waitFor after the http transport helpers were dropped
- kimi-inspect: remove useLiveEvent from ModelCatalogView; catalog polls on a slow interval

* chore: downgrade the kosong architecture changeset to patch
2026-07-21 13:03:54 +08:00
liruifengv
73eb5f89e0
fix(tui): remove red coloring from code syntax highlighting (#1995) 2026-07-21 12:54:47 +08:00
liruifengv
115b0968ce
fix(tui): hide system-trigger messages in resume replay (#1990)
Goal mode stores its per-turn continuation prompt as a user message with
a system_trigger origin. Live rendering never shows these model-facing
messages, but resume replay only filtered three specific system_trigger
names, so the continuation prompt leaked into the transcript as a user
message. Skip all system_trigger user messages during replay, matching
the live path and the markdown exporter, and drop the two goal-specific
filters this makes redundant.
2026-07-21 12:16:46 +08:00
Kai
71bcfba54a
fix(agent-core): drop vacuous assistant messages that permanently wedge sessions (#1968)
* fix(agent-core): drop vacuous assistant messages that permanently wedge sessions

A provider-filtered response can seal an assistant message holding only an
empty thinking part into the recorded history. The projection's empty-message
guard only counted parts, so it survived, and was serialized as an assistant
message with no content and no tool calls — which the provider rejects with
"the message at position N with role 'assistant' must not be empty" on
every resend, permanently wedging the session.

The projection now drops any message whose parts all serialize to nothing
(empty/whitespace text, or empty unsigned thinking). Content-bearing
messages keep every part verbatim, including empty thinking blocks that
preserved-thinking providers require back. Drops surface through a new
vacuous_message_dropped projection anomaly with log/telemetry counters, and
the structural-error recovery now recognizes this provider rejection so the
strict resend self-heals any residual path. Mirrored in agent-core-v2.

* docs(agent-core-v2): move vacuous-message rationale into the file header

The v2 comment convention keeps comments solely in the top-of-file block.
Move the vacuous-message drop rationale there and drop the inline/JSDoc
comments added beside statements in the projector service, the llmProtocol
error patterns, and the projector tests. No behavior change.

* fix(agent-core-v2): drop output-free steps at fold settle time

The loop-event fold already dropped a settled assistant message when it was
structurally empty (no content, no tool calls), but a step that recorded
only an empty thinking part — e.g. a provider-filtered response carrying an
empty reasoning field — survived into history, relying on the projection to
keep it off the wire. Widen the settle condition to drop the step whenever
nothing sendable was recorded (no tool calls; every content part vacuous),
using a predicate now shared with the context projector. This heals stored
v2 histories at restore time instead of only at request time.

* fix(agent-core): drop duplicate-stripped assistants left with only vacuous content

The strict resend's dedupe pass could re-create the exact empty-assistant
shape the projection now guards against: when every tool call a message
carried was removed as a later duplicate, a remainder holding only an empty
thinking part was kept (its content was non-empty), serialized as an
assistant message with no content and no tool calls, and rejected again
with "the message at position N with role 'assistant' must not be empty"
— the same failure the strict resend was recovering from.

The keep condition now requires sendable content, so such a message is
dropped wholesale with a vacuous_message_dropped anomaly alongside the
duplicate-tool-call one. Mirrored in agent-core-v2.

* fix(agent-core-v2): mirror output-free step drops in the transcript reducer

The live loop-event fold drops a settled assistant step when nothing
sendable was recorded (no tool calls; every content part vacuous), but the
cold transcript reducer still pushed an assistant on every step.begin and
never dropped it. Cold readers (snapshot / messages) therefore kept
showing output-free phantom assistants — including one per failed retry
attempt, which predates the vacuous-step case — and foldedLength overcounted
the live folded history, which can misplace the unflushed-tail splice in
the messages view while a turn is in flight.

The reducer now settles steps the same way the fold does: at a step's end
or at the next begin, an assistant with no tool calls and only vacuous
content is removed and foldedLength is decremented. Steps carrying any
sendable output — real text, real thinking, signed thinking, or tool calls
— are kept verbatim.

* fix(agent-core-v2): update vacuous-content predicate documentation for clarity

* docs(changeset): simplify the vacuous-assistant fix changelog entry
2026-07-21 11:04:55 +08:00
liruifengv
c2d7bebd04
docs(changelog): sync 0.28.1 from apps/kimi-code/CHANGELOG.md (#1975)
Some checks are pending
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 / 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 / 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-07-20 23:46:39 +08:00
github-actions[bot]
efacf0452d
ci: release packages (#1946)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 22:57:20 +08:00
tt-a1i
c5b6103bb9
fix(acp): allow configured provider auth (#934)
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
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
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 / Publish native release assets (push) Blocked by required conditions
2026-07-20 22:05:17 +08:00
Haozhe
ad8cc85251
feat(cli): foreground-only web servers, deprecated kimi server kill (#1967)
- the /web slash command now always starts a new server instead of offering to reuse a running one
- remove the kimi web kill and kimi web ps subcommands; foreground servers stop with Ctrl+C
- keep kimi server kill as a deprecated fallback that only stops servers started by a version before 0.28.0 (recorded in the legacy server lock)
2026-07-20 21:50:09 +08:00
liruifengv
dde92fe5ab
feat: support max and exact version targeting for tips banner (#1960)
* feat: support max and exact version targeting for tips banner

Add optional banner_max_version (exclusive upper bound) and banner_version (exact match) fields to the remote tips banner schema. They combine with banner_min_version (all must pass) and apply to both the active banner and fallback list entries. Invalid semver constraints fail closed and hide the banner.

* chore: drop changeset for banner version targeting
2026-07-20 20:08:52 +08:00
qer
bcee3ac542
chore(vscode): release 0.6.4 (#1958) 2026-07-20 18:42:13 +08:00
qer
9223a37622
feat(vscode): scope thinking effort to the current session (#1951) 2026-07-20 18:27:42 +08:00
liruifengv
e8f2a077d3
docs(changelog): sync 0.28.0 from apps/kimi-code/CHANGELOG.md (#1944)
* docs(changelog): sync 0.28.0 from apps/kimi-code/CHANGELOG.md

* docs: tighten the 0.28.0 changelog and fix web command references

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-20 17:28:48 +08:00
Haozhe
f6f4192957
fix(agent-core-v2): broadcast permission mode switches to live subagents (#1948)
* fix(agent-core-v2): broadcast permission mode switches to live subagents

- add IAgentLifecycleService.broadcastPermissionMode to fan a mode out to
  every live agent in the session registry
- fan out setPermission RPC calls addressed to the main agent so
  already-running subagents observe the new mode
- route the legacy agent_config.permission_mode patch through the same
  broadcast instead of only touching the main agent

* style(agent-core-v2): remove non-header comments
2026-07-20 15:55:27 +08:00
Haozhe
5ae60fa673
feat(transcript): add unified transcript layer, drop the /api/v2 RPC surface (#1888)
* feat(transcript): add unified transcript layer and v1 surface

- add packages/transcript: agent-granular L1 store, idempotent L2 ops,
  off/turn/block/delta L3 granularity, L4 view registry, and turn-cursor
  pagination; sole owner of all transcript wire types
- kap-server: engine-event-driven TranscriptService with history backfill,
  GET /sessions/{id}/transcript, and transcript.ops WS deltas with
  per-connection granularity control
- kimi-inspect: render ChatView from the transcript surface (REST pages +
  delta-only WS) instead of context memory
- sync flake.nix workspace lists and document packages/transcript and
  packages/server-e2e in AGENTS.md

* fix(transcript): project live prompts and anchor backfilled items

- agent-core-v2: carry the extracted prompt text on turn.started so the
  transcript projector can render the user input at turn open (the context
  append with the same text is not a bus event and lands later)
- kap-server: keep the prompt through turn.ended; anchor backfilled
  markers/taskrefs to their following snapshot turn so replaying history
  after live turns arrived keeps the historical order
- transcript: add an optional beforeTurn placement anchor to
  marker.upsert / taskref.upsert; anchored inserts land before the first
  turn at or past the anchor instead of appending blindly

Addresses review feedback on #1888.

* fix(transcript): adopt backfilled stream frames and group goal turns

- kap-server: on mid-stream attach, adopt the backfill-seeded stream frame
  (id + offset) instead of opening an empty one whose upsert clobbered the
  seeded text and whose offset-0 appends could not land
- transcript: group a turn-opening system_trigger (goal continuation) into
  its own turn so cold rebuilds keep the turn boundary and stay
  ordinal-aligned with the engine's live turn numbering

Addresses review feedback on #1888.

* fix(transcript): drop stale live stores on close and heal after turns

- drop a session's live transcript store when the session closes or archives
  (lifecycle events plus a re-check on the cached-entry path) so reads fall
  back to the cold rebuild instead of serving a stale store
- re-read an ended turn from the persisted wire records (debounced per
  agent) and merge it back live-first: headers keep live state/timestamps
  while origin/prompt recover from disk, and truncated text/thinking frames
  from a mid-turn attach are restored only when the persisted text is
  longer — a fresh live frame or a lagging flush is never reverted

Addresses review feedback on #1888.

* fix(transcript): adopt seeded tool frames and route subagent questions

- kap-server: adopt the backfill-seeded tool frame when a tool.result
  arrives for a call that started before the projector attached, so the
  output lands instead of being dropped (the producer-store lookups now
  ride one projector options object)
- agent-core-v2: record the owning agent on question interactions
  (ISessionQuestionService.request gains an agentId option, passed by
  AskUserQuestionTool from its agent scope) so a subagent's questions
  route to its own transcript and WS events instead of 'main'

Addresses review feedback on #1888.

* fix(transcript): adopt parent frames, namespace live markers, redact resets

- kap-server: fall back to store adoption when subagent.spawned links a
  parent tool call that started before the projector attached, so the
  agentRefs link is not lost on mid-bind attaches
- kap-server: id live markers in their own namespace (live-mN) — the cold
  rebuild numbers its markers m1... too, and a colliding id made the
  store's upsert replace the historical marker instead of appending
- transcript/kap-server: redact transcript.reset snapshots to the
  subscriber's grade (below 'block' the step/frame detail is stripped), so
  a 'turn'-grade subscription no longer receives full content on resets

Addresses review feedback on #1888.

* fix(transcript): fold blocked turns into failed, drop inline v2 comment

- kap-server: map turn.ended reason 'blocked' to the 'failed' transcript
  state, matching the engine's TurnEndReason wire contract and the v1
  mapTurnReason folding (it was presented as a user cancellation)
- agent-core-v2: move the question agentId rationale into the ask-user
  file header — package rules keep comments in the top-of-file block only

Addresses review feedback on #1888.

* fix(transcript): keep roster descriptors and resolved-event agents

- kap-server: keep the metadata-seeded roster descriptor (parentAgentId /
  label) when an on-demand history backfill lands its roster entry, instead
  of downgrading it to { agentId, type }
- kap-server: remember each interaction's owning agent and stamp it on the
  resolved question/approval v1 events — they were hard-coded to 'main', so
  an agent-filtered subscriber saw a subagent's question open but never
  close

Addresses review feedback on #1888.

* fix(transcript): defer pending seeding, skip ghost roster entries

- kap-server: announce interactions pending at bind time only after the
  initial backfill (new TranscriptBinding.seedPendingInteractions), and
  adopt the seeded tool frame on the request/resolve paths, so a
  pre-existing approval lands next to its backfilled tool call and keeps
  the approvalId back-link
- kap-server: skip the roster entry when a probed agent id has neither a
  roster presence nor persisted content (agent_id=nope no longer conjures
  a ghost subagent)
- agent-core-v2: fold the turnEvents import rationale into the loop file
  header (package rule: comments live in the top-of-file block only)

Addresses review feedback on #1888.

* fix(transcript): reject hostile agent ids, honor the agent allowlist

- transcript/kap-server: validate agent ids as plain names (no separators,
  no dot segments) at the REST query layer and again before the id is
  joined into the wire-records path — an authenticated client could
  otherwise read a wire.jsonl outside the agents directory
- kap-server: compose the legacy v1 agent allowlist with transcript
  grades on every fan-out path (initial resets, per-ops fan-out,
  roster-driven resets), so a filtered connection no longer receives
  other agents' transcript frames

Addresses review feedback on #1888.

* fix(transcript): settle foreground shell tasks on shell.completed

- agent-core-v2: emit a transient shell.completed event when a foreground
  `!` command settles (detached runs keep reporting through the task
  lifecycle) — the generic task.terminated never fires for foreground
  tasks, so their transcript cards were stuck at 'running'
- kap-server: map shell.completed to the terminal transcript task state
  (completed/failed) and classify the event as volatile on both v1
  durability gates, like its shell.* siblings

Addresses review feedback on #1888.

* fix(transcript): replay early resolves, reset on a widened filter

- kap-server: register bind-time pending interactions without frames so a
  resolve arriving before the post-backfill seed still routes, then replay
  it at seed time — request and resolve land together with the approvalId
  back-link, instead of the interaction vanishing entirely
- kap-server: treat an agent newly admitted by a broadened legacy agent
  filter as owed a transcript.reset even when its grade transition is a
  no-op (delta → delta) — its ops were suppressed so far, so it has no
  baseline otherwise

Addresses review feedback on #1888.

* fix(transcript): keep materialized transcripts on agent disposal

- kap-server: only the projector dies with the agent scope — the
  materialized transcript and roster entry now survive disposal (the
  roster mirrors session metadata, which keeps completed agents).
  Dropping them lost already-served history for good: the backfill cache
  dedupes per agent, so the next read rebuilt an empty shell instead of
  replaying the persisted records

Addresses review feedback on #1888.

* fix(transcript): anchor refreshes by oldest turn, group slash turns

- kimi-inspect: re-cover the previously loaded window after a refresh by
  paging until the previous OLDEST turn is loaded again (extracted as
  recoverLoadedWindow) — a count-based stop silently dropped the window's
  head once new turns shifted the server window
- transcript: group user-slash skill/plugin activations into their own
  turn (marker included), mirroring the engine's isRealUserPrompt — their
  assistant output no longer folds into the previous turn on cold rebuilds;
  other triggers stay marker-only

Addresses review feedback on #1888.

* fix(transcript): compare all tool fields, overlay in-flight backfills

- transcript: include toolCallId/name/view/input in the tool frame
  equality check — an upsert correcting only those was dropped as a no-op,
  leaving stale tool metadata on clients
- kap-server: overlay the loop's active turn as 'running' after a backfill
  (cold grouping marks every rebuilt turn completed, so a live turn showed
  as finished until it ended); snapshot data supplies origin/prompt, and a
  projector-owned running header is never downgraded

Addresses review feedback on #1888.

* fix(transcript): gate ops before the seed, re-assert running headers

- kap-server: gate the transcript ops fan-out (and roster-driven resets) on
  a per-connection seeded flag set only after the baseline reset has
  landed — a subscriber joining mid-stream no longer receives deltas
  against an empty baseline
- kap-server: always re-assert the loop's active turn as 'running' after
  the snapshot ops in a backfill (skipping the overlay when a live running
  header existed let the snapshot's cold 'completed' header downgrade it);
  live header fields win over the snapshot's

Addresses review feedback on #1888.

* docs(agent-core-v2): fold turnEvents notes into the file header

Move the turn.started prompt rationale from field/function TSDoc into the
module header — package rules keep comments in the top-of-file block only.

Addresses review feedback on #1888.

* fix(transcript): emit shell failure output, dispose per-agent listeners

- agent-core-v2: emit the synthesized failure text as a final shell.output
  chunk before shell.completed (it was never streamed, so failed
  foreground commands showed empty output in transcript tasks until a
  full rebuild)
- kap-server: track each agent's bus subscription per agent and dispose it
  in onDidDispose — the listener captures the projector, so a disposed
  agent no longer keeps projecting late events into the store

Addresses review feedback on #1888.

* fix(transcript): route shell events by task id, tidy question docs

- agent-core-v2: keep the commandId → foreground-task-id mapping and carry
  taskId on shell.output / shell.completed, so consumers attaching
  mid-command (having missed shell.started) can still route output and the
  terminal state
- kap-server: fall back to the event's taskId in the shell output/completed
  projectors and seed the shell task before the first chunk, so output is
  preserved and the terminal upsert cannot clobber it with an empty tail
- agent-core-v2: fold the question agentId note into the question.ts file
  header (package rule: comments live in the top-of-file block only)

Addresses review feedback on #1888.

* fix(transcript): tighten agent id validation, match kinds in heals

- transcript: constrain agent ids to a filename-safe shape
  ([A-Za-z0-9._-], <=128 chars) — NUL-containing or overlong ids made the
  wire-records read throw unhandled errors (500) instead of failing
  validation
- kap-server: require the live frame's kind to match before the post-turn
  heal's length shortcut — a kind-mismatched frame (the projector guessed
  the stream kind wrong mid-turn) is now replaced by the persisted one
  instead of being skipped

Addresses review feedback on #1888.

* fix(transcript): heal missed tool results, seed pendings per agent

- kap-server: re-emit tool frames in the post-turn heal when the live step
  lacks the frame or the live frame lacks the outcome the persisted one
  carries (a tool.result dropped in the attach race is otherwise
  unrecoverable); live-only extras (display / agentRefs / approvalId) are
  preserved, and frames with a live outcome stay untouched
- kap-server: scope seedPendingInteractions by agent — the initial seed
  after backfillMain covers main-owned pendings, and each subagent's
  pendings seed after its own on-demand backfill, so placement and the
  approvalId back-link find the persisted tool frames

Addresses review feedback on #1888.

* fix(protocol): register shell.completed on the v1 event surface

- packages/protocol: add ShellCompletedEvent (plus optional taskId on
  shell.output / shell.completed and prompt on turn.started) to the event
  interfaces, zod schemas, the agent event union, and the volatile list —
  schema-validating consumers previously rejected the forwarded
  shell.completed frames outright
- kap-server: mirror the same fields in the v1 events-zod module

Addresses review feedback on #1888.

* test(node-sdk): cover shell.completed in the exhaustive event switch

The SDK's session-event type test asserts exhaustiveness with assertNever;
register the new event there (CI typecheck caught it).

Addresses review feedback on #1888.

* fix(transcript): source cold-session rosters from session metadata

- kap-server: add TranscriptService.readColdRoster (persisted state.json →
  descriptors, mapped like the live seeding) and use it for the cold
  transcript path — the requested agent id is only appended when it has
  content (or is main), so an empty probe (agent_id=nope) no longer
  fabricates a ghost roster entry, matching the live path

Addresses review feedback on #1888.

* fix(transcript): emit taskrefs when seeding missed shell commands

- kap-server: the mid-command-attach seeding in onShellOutput now emits
  the matching taskref.upsert (exactly like onShellStarted), and
  onShellCompleted emits one when the whole command was missed — the task
  no longer exists only in the global map with no timeline item to render

Addresses review feedback on #1888.

* fix(transcript): defer unseeded live pendings, keep cold tools running

- kap-server: pendings created before their owning agent's seed has run
  now defer into the same unseeded queue as bind-time ones (tracked per
  agent) — announcing them during the backfill window misplaced them into
  a synthetic step with no later repair
- transcript: cold grouping initializes tool frames as 'running' and lets
  the tool-message branch transition them to done/error — an approval-
  gated or still-executing tool no longer shows as completed on rebuilds

Addresses review feedback on #1888.

* fix(transcript): seed live-created agents, merge backfills live-first

- kap-server: agents created after binding are marked seeded immediately —
  their projector covers every event from creation on, so their pendings
  announce without waiting for an explicit history read (which previously
  left live subagent approvals/questions stuck in the unseeded queue)
- kap-server: the initial backfill merges turns live-first via
  healTurnOps (snapshotToOps gains a turn-mapper parameter) — live frame
  fields landed during the disk read (display/approvalId, longer text)
  are no longer replaced by the staler persisted version

Addresses review feedback on #1888.

* fix(transcript): count pages in turn segments, not head units

- transcript: the leading non-turn unit no longer consumes a turn slot —
  pages are counted in turn segments and the head unit rides only with the
  page reaching the first turn. A timeline with a head marker and exactly
  pageSize turns used to drop the marker from the newest page and
  hallucinate an older marker-only page (has_more: true with no older
  turns)

Addresses review feedback on #1888.

* fix(transcript): send baseline resets after cursor replay

- kap-server: broadcaster.subscribe gains deferTranscriptReset (recording
  prev grades/filter per target) plus flushTranscriptSeed; the v1
  connection defers the transcript baseline on cursor-carrying
  (re)subscribes and flushes it after replay — a reconnecting client no
  longer sees the reset's current seq ahead of the replayed lower-seq
  backlog

Addresses review feedback on #1888.

* fix(transcript): gate the ops fan-out only when a reset is coming

- kap-server: willSendTranscriptReset decides upfront whether any reset
  will be sent (grade upgrade or widened legacy filter); a same-grades
  resubscribe no longer un-seeds the target, so ops emitted mid-resubscribe
  keep flowing instead of being silently dropped by the fan-out gate

Addresses review feedback on #1888.

* fix(transcript): seed subscribers even when no reset is owed

- kap-server: a no-reset subscription (e.g. a client subscribing to a
  fresh session with an empty roster) now still marks the target seeded
  after subscribeTranscript completes — roster resets and ops would
  otherwise stay gated forever once agents appear

Addresses review feedback on #1888.

* fix(transcript): guard mismatched appends, expose prompt via klient

- transcript: appendAtOffset now treats an overlapping chunk whose head
  does not match the local tail as a gap (diverged stream) instead of
  silently rewriting from the offset and dropping local content
- klient: add the optional prompt field to the turn.started event schema
  so SDK listeners receive it instead of zod stripping it

Addresses review feedback on #1888.

* fix(transcript): open turns for subagent run prompts in cold grouping

- transcript: add the subagent system trigger to the turn-opening set —
  a subagent's run prompt (persisted as system_trigger/'subagent') always
  launches a new engine turn, so resumed subagent histories no longer fold
  the response into the previous turn or lose the prompt

Addresses review feedback on #1888.

* fix(transcript): guard bus subscriptions independently of projectors

- kap-server: subscribeAgent now guards on a dedicated subscribedAgents
  set instead of projector existence — a projector seeded before its
  agent's handle exists (e.g. during an on-demand backfill) no longer
  blocks the bus subscription, so the agent's live events keep flowing

Addresses review feedback on #1888.

* fix(kimi-inspect): reconcile the transcript on every socket open

- apps/kimi-inspect: TranscriptWs now reports onReconnected on the FIRST
  successful open too, not only on re-established ones — ops emitted
  between the REST page load and the subscription (a delayed or failed
  first connection) were previously lost onto a stale store; the consumer's
  refresh guard drops the no-op call while the initial load is in flight

Addresses review feedback on #1888.

* fix(transcript): derive the active step, dedupe tool error rendering

- kap-server: the projector gains a stepOrdinal lookup backed by the
  engine's activity view (resolved lazily through the agent lifecycle), so
  deltas after a late attach at step >= 2 land in the real active step
  instead of a synthesized t<N>.1
- apps/kimi-inspect: render a tool frame's error only when it differs from
  its output — onToolResult sets both to the same string for failed tools,
  which drew the failure twice in red

Addresses review feedback on #1888.

* fix(kimi-inspect): reconcile the transcript on the subscribe ack

- apps/kimi-inspect: TranscriptWs now fires onReconnected when the
  subscribe ack for its client_hello arrives instead of at socket open —
  the server attaches the transcript stream only after processing
  client_hello, so a refresh fired at open could finish before the
  subscription was active and still miss the ops in between

Addresses review feedback on #1888.

* fix(kimi-inspect): coalesce concurrent transcript refreshes instead of dropping

A subscribe ack landing while the initial REST load was still in flight
hit the `if (refreshing) return` guard, so ops emitted between the REST
page snapshot and the WS subscribe were neither in the page nor
delivered over the socket. Replace the drop guard with a coalesced
runner: at most one refresh in flight, and triggers during a run are
collapsed into exactly one follow-up run after it settles.

* fix(kap-server): force the transcript baseline after cursor-based replay

A cursor re-subscribe at unchanged grades deferred its baseline and then
compared against the previous grades on flush, so no reset was sent —
while volatile ops fanned out during the deferral had been dropped,
leaving the client with a permanent gap. flushTranscriptSeed now always
seeds a full baseline (previous grades no longer tracked in the deferred
record), and a regression test covers the same-grade cursor resubscribe.

Also drop the inline comments added to shellCommandService.ts — the
agent-core-v2 convention keeps commentary in the top-of-file block; the
context moved there.

* fix(kap-server): harden transcript seeding against stale and wildcard subs

Two subscribeTranscript gaps found in review:

- Re-read the target's subscription after the history awaits: subscribe
  work runs asynchronously, so an overlapping downgrade/unsubscribe used
  to be answered with resets computed from the stale spec. The reset
  loop now uses the latest grades/filter from state.targets and bails
  when the target is gone or no longer graded.
- Backfill roster agents admitted via the wildcard grade, not just
  explicitly named ones: a historical subagent seeded into the roster
  from session metadata had no materialized AgentTranscript, so
  wildcard subscribers silently never received its baseline reset.

Adds regression tests for the wildcard backfill, the mid-seed
downgrade, and the mid-seed unsubscribe (all three fail without the
fix); makeCore now accepts persisted agent metadata for roster seeding.

* fix(transcript): let meta.merge clear mode badges on mode exit

`agent.status.updated` with `planMode: false` / `swarmMode: false` was
dropped by the transcript projector because `meta.merge` could only set
mode badges, never clear them — clients kept rendering an exited mode
until the next full reset. The merge wire shape now accepts `null` per
mode key (set = object, clear = null, absent = keep): the reducer
deletes the key and normalizes an empty `modes` away, the zod schema
validates the nullable form, and the projector emits the clearing op
for exit events.

* fix(agent-core-v2): keep system-turn steering text out of turn prompts

`turn.started.prompt` was populated from the turn input for every
origin, so system-triggered turns (goal continuation, subagent run,
cron) exposed their internal steering text to live transcript
consumers; the cold rebuild mirrored the same leak when grouping
persisted history. The loop now populates the prompt only for
displayable user origins (user input, or a user-slash skill/plugin
activation) via the new isDisplayablePromptOrigin gate, and the cold
grouping opens hidden-origin turns promptless. Turns still open
normally — only the prompt text is withheld.

* fix(kap-server): reattach the transcript fan-out after a session reload

When the engine session closed or archived, TranscriptService dropped
the live store together with its ops listener set, but the
broadcaster's SessionState kept its transcriptStream — so
ensureTranscriptStream returned early for a later subscribe on the
resumed session, delivering a fresh reset but never the live
transcript.ops. The stream is now pinned to its TranscriptStore
instance and the fan-out re-registers whenever a rebuilt store shows
up. Adds a regression test that drops the service entry mid-stream and
asserts ops keep flowing after resubscribe (fails without the fix).

* fix(transcript): map legacy background_task origins in cold rebuilds

Legacy/v1 sessions persist background-task notifications with
origin.kind === 'background_task' (the live mapper already handles that
spelling), but the cold grouping only mapped 'task' — after a restart
those turns fell through to { kind: 'other' } and lost their taskId, so
the transcript could no longer associate the notification turn with its
background task. Both spellings now share the task-origin branch.

* fix(kap-server): project no-taskId shell failures into the transcript

A foreground `!` command that failed before onForegroundTaskStart ran
(Bash validation/spawn/registerTask errors) published shell.output /
shell.completed with taskId undefined, and the projector's guard
dropped them — the live transcript lost the stderr and the terminal
state of a command that did run. Shell events now resolve their task as
the id learned at shell.started, else the event's own taskId, else a
synthetic per-command id (shell-<commandId>), so early failures land
like any other shell task.

* refactor: drop the /api/v2 RPC surface and the klient http transport

- kap-server: remove the /api/v2 REST routes and /api/v2/ws socket (registerRpcRoutes renamed to serviceDispatcherRoutes; transport/ws/{eventMap,registerWs,wsClient,wsConnection,wsProtocol} deleted). /api/v1/debug/* is now the only RPC surface — a reflection dispatcher over the entire scoped DI registry with no whitelist — and /api/v1/ws the only WS endpoint
- klient: drop the http transport (transports/http/*, transports/ws/wsSocket.ts) and the kap-server devDependency; transports reduce to the ipc|memory subpath entries, and the dual/v2 e2e suites go with them
- kimi-inspect: target /api/v1/debug only with no fallback, replace the Service-event push channel (wsChannel/wsSocket) with on-demand fetch plus 15 s polling, and show a blocking "Debug surface unavailable" screen on connection failure
- transcript: add global attachment/interaction/todo entities (model, ops, wire schema) and project them from engine events in kap-server's coreEventMap

* fix(kimi-inspect): drop the unused TranscriptTodo import
2026-07-20 15:33:05 +08:00
github-actions[bot]
a05228c671
ci: release packages (#1868)
Some checks are pending
CI / lint (push) Waiting to run
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 / Publish native release assets (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
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-20 13:55:49 +08:00
qer
d71bf9e5a5
feat(web): add a cache invalidation note to the model switcher (#1940)
* feat(web): add a cache invalidation note to the model switcher

* feat(web): show the cache note in the mobile settings sheet
2026-07-20 13:43:05 +08:00
liruifengv
11c1683a1c
feat: scope thinking effort to the current session (#1933)
* feat: scope thinking effort to the current session

Selecting a model or thinking mode in the TUI or web UI no longer
persists the concrete effort to the config; only the boolean thinking
toggle is saved. The web UI now restores and submits each session's own
daemon-reported level instead of a browser-wide per-model pick, whose
localStorage store is removed.

* feat: persist bounded thinking efforts and migrate persisted max once

Only low/medium/high/xhigh are written to the config on a pick; max and
unrecognized levels stay session-only with just the boolean toggle saved.
A one-shot migration rewrites a previously persisted thinking.effort max
to high (recorded in migrations.json, never re-run). The web UI brings
back the per-model localStorage pick as the seed for new sessions, while
a session's own daemon-reported level keeps winning for existing ones.

* feat: gate effort persistence on the model top declared level

The session-only tier is now the last entry of the model's own
support_efforts (ordered by strength) instead of a fixed name list, so
custom provider-declared levels get the same treatment; anything below
the top persists as before, and unknown model metadata keeps the
concrete effort.

* docs: align changeset wording with the top-tier persistence rule

* refactor: rename the config migration marker file to migrations-effort.json

* feat: drop the legacy global thinking pick fallback in the web UI

A raw single-level string left in localStorage by older versions was
migrated into a '*' fallback entry applied to every model; stale values
(typically max) could silently steer any session. Non-map content is now
discarded instead of inherited.

* feat: drop the web per-model thinking pick store entirely

Thinking in the web UI is now sourced only from the daemon: a session's
own reported level wins when the model declares it, and everything else
falls back to the model's catalog default. The stale localStorage key is
cleaned up on startup.

* feat: skip the effort write when the picker confirms its initial value

Re-confirming the effort shown when the model/effort picker opened is not
an explicit choice: the TUI persists only the model (no effort key, or no
write at all when nothing changed), and the web skips the global config
write the same way it already did for model switches.

* revert: keep the web global thinking write on re-confirm for now

Only the TUI skips the effort write when the picker confirms its initial
value; the web behavior stays unchanged until the interaction is
reconsidered.

* fix: carry the draft thinking pick into a newly created session

A level picked on the empty composer lived only in rawState.thinking, so
selectSession's watcher overwrote it with the catalog default and the
first prompt/skill submitted that instead of the pick. createDraftSession
now captures the draft level and seeds the new session's own entry.

* docs: simplify the changeset wording

* fix: close the remaining top-tier persistence and draft-capture gaps

The provider-add default-model flow now resolves support_efforts through
effectiveModelForHost (overrides + protocol-profile inference), so a
top-tier pick on catalog models without declared efforts no longer
persists. The draft thinking level is captured before the session
creation awaits, so a concurrent session switch can no longer seed the
new session with another session's effort.

* fix: wait for the session status fold before resolving prompt thinking

In the cold window right after a reload or session switch, the session's
own level has not landed from /status yet; resolving straight to the
catalog default would carry the wrong level to the daemon, which writes
it into the session profile. Send, steer, side-chat and skill-activation
paths now await the fold when the session entry is missing.
2026-07-20 12:57:22 +08:00
7Sageer
df68995539
feat: add thinking_effort to turn telemetry events (#1916)
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-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
* feat: add thinking_effort to turn telemetry events

turn_started / turn_ended / turn_interrupted now carry the resolved
effective thinking effort in both engines, so per-turn and per-session
effort usage distribution becomes measurable. In agent-core-v2 the
profile domain mirrors the effective effort (including the
KIMI_MODEL_THINKING_EFFORT override) into the ambient telemetry context
on model / thinking changes and after restore; the loop snapshots it at
turn start. In agent-core the turn flow reads config.thinkingEffort at
each emission point.

* fix(agent-core-v2): align turn telemetry with request config

* fix(agent-core-v2): preserve model-less turn startup
2026-07-19 21:41:21 +08:00
7Sageer
a3e773f90c
fix(agent-core-v2): make host fs stat follow symlinks and add explicit lstat (#1843)
* fix(agent-core-v2): make host fs stat follow symlinks and add explicit lstat

* chore: simplify changeset wording

* fix(agent-core-v2): preserve dangling git root markers
2026-07-19 16:18:02 +08:00
Haozhe
8b9916c308
feat(minidb): switch agent-core-v2 query-store to ClusterDb with 16 shards (#1907)
- ClusterDb: add query() (per-shard fan-out with skip=0 and
  limit=skip+limit, global re-sort, then skip/limit) and compound index
  management (create/drop/list through the cluster registry, fanned out
  to every shard and caught up on shard open)
- MiniDbQueryStore: replace the single MiniDb with a 16-shard ClusterDb,
  so multiple kimi processes share the read model instead of failing
  with storage.locked; per-shard LockError now propagates as a transient
  error rather than permanently disabling the read model
- corruption: lift openOrRebuild's predicate (SyntaxError /
  CorruptFrameError) to one process-lifetime wipe-and-reopen rebuild
- lower lockAcquireTimeoutMs to 1s for the cache read model
- tests: cluster query merge and compound fan-out; store coexistence
  with a peer instance, corrupt-registry rebuild, 16-shard topology;
  sessionIndex locked-fallback test now drives a stub IQueryStore
2026-07-19 16:04:26 +08:00
Haozhe
a41a09c33c
feat(cli): replace the kimi server command tree with kimi web and share one home across servers (#1826)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (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-pi-tui (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
* feat(kap-server): enable multi-server shared home by default

- always register kap-server instances under server/instances and drop the
  legacy single-instance lock (acquireLock/getLiveLock/ServerLockedError)
- remove the multi_server experimental flag and its
  KIMI_CODE_EXPERIMENTAL_MULTI_SERVER env var from agent-core-v2
- discover running servers via the instance registry in server
  ps/kill/rotate-token, kimi web daemon reuse, and the desktop app
- remove the pending minidb changesets

* feat(cli): add per-instance targeting to server kill and ps

- `kimi server kill [serverId]` stops only the matching instance; without
  an id it still stops the longest-running one, and an unknown id errors
  with the live server ids listed
- `kimi server ps` lists connections grouped per server id (`--json`
  nests them under a per-server object); an unreachable instance degrades
  to a per-server note instead of failing the whole listing
- update the zh/en command reference and the multi-server changeset

* feat(cli): replace kimi server with the kimi web command tree

- `kimi web` now runs the local server in the foreground and opens the
  browser; the background daemon (ensureDaemon / spawn / idle-exit) is
  removed, so repeated runs simply start another instance on the next
  free port
- drop the OS-service lifecycle (install/uninstall/start/stop/restart/
  status) together with kap-server's svc layer
- `kimi web kill [serverId]`, `kimi web ps`, and `kimi web rotate-token`
  manage instances from the registry
- the TUI /web command now connects to an already-running instance
  instead of spawning a background daemon
- update the zh/en command reference, dev scripts, and tests

* feat(cli): route kimi server invocations to a deprecation notice

Any `kimi server …` call — bare or with any legacy subcommand/flags —
now prints a deprecation notice pointing at `kimi web` and exits 1,
instead of failing with an opaque "unknown command". The shim is
scheduled for removal in the next major version.

* feat(cli): add the `all` keyword to kimi web kill

`kimi web kill all` stops every live instance in the registry (ULIDs
can never collide with the keyword). Each instance still gets the API
shutdown + SIGTERM/SIGKILL treatment; a failure on one instance does
not stop the sweep and is reported at the end.

* docs(changeset): drop the web-foreground-default changeset

The kimi web command tree replaces the foreground-default behavior this
entry describes: --background, daemon reuse, and the version-mismatch hint
no longer exist, so the pending entry would contradict the actual release
notes.

* docs(changeset): tighten the multi-server entry wording

* feat(cli): let /web pick a running server or start a new one

The /web picker now lists the live instances from the registry with
their versions (flagging a CLI mismatch) instead of only connecting to
the longest-running one, and offers starting a new server: that one
runs in the foreground attached to the terminal after the TUI exits,
via the restored exit-takeover wiring. formatReadyBanner is exported
and adapts its Stop hint to Ctrl+C for the attached case.

* feat(cli): skip the /web picker and start a new server when none is running
2026-07-19 11:43:46 +08:00
qer
4f3c7240c4
feat(cli): run kimi web and the TUI /web command in the foreground by default (#1853)
Some checks are pending
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 / 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 / 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
* feat(cli): run kimi web and the TUI /web command in the foreground by default

kimi web and /web now start the server attached to the terminal (Ctrl+C
stops it) instead of backgrounding a daemon; --background opts back into
the daemon behavior. kimi server run is unchanged. A foreground kimi web
reuses an already-running server instead of failing to bind, the /web
handoff prints the same ready banner as kimi web (plus the session deep
link), and the banner Stop hint adapts to the hosting mode.

* fix(cli): resolve the server token after the server is up in /web

The token lookup had moved ahead of ensureDaemon()/startServerForeground()
during the foreground-default refactor, so a first-time start (no
server.token on disk yet) opened the browser without the #token= fragment
and left the user at the auth gate. Resolve the token after the daemon is
healthy, and inside the foreground ready hook.

* docs(changeset): flag the web foreground default as breaking in the changelog

The bump stays minor, but the entry now calls out the behavior break and
the --background mitigation for scripts that expect kimi web to return
immediately.

* feat(cli): flag a version mismatch when reusing a server from an older CLI

After an upgrade, an older still-running server is reused as-is. The
lock's host_version is now surfaced: kimi web / server run print a
version-mismatch line next to the reuse notice, /web shows the same as a
status warning, and the docs plus changeset tell users a single
kimi server kill switches them onto the new version.
2026-07-18 23:32:41 +08:00
Kai
3086e47039
fix: unify YOLO and Auto permission mode descriptions across surfaces (#1867)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (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 / build (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(tui): correct YOLO and Auto permission mode descriptions

* fix: unify YOLO and Auto permission mode descriptions across CLI, ACP, web, and docs

* docs: correct YOLO and Auto mode descriptions in the interaction guide

* fix: correct YOLO mode notices in session replay and vscode extension

* feat(vscode): rename /afk command to /auto, keeping afk as hidden alias

Also correct the stale 'afk' mode reference in the built-in MCP config
skill guidance of both agent engines.

* fix(vscode): forward engine approval requests instead of blanket-approving them

The extension-level approval handler auto-approved every request when
legacy yolo/afk was on, silently swallowing the sensitive-file,
plan-review, and ask-rule prompts the engine yolo mode still sends.
Forward every request to the user and let the engine permission mode do
the auto-approving, matching TUI and web behavior.
2026-07-18 02:09:31 +08:00
qer
7d393b56fb
chore(vscode): release 0.6.3 (#1856) 2026-07-17 22:05:24 +08:00
liruifengv
ada523ae6a
docs(changelog): sync 0.27.0 from apps/kimi-code/CHANGELOG.md (#1852)
Some checks are pending
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
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
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 / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
2026-07-17 21:18:19 +08:00
github-actions[bot]
5cc194956f
ci: release packages (#1785)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-17 20:45:40 +08:00
qer
f441193e1d
chore: remove kimi-desktop app and desktop release pipeline (#1849)
* chore: remove kimi-desktop app and desktop release pipeline

The desktop client has moved to a dedicated internal repository. Drop the
Electron shell app, the desktop-build reusable workflow and its release
job, and all workspace references (dev script, typecheck filter,
onlyBuiltDependencies, flake.nix entries, lockfile). The kimi-web desktop
detection code is intentionally kept intact.

* chore: fix flake.nix after kimi-desktop removal

Complete the workspaceNames cleanup missed in the previous commit and
refresh the pnpmDeps fixed-output hash for the regenerated lockfile.
2026-07-17 20:32:47 +08:00
qer
56ba8e0196
fix(server): allow inline styles in CSP so web math renders on non-loopback binds (#1847)
* fix(server): allow inline styles in CSP so web math renders on non-loopback binds

The security CSP fell back to default-src 'self' for styles, which strips
the inline style attributes that KaTeX (injected via innerHTML) uses for
all glyph positioning — formulas collapsed into overlapping characters on
any non-loopback-served web UI. Shiki highlighting and Mermaid diagrams
hit the same mechanism. Add style-src 'self' 'unsafe-inline'; scripts
remain strictly restricted.

* test(server): assert the effective script-src in the CSP regression test

A negative substring check only rejects one exact string: default-src
gaining 'unsafe-inline' (inline <script> allowed via fallback) or an
explicit script-src 'unsafe-inline' would both slip through. Parse the
directives and assert the effective script policy (script-src, falling
back to default-src) excludes 'unsafe-inline'/'unsafe-eval'/data:.
2026-07-17 20:21:10 +08:00
qer
9e1248416f
fix(web): remember the thinking level per model (#1838)
* fix(web): remember the thinking level per model

Persist kimi-web.thinking as a JSON map of model id to level instead of a
single global value, and resolve the active level against the model's
catalog (stored pick when still declared, else the model default) at
loadModels, setModel, and on active-model changes via a watcher.

Fixes the empty, unresponsive thinking picker shown for a model that does
not declare a previously stored level (e.g. a max-only model with a stale
global 'low').

* fix(web): resolve a submitted prompt's thinking from its own model

submitPromptInternal and the steer path read the single active-session
rawState.thinking, so a queue drain for a background session submitted the
level of whichever session the user had switched to since enqueueing —
the same cross-model leak on the submit path. Thinking now joins model and
the per-session modes in being resolved from the prompt's own session
model (its stored pick when declared, else the catalog default), falling
back to the active value only when the model has left the catalog.

* fix(web): keep model switches from persisting derived thinking defaults

setModel routed the resolved level through applyThinkingLevel, which
writes per-model storage unconditionally — a switch to a model with no
saved pick stored the catalog default as if it were an explicit choice,
pinning the user to it across later default changes, and the rollback
path did the same write for a switch that never happened. Model switches
now update the in-memory level only; storage writes stay with
setThinking, the explicit picker path.

* fix(web): resolve thinking per target session on the BTW and skill paths

sendSideChatPromptOn combined the captured parent's model with the
active-session level, so a session switch during the startBtw await sent
the BTW first turn at the wrong model's effort — resolve it from the
parent's own model, falling back to the active value off-catalog, same
as the other submit paths.

activateSkill carries no thinking either, so the daemon ran skills at
the session profile effort, which can predate the per-model restore the
picker now shows. Persist the resolved level to the session profile
first, mirroring the new-session skill path; that path itself now
resolves against the new session's model instead of the raw active
value.

* fix(web): keep per-model thinking picks in memory as the runtime truth

The resolver re-read localStorage on every submission, letting storage —
not the displayed state — decide what the daemon receives: with storage
unavailable (policy/quota) an explicit pick reached the UI while every
submit path fell back to the catalog default, and a pick made in another
tab silently changed what this tab submits mid-session.

Per-model picks now live in an in-memory map hydrated from localStorage
at startup; explicit picks update it first and persist best-effort
(read-modify-write merge, so concurrent tabs' entries still survive).
localStorage is only hydration plus persistence — another tab's pick can
no longer alter this tab's runtime level.

* fix(web): carry the legacy global thinking pick forward as a fallback

Pre-map installs stored a single global level as a raw string; the map
parser dropped it, silently resetting the user's explicit preference to
the catalog default on upgrade. The legacy value is now carried as a
fallback for models without their own entry — validated against each
model's catalog at resolution, so effort models keep the user's pick
while a max-only model still falls through to its default and can never
be trapped by it.

* fix(web): keep the legacy thinking fallback across the first map rewrite

The first explicit pick after an upgrade rewrote the raw legacy value
into a map containing only that one model, so the next reload saw a
nonempty map and dropped the legacy fallback for every other model.
The migrated value now lives inside the map under a '*' key that no
real model id can collide with: per-model entries override it, and
rewrites persist it alongside them instead of deleting it.

* fix(web): persist only the changed thinking pick on write

Overlaying the whole in-memory map on write could revert a newer pick
made in another tab for a model this tab still held a stale copy of.
Write the changed entry alone (delta-style, like saveUnread), carrying
only the migrated legacy '*' fallback along so it survives the first
rewrite into map format.

* fix(web): abort skill activation when the thinking profile persist fails

persistSessionProfile surfaces failures itself and resolves, so awaiting
it never blocked a following activation: a failed /profile write still
launched the skill at the session's stale effort. It now resolves a
success flag; both activation paths (existing session and new-session
draft) gate on it and skip activating when the persist fails, without
reporting a second, synthetic error.

* refactor(web): persist the new-session skill profile's thinking once

startSessionAndActivateSkill persisted the resolved thinking and then
activateSkill persisted it again unconditionally — a redundant profile
update and status refresh whose transient failure would false-veto an
activation whose prerequisite profile was already applied. Thinking is
now written by activateSkill alone (the single, gated writer); the
draft patch carries only model, plan/swarm and permission.

* fix(web): throw an Error instance for the profile-persist sentinel

oxlint --type-aware (only-throw-error) rejects throwing a Symbol; the
identity-based sentinel works the same as a shared Error instance.

* fix(web): resolve an empty session model through the default before skills

session.model can be '' transiently (daemon profile echo), so
activateSkill fell back to the raw active-view level; in the new-session
flow a concurrent switch could persist another model's effort onto the
target session. Normalize '' through the configured default_model first,
same as the prompt/BTW/steer paths.
2026-07-17 19:56:26 +08:00
qer
18d3374137
fix(vscode): reliable cancel and preserved session model on attach (#1845) 2026-07-17 19:55:45 +08:00
qer
03021b6db7
fix(kimi-web): stop the prompt queue from ghost-sending stale attachments (#1833)
* fix(kimi-web): stop the prompt queue from ghost-sending stale attachments

Sending while a turn was running queues prompts locally. A failed flush
left entries stuck, and every later session open silently re-submitted
them with their old file attachments. Gate the drain on locally
witnessed turns, re-drive stuck entries FIFO from real events with a
failure budget, restore merged entries on steer failure, persist the
queue per session so a refresh loses nothing, and converge queues
across tabs via storage events.

* fix(kimi-web): merge cross-tab queue updates by entry id (review P1/P2)

Whole-record adoption could silently discard a prompt another tab
enqueued concurrently. Queue entries now carry a stable id and enqueue
timestamp; adoption union-merges snapshots by id, a shared TTL'd removal
set stops flushed/discarded entries from resurrecting (and being
flushed twice), an in-flight marker covers the submit window, and
manual reorders re-stamp timestamps so they survive merges. The flush
failure budget is also tracked per entry instead of per session, so
removing or reordering the head no longer hands its strikes to the
next entry.

* fix(kimi-web): single-flusher queue entries, merge order convergence, forget guard (review round 2)

Turn-end events reach every open tab and the server accepts concurrent
submissions as distinct prompts, so two tabs holding the same adopted
entry could both submit it. Entries now record their owner tab and only
the owner flushes (ownerless legacy entries flush anywhere); an idle
send adopts entries left behind by closed tabs so a stranded queue can
still drain. Cross-tab merges now keep the newer enqueuedAt copy per id
so a manual reorder converges instead of ping-ponging writes, and the
flush failure callback no longer resurrects a queue whose session was
forgotten while the submit was pending.

* refactor(kimi-web): drop the cross-tab queue persistence, keep the minimal fix

Cross-tab queue sync over localStorage is a distributed-systems problem
(claim/lease, conflict merge, ownership) that keeps generating review
findings far beyond this PR's scope. Remove the persistence/hydration/
adoption machinery wholesale; keep the bug fix proper: gated queue
drain, event-driven FIFO retry with a per-entry failure budget, steer
queue restore, and the forgotten-session flush guard. Durable queued
prompts will be designed together with the server-side prompt queue.

* chore: align the changeset wording with the reduced scope

* fix(kimi-web): never duplicate on ambiguous submit failures; advance after drop

Two review findings: (1) restoring merged queue entries after ANY steer
failure could re-submit prompts the daemon had already accepted when the
failure was a lost response — submits now report ok/rejected/uncertain
and restores + flush re-queues happen only on definitive daemon
rejections, while ambiguous failures drop the entry (the failure toast
still tells the user); (2) dropping an exhausted queue head no longer
strands the entries behind it — the new head is submitted immediately,
carrying its own failure budget.
2026-07-17 18:57:38 +08:00
7Sageer
fa7e4ba421
fix(agent-core-v2): follow symlinks when loading AGENTS.md (#1840) 2026-07-17 18:55:46 +08:00
qer
429521b669
fix(vscode): allow editor mentions for files outside the working directory (#1836)
* fix(vscode): allow editor mentions for files outside the working directory

* fix(vscode): quote editor mentions whose paths contain spaces
2026-07-17 18:02:50 +08:00
Kai
1b907b07cd
fix(tui): hide whitespace-only thinking from the transcript (#1829)
Models occasionally stream whitespace-only thinking (e.g. a single
space). It starts a thinking draft that renders as a bare bullet line,
both while streaming and when replaying session history. Skip
whitespace-only thinking deltas before they create a draft, and skip
whitespace-only think text at the component funnel so stored
whitespace think parts never render on replay. Stored thinking is
still replayed verbatim to the model.
2026-07-17 17:19:22 +08:00
liruifengv
bfecd0128f
feat: refresh model list for API-key providers at the managed endpoint (#1824)
* feat: refresh model list for API-key providers at the managed endpoint

* docs: simplify changeset wording

* docs: add kimi-code/k3 to the default config example

* fix: clear stale default model when a refresh drops its alias

* fix: clear refresh defaults via replace; set(undefined) cannot delete

* docs: drop the auto model refresh paragraph from providers page

* docs: simplify changeset wording
2026-07-17 17:17:58 +08:00
liruifengv
a5c568dc7a
feat(tui): add /copy slash command to copy the last assistant message (#1822)
* feat(tui): add /copy slash command to copy the last assistant message

The command copies the latest assistant reply (text parts only, skipping thinking, tool-call-only turns, error and internal messages) to the clipboard. Clipboard writes now also emit an OSC 52 sequence (tmux-aware) so copying keeps working over SSH and in containers where no native clipboard tool exists.

* fix(tui): gate /copy to idle and report OSC 52-only copies as unverified

During streaming the in-flight assistant text is not in getContext() history yet, so /copy would silently copy an older message; make the command idle-only like /export-md. copyTextToClipboard now returns how the text was delivered so /copy can say when only an unverified OSC 52 escape carried it.

* fix(tui): source /copy text from the visible transcript

After /compact the model context keeps only user messages plus a user-role summary, so scanning it found no assistant message even though the last reply is still on screen. Read the last assistant transcript entry instead — it matches what the user sees and survives compaction and resume.

* fix(tui): mark real model text in the transcript so /copy skips synthetic cards

Hook-result and goal-completion cards are also 'assistant' transcript entries appended after the real reply, so /copy could copy a hook card instead of the answer. Tag the single entry-creation site for genuine model text (both live and replay flow through it) and have /copy only accept tagged entries.
2026-07-17 16:39:19 +08:00
Haozhe
44f3341919
feat(minidb): ClusterDb sharding, incremental reader catch-up, and engine hardening (#1816)
* feat(minidb): add ClusterDb sharding and harden engine under stress

Cluster layer:
- add ClusterDb: hash-routed keys over N MiniDb shard directories with a
  per-shard lock pool (lease renewal, lockHoldMs yield, takeover on dead PID),
  merged ordered scans, cross-shard index registry, live cross-process read
  visibility, and crash-recovery handoff
- add cluster bench suite and multi-process test suites

Engine hardening (stress-driven fixes):
- compaction: pre-copy now gives up when the tail copy is not converging and
  rotation seals the old WAL (retryable), so compaction always terminates
  under sustained write storms and no committed write slips through rotation
- recovery: re-sync WAL size bookkeeping after torn-tail truncation; read-only
  opens create/modify no files and never compact under a live writer
- lockfile: stale-lock takeover via atomic bid-rename + settle; release only
  unlinks its own pid
- query: streaming candidates with skip/limit applied before materialization,
  plus Store.rawKeys for value-free key scans
- eviction: O(1) LRU victim picking via insertion-ordered access set
- TTL: adaptive expire budget drains simultaneous-expiry storms in seconds
- store: O(1) size fast path when no TTL is set; has() no longer materializes
  disk-backed values
- openOrRebuild preserves data when only a sidecar definition file is corrupt;
  sidecar definitions written atomically; stale compaction temps cleaned on open
- RESP server serializes replies per connection; over-64KiB tokens can no
  longer poison the full-text index

* feat(minidb): incremental WAL catch-up for cluster readers

- cluster readers: track a per-shard WAL watermark (dev/ino/offset) and
  catch up from appended frames instead of fully reopening; fall back to a
  full reopen on rotation, truncation, or index-definition changes
- MiniDb.catchUpFromWal applies WAL tail frames to a live instance (store
  plus secondary/dt/compound/text indexes), sharing recover()'s frame
  interpretation; RecoveryInfo exposes walScanEnd/dev/ino as the safe anchor
- lock-pool: incremental refresh with stats (incrementalCatchups,
  catchupFramesApplied); bench/reader-catchup shows p50 read latency drop
  from 130ms to 0.4ms at 10k keys and from 532ms to 0.4ms at 50k keys
  while a neighbor process writes
- compaction: keep the db writable on rotation failure (fresh WAL swap,
  remap once); count stats.compactions only on full success including the
  onCompacted hook
- restoreKey: seq guard so a failed op never wipes a concurrently
  committed value; eviction DELs retry on WAL seal
- text index: atomic build (stage then swap), createTextIndex registers
  only after a successful build, open-time cleanup of db.text-*.postings.tmp
- RESP server: swallow per-connection socket errors, reset the parser
  buffer after an oversized request, isolate per-command errors while
  preserving reply order

* chore(minidb): add changesets for reader catch-up and review hardening

* fix(minidb): support Windows in WAL rotation and lock takeover

- compaction rotation: on Windows, renaming over an open destination is
  EPERM, so replace renames with a retrying renameReplace helper and let
  go of the db's own ValueReader handles for the renames (reopened right
  after the pointer remap)
- value reader: hold snapshot/WAL handles only in valueMode 'disk'; in
  memory mode the handles were idle and, on Windows, blocked rotation
- lock takeover: retry the takeover bid's rename on Windows, but re-check
  the corpse before every attempt — a blind retry loop could land our bid
  late and overwrite an already-verified winner, double-holding the lock
- lock takeover: raise the co-bidder settle window to 50ms so loaded CI
  machines with tens-of-milliseconds descheduling still elect one winner
- test hardening for shared CI runners: a 30s minidb-wide timeout floor,
  batched prefills instead of sequential setup loops, explicit timeouts
  for process-spawning and heavy e2e tests, and Vitest 4 test() signature
  normalization

Verified green across macOS (arm64), Windows Server 2022 (x64, 2-core),
and Ubuntu 22.04 (launchpad aarch64): 321 passed, 1 skipped in each.

* fix(minidb): make stale-lock takeover exactly-one under CI load

- takeover now registers a liveness watch file before touching the lock,
  so every contender is visible to every other for the whole attempt;
  the settle/verify loop abstains while any live foreign watch exists.
  Settle-only heuristics could not survive a bidder descheduled before
  its bid write on shard-parallel CI runners (observed double-holds on
  ubuntu-latest and on a 2-core Windows box).
- adaptive settle scales with the attempt's own wall clock (floored,
  capped), replacing the fixed window.
- keep the Windows EPERM tolerance in the bid rename, re-inspecting the
  corpse before every attempt on ALL platforms, not just win32.
- lint: fix restrict-template-expressions in an e2e RESP helper.
- test: widen the cluster wait-read budget for slow CI spawns.

* fix(minidb): clean remaining CI lint and consumer-test failures

- wrap the compaction-storm error interpolation as String() so the
  type-aware restrict-template-expressions lint passes
- agent-core-v2's minidb query-store corruption test now expects the
  intended semantics: a corrupt index-definition sidecar is dropped while
  the data survives, and the definition can be re-registered

* fix(minidb): harden ClusterDb index administration across processes

Address three multi-process administration races in the cluster layer:

- findRange now merges candidates from all shards first and only then
  applies reverse/offset/count globally, instead of clipping per shard
  and discarding reverse
- cluster.indexes.json mutations go through a compare-and-swap loop
  (reload, re-apply idempotently, publish, verify) with an in-process
  mutex, so concurrent create/drop from two processes loses neither
  registry entries nor shard sidecars
- a failed createIndex/createTextIndex fan-out now rolls back exactly the
  shards it already created on, so the registry and every shard agree
  whether an index exists

Also make LockFile sidecars (tmp/bid/watch) unique per acquire attempt:
two lock users in the same process (independent shard pools) must never
share a path, or one user's cleanup would delete the other's in-flight
file.
2026-07-17 15:57:10 +08:00
Haozhe
9b496946dc
feat(kimi-inspect): add kap-server web inspector with dev-only /api/v1/debug RPC surface (#1806)
* feat(kimi-inspect): add web inspector for kap-server /api/v2 surface

- new apps/kimi-inspect app: connect screen (server URL + optional bearer
  token, persisted in localStorage, deep-linkable via ?url=/?token=),
  workspace/session browser sidebar, per-session chat view, and live
  Service panels with data and trigger buttons for Session/Agent scopes
- built on @moonshot-ai/klient (HTTP for calls, /api/v2/ws for events);
  Vite dev server proxies /api to a running kap-server
- register the workspace in AGENTS.md project map and flake.nix
  workspacePaths/workspaceNames

* fix(kimi-inspect): align dependency versions with the workspace (sherif)

* feat: dev /api/v1/debug RPC surface and kimi-inspect channel rework

- kimi-inspect: replace @moonshot-ai/klient with an in-app old-klient-style
  channel layer (service-bound IChannel, HTTP ProxyChannel, shared /api/v2/ws
  socket with ref-counted event listens), typed by agent-core-v2 interfaces;
  /channels descriptors + serviceByName keep every wire protocol loaded 1:1
- kimi-inspect: local server auto-discovery (Vite middleware over the
  kap-server instance registry + home token), zero-config startup connect,
  and a header switcher for runtime server switching
- kap-server: wire the dormant --debug-endpoints flag to a new
  whitelist-free /api/v1/debug dispatcher (every scoped service callable),
  gated to loopback binds; repo dev scripts pass the flag
- kimi-inspect: probe the debug surface at connect, falling back to /api/v2
  on servers without it
- tests: channel + discovery unit tests in kimi-inspect; debug RPC and
  loopback-gating coverage in the kap-server rpc/debugNonloopback suites

* chore(changesets): ignore @moonshot-ai/kimi-inspect

The private dev app never ships, so it should never appear in a changeset.
Add it to the changeset config ignore list (next to vis*) and note the rule
in the gen-changesets skill.
2026-07-17 15:56:53 +08:00
qer
a76d54bd8a
chore(ci): remove the VSIX package audit job (#1823) 2026-07-17 15:16:46 +08:00
qer
66cc279975
chore(vscode): release 0.6.2 (#1820) 2026-07-17 15:00:02 +08:00
Haozhe
56a321d4d1
fix(workspace): dedupe workspaces across Windows path spelling variants (#1809)
* fix(workspace): dedupe workspaces across Windows path spelling variants

The same directory reached the workspace registry as distinct strings on
Windows (drive-letter casing, typed vs on-disk casing, slash style), and
every identity check compared exact strings, so one folder could appear
as multiple workspaces with sessions split across hash-keyed buckets.

- add workspaceRootKey (slash-normalize + case-fold Windows-shaped
  paths) in agent-core, agent-core-v2, and the web app, and compare
  roots by identity key everywhere instead of exact strings
- registry createOrTouch folds alias spellings onto the existing entry
  instead of minting a new workspace id; session buckets reuse the
  registered id via a resolver in the v1 session store
- list endpoints expand alias buckets (resolveAliasIds /
  resolveAliasWorkDirs, including session-index-only spellings) so
  previously split workspaces list all sessions and counts under one
  merged group; session_index entries use the registry-resolved id

* fix(workspace): fold the runtime touch path and drive-root identity keys

Two gaps in the Windows path-spelling folding, both reachable in the
v1 session-create flow:

- touchWorkspaceRegistry minted the alias spelling's id outright; the
  freshly persisted alias entry then became the resolver's preferred id
  on the next create, splitting sessions into a duplicate bucket again.
  It now folds onto the identity-matching existing entry, mirroring the
  registry service.
- workspaceRootKey stripped trailing separators before testing the
  Windows shape, so a drive root (C:\) collapsed to C: and escaped the
  case-fold. The shape test now runs before the strip in all three
  copies (agent-core, agent-core-v2, web).

* fix(workspace): unfold symmetric operations that escaped the identity key

Two asymmetric spots left the folded comparison one-sided:

- the web app matched hidden roots by folded key but cleared them on
  re-add by exact string, so hiding C:\Foo and re-adding c:\foo kept
  the workspace hidden forever; clearing now folds too
- registry delete (both engines) removed and tombstoned only the exact
  id, so a legacy split sibling resurfaced as the directory's
  representative on the next list; delete now removes every registered
  spelling sharing the root's identity key and tombstones the full
  alias set (registered ids plus session-index spelling mints), so the
  session-index merge cannot resurrect the directory either
2026-07-17 14:56:06 +08:00
qer
b53e00db91
fix(oauth): include the transport root cause in connection error messages (#1808)
* fix(oauth): include the transport root cause in connection error messages

* chore: add changeset for oauth connection error diagnostics
2026-07-17 14:45:16 +08:00
qer
1a5c0de19a
fix(vscode): stop mid-turn core errors from corrupting the active turn (#1807)
* fix(vscode): stop mid-turn core errors from corrupting the active turn

* fix(vscode): reject prompts during exclusive operations with a terminal error
2026-07-17 14:40:43 +08:00
liruifengv
cec15e2188
fix(tui): dismiss /btw panel before cancelling compaction on Esc and Ctrl+C (#1811) 2026-07-17 13:29:12 +08:00
Haozhe
31449728b7
fix(security): close FetchURL SSRF bypasses and DNS-rebinding window (#1791)
Some checks are pending
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
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 / VSIX package audit (darwin-arm64) (push) Waiting to run
CI / VSIX package audit (all) (push) Waiting to run
CI / VSIX package audit (win32-x64) (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 / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Release / Native release artifact (push) Blocked by required conditions
* fix(security): close FetchURL SSRF bypasses and DNS-rebinding window

- resolve hostnames via DNS and reject any target resolving to loopback /
  RFC1918 / link-local / CGNAT / ULA ranges, including IPv4-mapped IPv6
  forms (e.g. localtest.me, [::ffff:7f00:1]) — the static host denylist
  only matched literals and could be bypassed by crafted domains
- follow redirects manually with full per-hop revalidation (10-hop cap)
  instead of auto-following, so a public URL cannot 302 the fetcher at
  internal services or cloud metadata endpoints
- pin each connection to the DNS answers the check validated (per-hop
  undici Agent with a pinned lookup), closing the TOCTOU / DNS-rebinding
  window between the check and the connect; skipped when a proxy is
  configured or allowPrivateAddresses is set
- apply to both agent-core and agent-core-v2 providers, with SSRF /
  redirect / pinning test coverage

* chore: add changeset for FetchURL SSRF hardening

* test: bridge undici type declarations in fetch pinning tests

* fix(security): keep dispatcher option lib-agnostic for DOM typecheck consumers

* fix(security): address review — pin NO_PROXY bypasses, drain oversized bodies, header-only comments
2026-07-17 11:21:05 +08:00
Kai
373abb02f0
fix(agent-core): record and close tool calls interrupted mid-stream (#1790)
Some checks are pending
CI / build (push) Waiting to run
CI / VSIX package audit (darwin-arm64) (push) Waiting to run
CI / VSIX package audit (all) (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / VSIX package audit (win32-x64) (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
CI / lint (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
When the provider stream breaks off mid-tool-call (pause_turn, engine
overload, token limit), the step ended with the partial tool call dropped
silently: never executed, never recorded. If the response carried no other
usable content, the persisted assistant message was effectively empty and
every subsequent request — including compaction — was rejected with
"assistant must not be empty" (HTTP 400), wedging the session.

Record each unexecuted call instead (arguments sanitized to {} when the
truncated JSON is unparseable) and immediately close it with a synthetic
interrupted error result. The exchange stays wire-valid, the history stays
truthful, and the model learns the calls never ran so it can re-issue them.
2026-07-16 21:12:00 +08:00
Haozhe
319001ae5c
refactor: remove git detection from workspace wire and folder browse (#1787) 2026-07-16 21:05:19 +08:00
Kai
365ba0001d
fix: use timestamped default filename for session debug exports (#1788)
Some checks are pending
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
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / VSIX package audit (darwin-arm64) (push) Waiting to run
CI / VSIX package audit (all) (push) Waiting to run
CI / VSIX package audit (win32-x64) (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Both engine implementations defaulted the export ZIP path to
<sessionId>.zip, so running /export-debug-zip or kimi export twice for
the same session silently overwrote the first archive. The default
filename is now kimi-debug-<shortId>-<timestamp>.zip (UTC, second
precision), matching the /export-md naming convention. Explicit
-o/outputPath behavior is unchanged.
2026-07-16 19:32:06 +08:00
liruifengv
3423ab8273
docs(changelog): sync 0.26.0 from apps/kimi-code/CHANGELOG.md (#1786) 2026-07-16 19:15:22 +08:00
github-actions[bot]
36b05820cb
ci: release packages (#1767)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-16 18:54:29 +08:00
qer
3ace52b697
chore(vscode): release 0.6.1 (#1783)
* chore(vscode): release 0.6.1

* test(vscode): read the extension version from the manifest in version assertions

* test(vscode): declare version on the runtime rig type
2026-07-16 18:53:16 +08:00
Haozhe
d465591eb3
fix(agent-core-v2): skip no-op agent registration on session resume (#1784)
- SessionMetadata.registerAgent now compares the incoming metadata with
  the persisted entry (tolerating persisted-JSON artifacts: dropped
  undefined keys, null parentAgentId, label order) and skips the write
  entirely when nothing changed
- previously the first resume per server process rewrote state.json via
  the status rollup's ensureMainAgent -> doCreate path, bumping
  updatedAt and pushing the session to the top of the list
2026-07-16 18:50:12 +08:00
Haozhe
072eed476b
fix(agent-core-v2): keep context size readings on the measured path (#1782)
* fix(agent-core-v2): keep context size readings on the measured path

The step fold creates the assistant message in the context before the
exchange finishes (a skeleton at step.begin, filled by content.part folds
during streaming), and the input array llmRequester passes to
contextSize.measured() is that same live array — it already includes the
output. Taking input.length + output.length therefore counted the folded
output twice, storing a measured prefix length one past the live context.
The inflated length permanently failed get()'s measured fast path, so
reads silently fell back to per-message estimates (e.g. ~50 tokens shown
for a ~29k-token "hi").

Take the live context length as the measured prefix instead (input and
context are identical under the identity guard), and clamp the measured
prefix to the context length in get() so a bad record can never knock
reads off the measured path again.

Add contextSize tests driving real turns that assert the wire model,
get(), and rpc getContext against the exchange totals.

* feat(klient): add context-usage example tracing a fresh session

Polls agent.getContext()/agent.getUsage() and streams agent events for a
new session after one "hi" against a real server, printing a timeline of
when the context/token readings move, plus a final consistency check
comparing the measured tokenCount to cumulative usage. Model seeding is
optional via KIMI_EXAMPLE_* env; the server token resolves from
<kimi-home>/server.token like the v2 e2e helpers.

* chore: add changeset for the context size fix
2026-07-16 18:37:28 +08:00
_Kerman
09e855401b
fix: report task stop reasons to the model (#1781)
* fix: report task stop reasons to the model

* chore: add task stop reason changeset
2026-07-16 18:32:06 +08:00
qer
ba36c6a563
fix(vscode): make the gear sign-in actually log in (#1779)
* fix(vscode): count configured provider credentials as signed in and make the gear sign-in actually log in

* fix(vscode): keep the gear auth action scoped to the Kimi account session
2026-07-16 18:21:53 +08:00
Kai
d531398d01
fix: scope Anthropic effort fallback profile to non-Kimi providers (#1765)
* fix: scope Anthropic effort fallback profile to non-Kimi providers

Managed Kimi models routed through the Anthropic protocol (protocol =
"anthropic", no catalog-declared think_efforts) inherited the inferred
latest-Opus effort profile, so the UI showed reasoning effort choices the
server never declared. The fallback now applies only when the provider type
is known, non-kimi, and the effective wire protocol is Anthropic; Kimi
providers keep catalog-declared efforts only, and callers without provider
context fall back to name matching.

* fix: align v2 catalog with resolver for providerless Anthropic models

Flat models (inline base_url, no named provider) and providers without a
declared type now fall back to the model's own protocol when deciding the
Anthropic fallback effort profile, so the model catalog stays consistent
with runtime resolution.

* fix: keep flat Anthropic model effort metadata in TUI and ACP catalogs

Flat models without a named provider (inline base_url, protocol:
"anthropic") have no provider entry to look up; fall back to the
model's own protocol as the provider identity so the effort picker and
ACP catalog stay consistent with runtime resolution.

* style: move v2 anthropic fallback rationale into the modelAuth header

The v2 comment convention keeps comments in the top-of-file block only;
drop the inline explanations added beside functions and statements.
2026-07-16 17:30:20 +08:00
qer
d1ca65e1de
feat(vscode): migrate extension to Node SDK (#1769)
* feat(vscode): migrate extension to Node SDK

* fix(vscode): address CI failures

* fix(vis): handle token count records

* fix(vscode): keep chat toolbar and header readable at narrow widths

* fix(vscode): map yolo to core yolo permission and honor the global yolo setting

* docs(vscode): record Node SDK migration design

* docs(vscode): split breaking changes out of the 0.6.0 changelog

* fix(vscode): keep a resumed session's thinking effort instead of reapplying the default

* fix(vscode): announce session status when a view attaches so the display matches it

* fix(vscode): align webview thinking effort handling with the TUI
2026-07-16 17:27:21 +08:00
Haozhe
ffaf0b98ca
feat(agent-core): align coder subagent tools with v2 and drain background tasks (#1776)
* feat(agent-core): align coder subagent tools with v2 and drain background tasks

- align the bundled coder subagent profile tools with agent-core-v2
  CODER_TOOLS (Skill, Agent, AgentSwarm, Task* trio, plan-mode tools,
  TodoList); cron tools stay declared-but-undelivered on sub agents,
  matching v2
- rebuild builtin tools in setActiveTools: profile-gated capabilities
  (Bash/Agent allowBackground via the Task* trio) were baked at
  construction with an empty enabled set, so profiles applied later
  (every subagent) silently lost them
- hold subagent completion until the child agent's background tasks
  settle (print-mode drain semantics) and suppress their terminal
  notifications, so no unobserved follow-up turn runs on a finished
  subagent; the run's timeout/cancel signal bounds the drain
- cover with real-Session e2e (tool execution, nested Agent/AgentSwarm,
  drain blocking and cancel) and update profile/subagent-host tests

* chore: add changeset for coder subagent tool alignment

* docs(agents): sync sub-agent capabilities with expanded coder tool set

- coder sub-agents can now dispatch nested sub-agents and use background
  tasks, todo lists, Plan mode, and skills; drop the stale claim that
  sub-agents cannot schedule nested sub-agents
- note that a sub-agent run reports completion only after its background
  tasks settle

* fix(agent-core): close drain race for tasks settled before completion

A background task that terminated before the drain's suppression pass was
excluded from the active-only list, so its terminal notification escaped
suppression and could still steer an orphan turn onto the finished
subagent. Suppress every child task (including settled ones whose
notification may still be in flight) and run the pass both before and
after the settle wait; notification delivery re-checks suppression after
its async output snapshot, which makes the block deterministic. Cover the
mid-turn delivery path with an e2e case.
2026-07-16 17:14:53 +08:00
Kai
3d5d630c12
fix: honor explicit thinking off on OpenAI-compatible providers (#1774)
An explicit withThinking('off') collapsed to the same internal state as
"never configured" on chat-completions providers, so the history-based
auto reasoning_effort injection (#1616) silently switched reasoning back
on and could leak the field to models that reject it. Store the requested
effort verbatim and derive the wire encoding per request, suppress the
auto-enable for an explicit 'off', and report the accurate current effort
('on'/'off') instead of recording 'off' for both.
2026-07-16 17:11:18 +08:00
Kai
1169a6d5fd
fix: replay empty thinking verbatim on preserved-thinking endpoints (#1773)
Remove the wire-level fallback that substituted a space for empty
thinking content when replaying assistant history to Anthropic-compatible
and Kimi endpoints with thinking keep=all. The strict endpoints that
motivated the placeholder no longer reject empty thinking, so the
placeholder only distorted replayed history.
2026-07-16 17:00:06 +08:00
Haozhe
ba49e3a2f3
chore: drop changesets merged with the klient facade (#1768) (#1775)
The klient facade PR landed with its release notes planned for a later
feature release; removing the two pending changesets so the next release
does not pick up version bumps and changelog entries for it.
2026-07-16 16:49:44 +08:00
Haozhe
9e3e6700f9
fix(agent-core-v2): settle killed subagent runs only after the child loop goes idle (#1759)
- awaitRun: cancel by turn id so aborts also reach turns still queued, not just the active turn
- awaitTurn: cancel first, then wait for turn.result instead of racing against the abort signal, so task settlement, the task.killed notification, and the resume guard never observe the run as finished while the loop is still unwinding
- rethrow the original abort reason so consumers keep matching it by identity
- add regression test for the manual stop -> auto-resume "already running" race
2026-07-16 16:47:56 +08:00
Haozhe
4cffd732c2
feat(klient): contract-driven facade with http/ipc/memory transports (#1768)
* feat(klient): contract-driven facade with http/ipc/memory transports

- add zod-validated contract sections (global/session/agent) under
  src/contract and a facade exposing global.*, session(id).*, agent(id).*
- select transport once at creation via subpath entries
  (@moonshot-ai/klient/http|ipc|memory); drop legacy channel/client/
  httpChannel/wsChannel/wsKlient/proxy implementations
- absorb packages/server-e2e into packages/klient test/e2e suites
  (dual-backend, legacy v1, v2 wire) and remove the server-e2e workspace
- expose model registry and catalog services on kap-server v2 RPC surface

* fix(klient): derive session status from agentActivityView

The engine retired its sessionActivity service in #1751 (session busy is
now derived from agent activity views), but the facade still called the
deleted wire channel and imported the deleted engine module, breaking
typecheck and every klient suite at import time.

- drop the sessionActivity contract/registry entries and mirror the
  agentActivityView service instead (agent scope)
- compose session status() client-side from the pending interaction lists
  and each agent's agentActivityView, keeping the retired service's
  precedence and typing SessionStatus locally in the facade
- replace the deleted-channel call in the v2 smoke suite with a
  sessionInteractionService probe
- fix the legacy image-file suite to wait with the harness's
  waitForSessionBusy
2026-07-16 16:43:09 +08:00
liruifengv
b5139757e2
fix: align context usage display with 1024-based units and ceiled percents (#1771)
* fix(web): align context usage display with 1024-based units and ring-only meter

- simplify the composer context meter to the ring only; the full
  used/max/pct numbers live in the tooltip
- format token counts with 1024-based k/M units via a shared formatTokens
  helper (256k context reads "256k", not "262k"), applied to the composer
  tooltip, status panel, mobile settings sheet, model picker, goal strip,
  and turn rendering
- ceil the usage percent so sub-0.5% usage still shows a sliver instead
  of an empty meter

* fix(tui): render context usage with 1024-based units and ceil percent

- formatTokenCount is now 1024-based ("256k", not "262.1k"); the footer,
  /status and /usage panels, subagent cards, and goal stats all share it,
  replacing five local 1000-based copies
- the footer and panel percents use an integer ceil (new usagePercent
  helper) so any non-zero usage shows at least 1% instead of "0.0%"

* fix(web): clamp the status panel context percent to [0,100]

ctxUsed can momentarily exceed ctxMax (estimates), which could flash a
"101%" readout — the composer and mobile sheet already clamp the same
ConversationStatus data, so apply the same clamp around the ceiled
percentage here.

* chore: merge the context usage changesets into one

* chore: reword the context usage changeset in English
2026-07-16 16:41:40 +08:00
Haozhe
78967e283d
refactor(model-catalog): drop WS catalog-changed event; refresh on picker open (#1772)
- kap-server: remove event.model_catalog.changed from the v1 WS union,
  broadcaster forwarding, and the zod event registry
- web: refresh all providers (POST /providers:refresh) before loading
  models when the model picker opens, replacing the event-driven refresh
- keep domain publishers, the protocol schema, and the web receiver for
  compatibility with older daemons
2026-07-16 16:34:32 +08:00
_Kerman
7042af3571
fix(web): keep the sidebar resize handle above the chat composer background (#1766) 2026-07-16 16:17:20 +08:00
liruifengv
81414b6ad5
feat: warn about context-cache loss when switching model or thinking effort (#1763)
* feat: warn about context-cache loss when switching model or thinking effort

* chore: polish the switch warning copy

* feat: wrap the switch warning instead of truncating it

* chore: bold /new in the switch warning

* Revert "chore: bold /new in the switch warning"

This reverts commit a438e87cda543202a6d4726c96cdabd73b8dcf6f.

* chore: align the changeset with the warning copy
2026-07-16 15:43:23 +08:00
liruifengv
ede69068ea
docs(changelog): sync 0.25.0 from apps/kimi-code/CHANGELOG.md (#1760)
Some checks are pending
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 / test (4) (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 / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Release / Release (push) Waiting to run
* docs(changelog): sync 0.25.0 from apps/kimi-code/CHANGELOG.md

* docs(changelog): simplify verbose 0.25.0 entries

* docs(changelog): trim diagnostics entry wording

* docs(changelog): drop crash telemetry entry

* docs(changelog): trim symlinked workspace entry

* docs(changelog): trim subagent output entry

* docs(changelog): drop refactor entry

* docs(changelog): simplify remaining verbose 0.25.0 entries
2026-07-16 10:42:48 +08:00
github-actions[bot]
50b919b67e
ci: release packages (#1752)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-16 10:17:32 +08:00
7Sageer
70211a727f
feat: attach trace_id to telemetry events (#1724)
* feat: attach KFC trace_id to telemetry events

Capture the x-trace-id response header in both kimi providers (kosong
and agent-core-v2 llmProtocol) via the openai SDK withResponse(), and
thread it through the request result chain into telemetry so events can
be joined with server-side request logs by trace id instead of
session_id + time window.

- kosong / llmProtocol: StreamedMessage/GenerateResult.traceId,
  GenerateOptions.onTraceId early capture (available mid-stream),
  APIStatusError.traceId on the error path
- v1 (agent-core): per-turn traceIdByTurn on TurnFlow; attach trace_id
  to turn_ended/turn_interrupted/api_error/cancel/tool_call(+dedup,
  repeat)/permission_approval_result/question_*/compaction_*
- v2 (agent-core-v2): AgentTelemetryContext carries turn_id/trace_id
  updated by llmRequester; registry gains trace_id on 13 events plus
  turn_id on turn_* and turn_id/step_no on api_error

* fix: attribute trace_id to the failed request on error paths

Review follow-ups for trace_id attribution on failure paths:

- v1: chatWithRetry feeds a failed attempt's APIStatusError trace id into
  the turn's traceIdByTurn before the loop dispatches turn.interrupted,
  so turn_ended/turn_interrupted on error turns attribute to the failed
  request instead of the previous successful step.
- v2: llmRequester keeps the onTraceId-captured trace in per-request
  state and trackApiError prefers it over error extraction, so failures
  after response headers arrived (empty response, mid-stream decode
  errors) no longer clear the ambient trace_id.
- v2: compaction_failed falls back to the ambient trace_id once a
  summarizer request actually hit the wire, covering mid-stream failures
  whose error carries no trace.
- kosong: parseTraceId filters empty x-trace-id headers and both kimi
  providers read the header through it.
- v2 test harness forwards onTraceId to GenerateFn so tests can simulate
  the early header capture.

* fix: attribute v1 api_error to the in-flight request on post-headers failures

- v1: a failure after response headers arrived (mid-stream decode error,
  empty response) carries no trace on the error itself, so api_error lost
  the trace the client had already captured via onTraceId. Add a per-step
  in-flight capture (written by onTraceId, cleared at step begin/end) and
  fall back to it when the error carries no trace, aligning with v2's
  per-request requestTraceId. Failures before any response headers
  (network errors, local aborts) still report no trace rather than
  leaking a previous request's.
- v2: document that ambient trace_id distribution assumes serialized LLM
  requests per agent, naming the two supported paths that break it
  (after-step compaction below the block ratio; inject-path turns during
  a manual compaction).

* fix: isolate telemetry trace attribution

* fix: isolate telemetry trace by request

* fix: declare @moonshot-ai/protocol dependency of agent-core-v2

The trace-by-request refactor imports types from @moonshot-ai/protocol
in toolContract and toolExecutorService, but the package was not
declared, failing clean CI installs (TS2307). Also drop three type
imports that are unused after the merge with main.
2026-07-16 09:14:12 +08:00
Kai
1d7c205e83
fix: close two silent-exit vectors around unhandled rejections (#1758)
Some checks are pending
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 / test (4) (push) Waiting to run
CI / test (5) (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
CI / typecheck (push) Waiting to run
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix: record crash telemetry for unhandled promise rejections

The crash handler only listened on uncaughtExceptionMonitor, which never
fires for a rejection that has a listener — and the TUI always registers
one, converting rejections into a silent exit(1) with no telemetry at
all. Observe unhandledRejection directly so those crashes still leave a
trace. Since registering a real listener suppresses Node's default
crash-on-rejection, rethrow when we are the only listener (print /
server modes), deduped so the monitor does not double-report.

* fix: fall back to text paste when the clipboard image handler rejects

The Ctrl+V image-paste dispatch chained off the async handler without a
rejection branch, so any failure inside it became an unhandled
rejection — which the CLI's crash path turns into a silent exit(1).
Treat a rejection like "no image available" and paste as text.

* fix: dedupe all rethrown rejection reasons at the crash monitor

Non-Error rejection reasons (plain objects, strings, null) were recorded
by the rejection handler but not added to the dedupe set, so the
uncaughtExceptionMonitor pass after the rethrow reported a second crash
for the same failure; null/undefined reasons also crashed the monitor's
own error-type extraction. Use a Set so primitives dedupe by value, add
every rethrown reason, and classify monitor crashes null-safely.
2026-07-16 02:14:14 +08:00
Kai
918c1354d9
fix: align Anthropic-compatible model capabilities (#1746)
* fix: align Anthropic-compatible model capabilities

* fix: warn on Anthropic effort mismatches

* fix: harden Anthropic model resolution

* fix: align Anthropic replay and ACP thinking state

* fix: normalize Anthropic thinking stream payloads

* fix: backfill non-empty preserved thinking

* fix: resolve session thinking effort with provider context

* fix: honor adaptive thinking opt-out in effort resolution
2026-07-16 01:31:38 +08:00
Kai
f0c8a103c6
fix: preserve the crash error in diagnostic logs on unexpected exit (#1757)
The TUI crash path (uncaughtException / unhandledRejection) logged the
error into an asynchronously-drained sink and then called process.exit()
on the same tick, so the one line explaining the crash was never written
to disk. Flush the diagnostic logs synchronously before exiting.
2026-07-16 01:26:27 +08:00
_Kerman
df75a0f5c2
refactor(agent-core-v2): derive session busy from agent activity (#1751) 2026-07-15 23:33:58 +08:00
qer
e885aec7ff
feat(web): show detailed diagnostics for model request failures (#1756)
Surface the coded provider error the daemon already sends: a semantic
title per error code, the provider's raw message, and expandable
diagnostics (error code, HTTP status, request ID, SDK error name) with
copy support, instead of a bare text-only toast.
2026-07-15 23:07:38 +08:00
qer
1186686554
fix(web): dedupe background subagent rows in the agents dock (#1754)
* fix(web): dedupe background subagent rows in the agents dock

* fix(web): seed agent identity on late task registration and prefer REST output in task fold

* fix(web): backfill terminal output to folded background subagent rows

* fix(web): sync subagent phase when the REST fold makes a row terminal
2026-07-15 23:07:23 +08:00
Haozhe
d8ddabb605
fix(kap-server): close auth bypass via percent-encoded API paths (#1753)
* fix(kap-server): fail closed on percent-encoded API paths in auth bypass

The auth hook checked the raw, still percent-encoded request URL against
its bypass policy, while find-my-way matches routes against the decoded
path. A raw /%61pi/v1/... URL therefore skipped the bearer-token check
yet still reached the /api/... handlers, allowing unauthenticated access
to every API route.

Decode the request path the same way the router does before matching,
and fail closed when the path cannot be decoded. Add tests covering
encoded /api/ paths, encoded meta documents, the encoded healthz probe,
and unaffected non-API bypasses.

* fix(agent-core-v2): make session fs path confinement symlink-aware

SessionFsService confined paths only lexically, so a symlink planted
inside the workspace could steer read, list, stat, mkdir, resolvePath
and resolveDownload to host files outside the session directory.

- add IHostFileSystem.realpath (Node fs.realpath semantics) and
  implement it in the node-local HostFileSystem
- resolveWithin now re-verifies each candidate through realpath of the
  longest existing prefix (so not-yet-created paths still work) and
  rejects paths escaping the canonicalized workspace roots, which are
  resolved once and cached
- cover symlink escapes for every fs entry point plus kap-server
  read/download e2e; in-workspace symlink targets remain allowed

* fix(agent-core-v2): accept workspace roots given through a symlink

WorkspaceRegistryService.createOrTouch probes the root with the
lstat-based IHostFileSystem.stat, so a root given as a symlink to a
directory reported isDirectory=false and session creation was rejected
with fs.path_not_found ("not a directory").

Re-check a non-directory root through IHostFileSystem.realpath before
failing, while keeping the workspace identity lexical (v1 deliberately
never realpaths the root either). Covered by a unit test for a
symlink-form root and a kap-server e2e that creates a session with a
symlinked cwd and reads a file through it.
2026-07-15 23:05:19 +08:00
_Kerman
4f99114342
refactor(kap-server): drop the @moonshot-ai/protocol dependency (#1755) 2026-07-15 22:40:05 +08:00
Haozhe
0b790cdc05
feat(web): allow attaching any file type and fix CSP on non-loopback binds (#1731)
* feat(web): allow attaching any file type in chat

- Composer, paste, and drop no longer filter out non-media files;
  arbitrary files upload as generic icon chips and are submitted as
  file content parts
- kap-server materializes file parts into the session's attachments
  dir and replaces them with a path reference, so the model opens the
  file with the Read tool on demand instead of receiving inline bytes
- Images rejected by the provider format gate (SVG, AVIF, ...) are now
  persisted and referenced by path instead of being dropped with a
  notice; uploaded file names are sanitized before hitting disk

* fix(server): stop CSP from blocking web bootstrap script and fonts

- Move the anti-FOUC bootstrap from an inline <script> in index.html
  to /boot.js: CSP 'self' never covers inline scripts, while a classic
  same-origin script keeps the same render-blocking timing
- Allow data: in font-src — KaTeX and the Inter / JetBrains Mono
  Variable fonts ship @font-face data URIs in their distributed CSS
- Set explicit form-action, base-uri, and frame-ancestors, which do
  not fall back to default-src
- Add a regression test asserting the served index.html carries no
  inline scripts or inline event handlers

* fix(web): normalize empty attachment MIME so extensionless files submit

Files with an empty File.type (Makefile, LICENSE, other extensionless
or unknown types) stored mediaType: '' on the chip, and the submit
fallback used ?? which does not catch empty strings — the wire schema
requires a non-empty media_type, so the prompt was rejected. Normalize
to application/octet-stream at attachment creation, adopt the
server-recorded MIME after upload completes, and make both submit
mappings use || so reloaded chips with '' are covered too.

* feat(web): render all user-turn attachments as chips

* feat(web): attach files by dropping them anywhere in the window

* refactor(web): share one attachment chip between composer and chat bubble

* fix(web): neutral attachment chips, paperclip attach icon, and clickable file chips

* fix(web): drop the extension badge from attachment chips

* fix(web): use the tabler paperclip for the attach button

* fix(web): whitelist attachment previews, reject active document types

Clicking a file chip navigated a new tab to a blob: URL of the uploaded
bytes whenever the type looked browser-renderable. blob: inherits the
web origin, so a text/html or image/svg+xml attachment would execute
same-origin script with the daemon credential (localStorage) and a live
window.opener.

Preview is now restricted to inert types (pdf, non-SVG images, video,
audio, non-HTML text), the blob is re-wrapped with the whitelisted MIME
instead of trusting the recorded content-type, and window.opener is
severed. Non-whitelisted types no longer silently download: the chip
reports 'unsupported' and the pane shows a transient hint.

* fix(web): recover file attachment chips from the server notice

The kap-server prompt route replaces file parts with an "Attached file
…" text notice before enqueueing, so after any snapshot resync the
attachment chip degraded into raw notice text leaking the absolute
server path — unlike image/video uploads, which already recover their
chip from the <video|image path> tag. Parse the notice the same way:
the materialized basename carries the file id, so the chip becomes
clickable again (and editable back into the composer); inline-base64
notices (content-hash named, no file id) still collapse into a
non-clickable chip instead of raw text.

The notice wording is now a client/server contract — flagged on
buildAttachedFileNotice.

* fix(web): preserve UUID file ids when rebuilding attachment chips

* fix(web): skip unresendable file chips when loading attachments for edit

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-15 21:54:48 +08:00
qer
b89d385fa5
fix(web): confirm dialogs respond to Enter and await async actions (#1744)
Some checks are pending
CI / test (3) (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 (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 / Publish native release assets (push) Blocked by required conditions
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
* fix(web): confirm dialogs respond to Enter and await async actions

The confirm dialog's initial focus was resolved from the Button
component's $el, which is a text node in dev builds (the component has
a template-root comment, so it renders as a fragment). Focus fell back
to the header close button, so Enter cancelled instead of confirming.
Resolve the initial focus with a CSS selector on the confirm button
instead.

ConfirmOptions now accepts an async action: the dialog stays open with
a loading state (cancel/Esc/overlay suppressed) until the work settles.
The archive-session, remove-workspace, and delete-provider confirms
move from the menu components into App.vue so the dialog can await the
actual client call.

* fix(web): block superseding a confirm dialog while its action runs

A second confirm() during an in-flight action would replace the busy
dialog and inherit the global busy state, opening inert until the first
action settled. Resolve the new request unconfirmed instead.
2026-07-15 20:04:50 +08:00
liruifengv
f27c3cd02f
docs(changelog): sync 0.24.2 from apps/kimi-code/CHANGELOG.md (#1748) 2026-07-15 18:53:11 +08:00
github-actions[bot]
6a8e610ca3
ci: release packages (#1702)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-15 18:00:18 +08:00
liruifengv
fcb63ebdc2
chore: downgrade unreleased changesets to patch (#1747) 2026-07-15 17:57:36 +08:00
qer
72f425e18d
fix(agent-core-v2): make Gemini tool call ids unique across turns (#1730)
* fix(agent-core-v2): make Gemini tool call ids unique across turns

* fix(kosong): recover tool names from entropy-suffixed Gemini call ids

The orphan tool-message fallback in toolCallIdToName stripped only one
trailing "_<suffix>" segment, so with the new
"{tool_name}_{upstream_id}_{entropy}" id shape it returned
"AgentSwarm_0" instead of "AgentSwarm" once the preceding assistant
call had been compacted away. Strip the fixed 8-hex entropy suffix
first, then the upstream id segment, in both the kosong and
agent-core-v2 providers. Also drop the inline implementation comment
that agent-core-v2 forbids outside the file header.

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-07-15 17:47:53 +08:00
Haozhe
1824e73020
style(agent-core-v2): fold retry-budget note into the file header (#1742)
packages/agent-core-v2/AGENTS.md requires comments to live only in the
top-of-file block, never beside statements.
2026-07-15 17:10:04 +08:00
_Kerman
27236bd75f
refactor(agent-core-v2): drop the @moonshot-ai/protocol dependency (#1745) 2026-07-15 16:03:19 +08:00
Luyu Cheng
481b28b8f4
fix(agent-core-v2): correct goal lifecycle error guidance (#1743)
* fix(agent-core-v2): correct goal lifecycle error guidance

* refactor(agent-core-v2): remove unused goal service API
2026-07-15 15:42:39 +08:00
Luyu Cheng
8a3f1ffa6f
fix(kap-server): derive session title from first skill slash command (#1741) 2026-07-15 15:42:25 +08:00
Haozhe
a160915596
test(klient): add real-server smoke coverage (#1713)
Add transport and persisted-session smoke scripts for HTTP, WebSocket, and scoped service calls.
Type-check all examples and document remote-server usage and side effects.
2026-07-15 15:24:36 +08:00
Haozhe
a74ab44ac7
feat: raise default per-step LLM retry budget to 10 attempts (#1740)
* feat: raise default per-step LLM retry budget to 10 attempts

The default of 3 attempts only produced ~1.5s of backoff (0.5s/1s), so
sustained provider overload (429) surfaced to the user almost
immediately. With 10 attempts the exponential ramp (500ms base, x2,
32s cap, 25% jitter) waits out multi-minute overload windows before
failing the turn. Applies to both agent-core (chatWithRetry) and
agent-core-v2 (stepRetry service); loop_control.max_retries_per_step
still overrides.

* test(agent-core): pin retry-dependent tests to the new default budget

- error-paths: the warn-log attempt field now reads 1/10 with the
  default 10-attempt budget
- goal-session pause tests: every LLM call throws a retryable error, so
  the default 10-attempt backoff (~2min) blew the 5s test timeout; pin
  maxRetriesPerStep=1 since the pause behavior does not depend on the
  retry count
2026-07-15 15:24:17 +08:00
qer
6eb8e13417
fix(kimi-web): improve mobile safe-area handling (#1459)
* fix(kimi-web): improve mobile safe-area handling

* fix(kimi-web): restore dock-height fallback where ChatDock is absent

* fix(kimi-web): pin the app shell to the visual viewport height

* fix(kimi-web): pin the app shell to the visual viewport

* chore: add changeset for mobile safe-area fixes
2026-07-15 14:50:01 +08:00
Haozhe
5d6ff022b1
feat(agent-core): drop default timeouts for print-mode background tasks (#1737)
* feat(agent-core): drop default timeouts for print-mode background tasks

- add `bash_task_timeout_s` config under [background] (0 = no timeout),
  covering the background Bash default and the re-arm after a foreground
  command is moved to the background on timeout
- allow `[subagent] timeout_ms = 0` (no timeout) and thread it through
  Agent/AgentSwarm task registration and the swarm batch timer
- fill both with 0 in print-mode config defaults so `kimi -p` never kills
  background work by wall-clock and only the model stops a task;
  interactive defaults are unchanged
- sync the Bash tool description/parameter text with the effective
  default and update user docs (en/zh) plus changeset

* test(agent-core): satisfy KimiConfig providers requirement in print-defaults tests

* fix(agent-core): clear the foreground deadline on detach when the background timeout is disabled

`detach()` only re-arms when `detachTimeoutMs` is defined, so passing
`undefined` with `bash_task_timeout_s = 0` kept the armed foreground
deadline and a manually detached command was still killed at its
foreground timeout. Pass `0` instead so the reset clears the timer.
Caught by Codex review on #1737.
2026-07-15 14:35:32 +08:00
Luyu Cheng
513f374aa0
fix(agent-core-v2): validate goal records (#1694) 2026-07-15 14:19:03 +08:00
qer
b6ae0a1054
fix(web): surface session list load failures (#1641)
* fix(web): surface session list load failures

* fix(web): preserve partial session pages
2026-07-15 13:58:03 +08:00
qer
d8d4e8ceb5
fix(web): keep long streams responsive (#1643)
* fix(web): keep long streams responsive

* fix(web): drop queued events for archived sessions
2026-07-15 13:47:11 +08:00
qer
b24a347e20
fix(kap-server): carry the live subagent roster in the session snapshot (#1719)
* fix(kap-server): carry the live subagent roster in the session snapshot

* fix(kap-server): clear the subagent roster on the next main turn start

* fix(kap-server): exclude background subagents from the snapshot roster

* fix(kap-server): finalize live roster entries when the main turn aborts

* fix(kap-server): drop roster entries when foreground subagents detach

* fix(web): expand the swarm card by default while subagents are running

* docs(kap-server): qualify the roster-clearing durability claim
2026-07-15 12:40:30 +08:00
_Kerman
2591395158
refactor(agent-core-v2): remove dead ExecutableToolResult.message field (#1729) 2026-07-15 12:32:25 +08:00
7Sageer
c291ee2ddb
fix(telemetry): deliver session_started in v2 headless mode (#1687)
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 / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (1) (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (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
* fix(telemetry): deliver session_started in v2 headless mode

session_started/session_load_failed fire inside create()/resume(), but the CloudAppender was installed only after resolveNativeSession() returned, so they were dropped to the null appender.

- run-v2-print: install the appender before resolving the session; reconcile a resumed session's real model via setContext afterwards.
- sessionLifecycle: bind the session id in announceCreated before emitting so session_started carries session_id.
- CloudAppender.setContext: allow updating the model context.

* fix(agent-core-v2): bind session id on session_load_failed

The resume failure path emitted session_load_failed without binding the
session id first, so the event went out with session_id null even though
the service knows which session failed to load. Bind it via setContext
before track2, mirroring the announceCreated path for session_started,
and update the two tests that locked the empty context in as expected.
2026-07-15 11:52:01 +08:00
liruifengv
286d3e7aca
feat: add check-kimi-code-docs builtin skill (#1727)
* feat(agent-core): add check-kimi-code-docs builtin skill

* feat(agent-core-v2): add check-kimi-code-docs builtin skill

* docs: list check-kimi-code-docs in builtin skill commands

* chore: add changeset for check-kimi-code-docs skill
2026-07-15 11:42:47 +08:00
Haozhe
3703d0346e
feat(agent-core): default print mode to steer with unbounded limits (#1722)
- change the default print background mode from 'exit' to 'steer' so
  `kimi -p` stays alive while background tasks are pending and each
  completion steers a new main turn
- add print-mode config defaults (applyPrintModeConfigDefaults):
  effectively unbounded wait ceiling and turn cap, 72h subagent
  timeout; explicit user config always wins
- thread the new `uiMode` option from SDKRpcClient through KimiCore
  into session create/resume
- clamp setTimeout delays to 0x7fffffff so huge timeouts are not
  clamped to 1ms by Node
2026-07-15 11:11:00 +08:00
liruifengv
3770447e57
fix(agent-core-v2): run stdio node plugins with the bundled Electron Node on Electron hosts (#1723) 2026-07-15 11:00:45 +08:00
Luyu Cheng
7de218a909
fix(kimi-web): resume paused goals (#1693)
Some checks are pending
Release / Release (push) Waiting to run
CI / typecheck (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 / test (2) (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 / Publish native release assets (push) Blocked by required conditions
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
* fix(kimi-web): resume paused goals

* fix(agent-core-v2): defer paused goal continuation
2026-07-15 00:00:29 +08:00
Luyu Cheng
2bf009fe27
fix(agent-core-v2): reject subagent goals (#1697)
* fix(agent-core-v2): reject subagent goals

* fix(kap-server): guard reflected subagent goals
2026-07-14 23:30:30 +08:00
qer
de493aeec9
fix(web): use upward chevron for dock card expand buttons (#1715)
* fix(web): use upward chevron for plan card expand button

* fix(web): use upward chevron for question card expand button
2026-07-14 23:06:15 +08:00
Luyu Cheng
722694adf9
fix(agent-core-v2): enforce goal deadlines (#1698) 2026-07-14 23:00:05 +08:00
qer
20b69724aa
fix(web): make code block copy work over plain HTTP (#1714) 2026-07-14 22:31:36 +08:00
Luyu Cheng
5c0f17cfcf
fix(agent-core-v2): persist active goal time (#1695)
* fix(agent-core-v2): persist active goal time

* fix(agent-core-v2): migrate active goal anchors

* fix(agent-core-v2): migrate envelope-less goal logs

* fix(agent-core-v2): advance migrated goal checkpoints

* fix(agent-core-v2): align crash recovery with wire domain
2026-07-14 22:29:36 +08:00
Luyu Cheng
e53cd79957
fix(agent-core-v2): align goal turn budgets (#1692)
* fix(agent-core-v2): align goal turn budgets

* fix(agent-core-v2): preserve blocked goal outcomes

* fix(agent-core-v2): stop resumed goals at turn budget
2026-07-14 22:11:28 +08:00
Haozhe
78ca44b4d1
feat(kimi-code): keep print-mode goal runs alive until the goal settles (#1712)
* feat(kimi-code): keep print-mode goal runs alive until the goal settles

- applyPrintBackgroundPolicy waits for goal continuation turns via a goalActive hook before applying the exit/drain/steer mode, bounded by the wait ceiling
- createPrintTurnEndings skips the timeout when the remaining budget is not finite
- update the changeset to cover the goal-run lifecycle

* fix(kimi-code): wake the print goal wait periodically so settled goals exit promptly
2026-07-14 21:09:09 +08:00
Luyu Cheng
b781e8cbcf
fix(agent-core-v2): preserve goal continuations (#1696)
* fix(agent-core-v2): preserve goal continuations

* fix(agent-core-v2): cancel preserved continuation turns
2026-07-14 20:46:18 +08:00
Luyu Cheng
3107f963a5
fix(agent-core-v2): isolate replaced goals (#1700)
* fix(agent-core-v2): isolate replaced goals

* fix(agent-core-v2): tighten stale goal guards

* fix(agent-core-v2): preserve replacement goal budgets

* fix(agent-core-v2): continue replacement goals

* fix(agent-core-v2): adopt resumed goal turns

* fix(agent-core-v2): guard stale goal budgets

* fix(agent-core-v2): keep stale outcomes nonterminal

* fix(agent-core-v2): preserve same-batch goal outcomes

* fix(agent-core-v2): gate stale replacement outcomes

* fix(agent-core-v2): preserve replacement goal mutations

* fix(agent-core-v2): revalidate delayed goal creation
2026-07-14 20:32:05 +08:00
_Kerman
26d499bca7
refactor(agent-core-v2): consolidate wire services (#1680) 2026-07-14 19:51:29 +08:00
qer
9eff230f97
fix(web): show errors for failed actions and add daemon request/operation logging (#1711)
Some checks are pending
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / build (push) Waiting to run
CI / test (1) (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
CI / typecheck (push) Waiting to run
CI / lint (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
* fix(web): show errors for failed actions and add daemon request/operation logging

* fix(web): dedupe identical operation-failure toasts to avoid flooding

* Revert "fix(web): dedupe identical operation-failure toasts to avoid flooding"

This reverts commit 54fcfdcc97a034d7236d61cbb3664292a8966c64.
2026-07-14 19:17:28 +08:00
qer
ddfdfb0b09
fix(server): synthesize session status only from the main agent's turn (#1708)
* fix(server): synthesize session status only from the main agent's turn

A sub-agent runs its own turn on the shared session channel, and the
broadcaster synthesized event.session.status_changed from every agent's
turn.started/turn.ended. A foreground sub-agent finishing mid-turn thus
emitted a bogus idle transition that clients read as 'turn finished'
(notifications, sounds, unread dots, queued-message drain), while the
real main-agent turn end was swallowed by dedup.

Gate the status synthesis on MAIN_AGENT_ID, matching the existing
main-only rule in InFlightTurnTracker.

* fix(server): emit idle when a sub-agent turn ends as the last active agent

Defensive follow-up to the main-only status synthesis: ensureState seeds
lastStatus from ISessionActivity, which counts any agent's turn, so a
session first activated while only a sub-agent was active would stay
running for subscribers until the next main turn. A sub-agent's own
lease and loop state are already released when its turn.ended is
published, so a session-wide idle activity read cannot be a
mid-main-turn foreground sub-agent — emit idle in that case only.
Dedup keeps it a no-op everywhere else.

* revert: drop the defensive idle emit for sub-agent-only turn endings

This reverts 7918b45fe. The stale-seed scenario it guarded is
effectively unreachable in production: sessions are activated at
creation / first prompt and stay active for the process lifetime, and
persisted detached tasks are reconciled as lost on restart, never
resumed. The guard also raised a new behavioral question of its own
(failed/cancelled sub-agent endings would emit a success-style idle).
Keep the PR to the minimal main-only status synthesis; the theoretical
corner self-corrects on the next REST status pull or main turn.
2026-07-14 18:35:49 +08:00
Haozhe
38a2363a00
fix: cross-version session compatibility and real message times in snapshot history (#1704)
* fix(server): report CLI version as server_version

- kap-server: accept opts.version in startServer, reported as
  server_version (/meta, OpenAPI, session exports, lock registry,
  default User-Agent); defaults to its own package version
- kimi-code: pass the CLI product version when starting the server
- add boot test covering /api/v1/meta, lock file, and User-Agent

* fix(agent-core-v2): make new sessions resumable by older v1 builds

- add wireRecord.seal() to write the metadata envelope at agent creation,
  so fresh logs satisfy v1 replay's first-record-must-be-metadata invariant
- seal the wire log before any op dispatch in AgentLifecycleService.create;
  no-op when the log already has records (resume / forked copies)
- seed empty agents/custom maps in session metadata so v1 Session.resume()
  can index agents['main'] on a v2-created state.json
- add seal unit tests and lifecycle sealing tests

* fix(kap-server): show real message send times in snapshot history

- read per-record times stamped on wire.jsonl via reduceContextTranscript
  and cache them alongside the transcript messages
- prefer each record's real dispatch time for created_at; records without a
  stamp fall back to session.createdAt + index, clamped so the page stays
  strictly increasing (mirrors MessageLegacyService.list)
- add tests for fallback, clamping, and the page-offset index mapping

* fix(agent-core-v2): backfill missing agents/custom maps in existing session metadata

- load() now heals pre-fix v2 state.json documents that never gained the
  agents/custom maps, persisting the backfill so one open on a new build
  leaves the session resumable by released v1 builds (Session.resume()
  indexes agents['main'] unconditionally)
- updatedAt is deliberately untouched so the format heal does not reorder
  session listings

* feat(agent-core-v2): make subagent timeout configurable, default 2h

- add [subagent] config section with timeout_ms and KIMI_SUBAGENT_TIMEOUT_MS env override
- resolve Agent and AgentSwarm per-run timeouts through resolveSubagentTimeoutMs
- update tool descriptions and tests to reflect the 2-hour default

* feat(agent-core-v2): align print-mode background policy with v1

- add printBackgroundMode/printMaxTurns to the task config section with resolvePrintBackgroundMode (keepAliveOnExit fallback)
- apply exit/drain/steer policy to kimi -p on the experimental engine, buffering turn.ended events and failing the run when a steered turn fails
- register the subagent config section from the package entry

* fix(agent-core-v2): strict equality in metadata heal, comments in file headers only

- replace == null with === undefined in the session-metadata heal to
  satisfy eqeqeq (oxlint --type-aware in CI)
- move the seal() rationale into the wireRecord contract header and the
  agents/custom invariant into the sessionMetadata header per the
  tree's header-only comment convention
2026-07-14 18:35:02 +08:00
7Sageer
ac216163b9
fix: emit turn_id on turn_started/ended/interrupted telemetry (#1668)
* fix: emit turn_id on turn_started/ended/interrupted telemetry

The turn lifecycle telemetry events (turn_started, turn_ended,
turn_interrupted) never carried the turn id, while tool_call and
tool_call_dedup_detected did. Any analysis correlating a turn's start,
end, or interruption back to its tool calls had nothing to join on.

Add turn_id (already in scope) to all three track() calls, matching the
existing key/value convention used by tool_call_dedup_detected.

Update the strict turn_started/turn_interrupted assertion to cover it.

* fix(agent-core-v2): emit turn_id on turn_started/ended/interrupted telemetry

Port the v1 fix to agent-core-v2: turn lifecycle telemetry events
(turn_started, turn_ended, turn_interrupted) carried no turn id while
tool_call did, leaving nothing to correlate a turn's start, end, or
interruption back to its tool calls.

Add turn_id to the three event interfaces, the telemetry registry
property docs, and the three track2() calls in AgentLoopService,
matching the existing ToolCallEvent key convention. Extend the
turn telemetry assertions in loop.test.ts to cover it.

* fix: emit turn_id on tool_call telemetry

* docs(agent-core-v2): clarify turn_id is a per-agent index in the telemetry registry
2026-07-14 18:31:43 +08:00
Haozhe
8490c3e36b
feat(agent-core-v2): record compaction droppedCount in wire traces and rename select_tools capability (#1707)
* feat(agent-core-v2): record droppedCount on full-compaction llm.request wire ops

- add per-attempt droppedCount to the llm.request source logFields so
  replays can see how much history each retry round blinded
- cover compaction/loop request events in the full-compaction test

* refactor(agent-core-v2): rename select_tools capability to dynamically_loaded_tools

- rename the ModelCapability bit and catalog field across capability.ts,
  catalog.ts, modelResolverService and the toolSelect gate
- update toolSelect flag description wording to match
- update all affected tests and the test harness capabilityNames helper

* chore: add changesets for v2 wire trace and capability rename
2026-07-14 18:01:11 +08:00
Haozhe
9e6d53b025
fix(workspace): re-read the catalog on every v2 registry operation (#1706)
- drop the in-memory write cache in the v2 registry: every list/get/
  mutation is a fresh read-modify-write of workspaces.json under the op
  mutex, so v1 writers (touchWorkspaceRegistry) are never clobbered by a
  stale snapshot; the session-index merge stays once per process behind
  a flag (Codex P1)
- move the shared workspaces.json helper from services/workspace (the
  upper facade) to session/store, so the rpc runtime no longer imports
  back into services/ (Codex P2)
2026-07-14 17:52:03 +08:00
liruifengv
6b0a116326
docs(changelog): sync 0.24.1 from apps/kimi-code/CHANGELOG.md (#1703) 2026-07-14 17:47:24 +08:00
Haozhe
07c3632415
fix(workspace): sync workspace catalog across v1/v2 and soft-delete (#1701)
- register the cwd in workspaces.json when a v1 (TUI/SDK) session is
  created, via a shared workspaceRegistryFile read/write/touch module
- merge session_index.jsonl into the catalog once at v2 startup
  (awaited during kap-server boot), adding only paths absent from
  workspaces.json
- make v2 workspace delete a soft delete through the v1-compatible
  deleted_workspace_ids field: the session-index merge never resurrects
  tombstoned ids, while an explicit createOrTouch clears the tombstone
2026-07-14 17:34:35 +08:00
github-actions[bot]
1d2249bfa4
ci: release packages (#1682)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-14 17:03:41 +08:00
qer
ab22a2adf0
fix(web): show just the thinking level name in the model pill (#1689)
Drop the "thinking:" / "思考:" prefix from the effort suffix and
capitalize the level via effortLabel, matching the segment labels.
2026-07-14 17:00:09 +08:00
qer
ed420c99fd
test(agent-core-v2): add lifecycle create stub to sessionLegacy status test (#1691)
SessionLegacyService.status() now resolves the main agent through
ensureMainAgent() -> IAgentLifecycleService.create() (since d158e0a7a),
but the scenario's hand-rolled lifecycle stub only implemented
whenReady, so the test errored with 'create is not a function' before
any assertion ran. Return the existing main agent from create, matching
the real service's create-or-get contract for explicit ids.
2026-07-14 16:56:11 +08:00
Haozhe
94c0ef89d2
fix(agent-core): re-register builtin tools when provider resolves late (#1688)
When the model provider becomes resolvable only after the agent starts
(async OAuth / managed free-tokens registration), the builtin tool table
stayed empty: initializeBuiltinTools was gated on hasProvider at fixed
checkpoints and none fired. loopTools now self-heals on read once a
provider resolves, so tool calls no longer fail with "Tool not found".
2026-07-14 16:37:17 +08:00
Kai
d158e0a7ac
fix: resolve and synchronize thinking effort (#1625)
* fix: preserve provider thinking effort values

* fix: resolve Kimi thinking effort fallbacks

* fix: use resolved Kimi effort in v2 requests

* fix: align Kimi effort resolution paths

* fix: synchronize forced Kimi effort state

* fix: synchronize forced Kimi effort in v2 state

* fix: tolerate unresolved models in v2 status
2026-07-14 16:37:03 +08:00
liruifengv
268fd41734
docs(changelog): sync 0.24.0 from apps/kimi-code/CHANGELOG.md (#1683)
* docs(changelog): sync 0.24.0 from apps/kimi-code/CHANGELOG.md

* chore: update changelog

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-14 16:23:07 +08:00
Kai
e417ee7c2c
fix(kosong): restore Kimi preserved-thinking sessions (#1684)
* fix: preserve empty reasoning across providers

* fix(kosong): restore Kimi preserved-thinking sessions
2026-07-14 16:17:48 +08:00
Luyu Cheng
ec1c9748c8
fix(agent-core-v2): preserve goal completion summaries and error messages (#1678)
* fix(agent-core-v2): preserve goal completion summaries

* fix(agent-core-v2): preserve loop error messages

* fix(agent-core-v2): keep goal cancellation responsive
2026-07-14 15:13:47 +08:00
qer
0f64b4dcc4
fix(web): submit thinking level verbatim and drop the hardcoded default (#1673)
* fix(web): submit thinking level verbatim and drop the hardcoded default

Align kimi-web's thinking-level handling with the TUI:

- Submit the stored level as-is on every prompt path (prompt, steer,
  skill activation, BTW side chat) instead of coercing it onto the
  target model's declared efforts.
- No stored preference (undefined) instead of a hardcoded 'high'
  default: prompts omit the thinking override and the daemon resolves
  the config/model default, same as an unset [thinking] in the TUI.
- Model switcher pre-selects the target model's own default level when
  switching models; re-selecting the current model keeps the level.
- Display the effective level (stored value, else the model default)
  in the composer, mobile sheet, and /status panel.

* chore(web): remove the dead dev:stub script

The stub daemon (dev/stub-daemon.mjs) no longer exists, so the
dev:stub npm script and its docs references were dead weight.

* fix(web): pin the model default thinking level and persist picks globally

- With no stored preference, loadModels() pins the active model's
  catalog default_effort as a concrete in-memory value, so what the
  UI shows, what prompts submit, and what the session runs always
  agree. localStorage stays reserved for levels the user picked.
- setThinking and model switches now also write the daemon-wide
  [thinking] config (same mapping as the TUI's thinkingEffortToConfig),
  so sessions created by other clients inherit the pick.
2026-07-14 15:04:28 +08:00
github-actions[bot]
2383137f3e
ci: release packages (#1583)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-14 15:01:30 +08:00
Haozhe
003e583d86
fix(agent-core-v2): project support_efforts and default_effort in the model catalog (#1677)
Align toProtocolModel with the v1 projection and the protocol wire schema by applying effectiveModelConfig and emitting support_efforts / default_effort, so clients of the v2 /models endpoint see thinking-effort levels again.
2026-07-14 14:53:50 +08:00
Haozhe
206c89574f
test(kap-server): use agent lifecycle get in blocked goal rig (#1679)
Replace the nonexistent getHandle call introduced by the blocked-goal continuation tests with the public get API and the shared MAIN_AGENT_ID constant, restoring typecheck and both continuation tests.
2026-07-14 14:47:51 +08:00
Kai
d1820ff0f8
fix: preserve empty reasoning across providers (#1676) 2026-07-14 14:30:56 +08:00
Chen, Yicun
88629bac3a
fix(web): allow image sources in CSP (#1672)
Co-authored-by: chenyicun <chenyicun@msh.team>
2026-07-14 14:15:22 +08:00
_Kerman
3215129860
refactor(agent-core-v2): split agent lifecycle into existence, subagent, and session MCP domains (#1624) 2026-07-14 14:13:41 +08:00
Luyu Cheng
28e9dd4d62
fix: continue agent after resuming blocked goals (#1627)
* fix(agent-core-v2): serialize agent startup

* style(agent-core-v2): align startup comments

* fix(agent-core-v2): resume blocked goals

* fix(agent-core-v2): continue blocked Web goals

* fix(agent-core-v2): keep exhausted goals blocked on resume

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-14 13:59:12 +08:00
Luyu Cheng
490303db16
fix(web): refine goal mode controls (#1669)
* fix(web): refine goal mode controls

* fix(web): hide goal progress without budget

* fix(web): use design system for goal cancellation

* docs: document web goal controls

* fix(web): remove collapsed goal actions from tab order
2026-07-14 13:34:37 +08:00
liruifengv
d780b2334d
feat(agent-core-v2): make /init runs cancellable (#1667)
The init run spawns a coder subagent through agentLifecycle.run with an
AbortController that was a local of generateAgentsMd, so nothing outside
the service could cancel it: Ctrl+C in clients only cancels the main
agent's loop turn, which does not exist during /init.

ISessionInitService gains cancelInit(): the service keeps the in-flight
run's AbortController (cleared in finally), and cancelInit aborts it with
a user-cancellation reason. Cancellations propagate unwrapped from
generateAgentsMd (UserCancellationError / AbortError, never
SESSION_INIT_FAILED, no subagent.failed event) so callers can distinguish
"aborted" from "failed".
2026-07-14 13:31:45 +08:00
qer
5eb62178b3
feat(web): add session diagnostic export (#1646)
* feat(web): add session diagnostic export

* fix(web): bound session export resources

* fix(web): make session export atomic

* feat(web): add session export entry to session menus with item icons
2026-07-14 12:48:28 +08:00
Kai
32a89c3643
fix(agent-core): prevent oversized media from poisoning sessions (#1657) 2026-07-14 12:46:45 +08:00
_Kerman
f02f7241af
refactor(agent-core-v2): default all DI services to eager instantiation (#1666) 2026-07-14 12:37:57 +08:00
7Sageer
1294a0e1ad
fix(agent-core-v2): keep invalidated OAuth flows observable during login (#1663)
* fix(agent-core-v2): keep invalidated OAuth flows observable during login

invalidateFlows() deleted flows outright when the provider configuration
changed, including terminal ones. That voided the TERMINAL_RETENTION_MS
window and, worse, left the status guards in handleSuccess /
finalizeAuthentication blind when the invalidation landed in the
microtask gap after toolkit.login resolved: setTerminal then wrote
'authenticated' onto an orphaned state the client could never observe,
so the login hung after a successful browser authorization.

Transition affected pending flows to a visible 'cancelled' terminal
status instead, mirroring abortExisting().

Also normalize the KIMI_CODE_BASE_URL env override at the source in
kimiCodeBaseUrl(): provision persists it verbatim while the model
refresh rewrites it normalized, and the deep-equal diff between the two
shapes fired a spurious providers-changed event mid-login.

* style(agent-core-v2): drop non-header comments from the oauth login fix

* chore: cover agent-core-v2 in the oauth login fix changeset
2026-07-14 12:16:28 +08:00
_Kerman
e7374d015c
test(agent-core-v2): deflake full compaction retry-count tests (#1665) 2026-07-14 12:01:19 +08:00
liruifengv
4be5b0c288
feat(server): allow X-Kimi-Client-* headers and reflect request headers in CORS (#1661)
* feat(server): allow X-Kimi-Client-* headers in CORS for cross-origin desktop renderer

* feat(server): reflect request headers in CORS preflight

For allowed origins, echo the preflight's Access-Control-Request-Headers back in Access-Control-Allow-Headers (falling back to CORS_ALLOW_HEADERS for non-preflight responses). Newly added client request headers (e.g. X-Request-Id) no longer require a matching server-side allowlist change, unblocking desktop app://renderer cross-origin requests that were failing preflight on headers absent from the fixed list.
2026-07-14 11:51:43 +08:00
liruifengv
bd5e6bddb4
chore: remove stale changeset for removed server package (#1662)
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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
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
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
2026-07-14 11:36:01 +08:00
Kai
7c889f3a96
fix(agent-core): mark auto-approved plan exits as not user-reviewed (#1638)
* fix(agent-core): mark auto-approved plan exits as not user-reviewed

In auto permission mode ExitPlanMode is approved without user
involvement, but its result read "## Approved Plan:" exactly like a
genuine user approval and the transcript showed a green "Approved"
chip. The model treated that as a signal to start executing, even
when the user had asked it to stop after planning.

- Emit an "auto-approved, not user-reviewed" result with a note that
  execution follows the user's original instructions (v1 + v2).
- Extend the auto-mode reminder: plan approvals are automatic and are
  not a user signal to proceed (v1 + v2).
- Render an "Auto-approved" warning-toned chip in the TUI, keeping
  backward compatibility with the old result marker.
- Document the Auto mode plan-exit behavior in interaction guides.

* fix(agent-core): only mark plan exits as auto-approved in auto permission mode

Address review feedback on the auto-approved plan exit change:

- Branch the direct-execution output on the permission mode in both
  engines: only auto mode yields the "auto-approved, not
  user-reviewed" result and telemetry outcome. In manual / yolo modes
  the direct path means a configured or session allow/ask rule let the
  call through — an explicit user decision that keeps the user-approved
  output, the Approved transcript chip, and the approved outcome.
- Move the v2 rationale out of the method body into the file header,
  per the agent-core-v2 comment convention.
- Drop the absolute "only Auto mode" wording from the interaction
  guides (en/zh).
2026-07-14 11:30:47 +08:00
Haozhe
0e1c51cb0e
style(agent-core-v2): remove non-header comments (#1656)
Enforce the package comment convention that comments live only in the
top-of-file block: strip mid-file doc, prose, and trailing comments
while preserving header blocks and eslint/oxlint/@ts-* suppression
comments. No code tokens are changed.
2026-07-14 10:50:22 +08:00
Haozhe
e49b3b8777
fix(agent-core-v2): harden wire restore and background task lifecycle (#1635)
* fix(agent-core-v2): fix wire migration rewrite race and reject unversioned logs (8 files)

- await the migration rewrite in wireRecordService.restore() so restore only resolves once the migrated log is durable and rewrite failures surface
- serialize AppendLogStore.rewrite() behind the log flush so appends arriving during the whole-file replace land after the new content
- reject wire logs missing the metadata envelope (missingWireMetadataError) in restore, session fork, and agent create instead of fabricating a current-version envelope over legacy records

* fix(agent-core-v2): stop background tasks on session close (10 files)

- add IAgentTaskService.stopAllOnExit: suppress terminal notifications, then SIGTERM → grace → SIGKILL for every active task (v1 stopBackgroundTasksOnExit parity, gated by keepAliveOnExit)
- call stopAllOnExit from agentLifecycle.remove before scope disposal, and abort live tasks in AgentTaskService.dispose as a last resort for disposal paths that bypass the graceful close
- honor [task] killGracePeriodMs in the stop grace window and bind the v1 KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT env override for keepAliveOnExit

* fix(agent-core-v2): root task persistence at the owning agent's scope

- persist task records under <sessionScope>/agents/<agentId>/tasks/ (v1's
  per-agent layout) so tasks written by older versions are found on resume
- stop one agent's restore from loading, marking lost, or re-notifying
  another agent's tasks
- add a per-agent isolation test covering loadFromDisk + reconcile

* fix(agent-core-v2): address task lifecycle review findings

* fix(agent-core-v2): preserve queued appends across rewrites

* test(agent-core-v2): cover rewrite cutover flush

* fix(agent-core-v2): surface append durability failures

* fix(agent-core-v2): retire released append log buffers

* fix(agent-core-v2): preserve session-level task records

* fix(agent-core-v2): harden append log cutover recovery

* fix(agent-core-v2): suppress only background exit notifications

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-14 10:02:24 +08:00
Haozhe
8027fe291b
fix(agent-core-v2): caller MCP servers and skill dirs parity (#1636)
* feat(agent-core-v2): support caller-supplied MCP servers on session create

- add mcpServers to CreateSessionOptions, forwarded to the session's first
  ensureMcpReady call so caller-supplied servers ride on the initial load
- ensureMcpReady accepts callerServers, honored only by the call that starts
  the initial load; later calls just await the in-flight single-flight load
- merge precedence mirrors v1 (rpc/core-impl.ts): file config <
  caller-supplied < plugin servers

* feat(agent-core-v2): file tools reach skill roots; add skillDirs seed

- add extendWorkspaceWithSkillRoots to path-access; Read/Write/Edit/Grep/
  Glob and media tools merge skill-catalog roots into the workspace per
  call (v1 merged once at construction), so skill dirs outside the cwd
  stay reachable and late-loading roots are picked up
- add skillCatalogRuntimeOptionsSeed; wire --skillsDir into the v2 print
  CLI and skillDirs into kap-server startServer (v1 SDK skillDirs parity)

* chore: add changesets for v2 caller MCP servers and skill dirs fixes
2026-07-14 10:02:15 +08:00
Haozhe
0e0a6e9a51
feat(agent-core-v2): support caller-supplied MCP servers on session create (#1637)
* feat(agent-core-v2): support caller-supplied MCP servers on session create

- add mcpServers to CreateSessionOptions, forwarded to the session's first
  ensureMcpReady call so caller-supplied servers ride on the initial load
- ensureMcpReady accepts callerServers, honored only by the call that starts
  the initial load; later calls just await the in-flight single-flight load
- merge precedence mirrors v1 (rpc/core-impl.ts): file config <
  caller-supplied < plugin servers

* chore: add changeset for v2 caller MCP servers
2026-07-14 10:02:07 +08:00
Haozhe
5d1f9049ca
fix(agent-core-v2): repair messages API history after resume (#1633)
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
CI / lint (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
- rehydrate restored `blobref:` media URLs to inline `data:` URIs from the
  agent's blob store, matching the shape of live emissions
- pass tool results carrying media (e.g. ReadMediaFile) through as raw
  content parts instead of flattening them to empty text
- use wire record times for `created_at`, nudged to stay strictly
  increasing, instead of a synthesized session-start offset
- carry per-entry record times through `ContextTranscript` and the
  live-tail merge
2026-07-14 00:05:49 +08:00
Haozhe
2d874fbd73
fix(agent-core-v2): surface provider auth errors and align agent copy with v1 (#1631)
* fix(agent-core-v2): surface provider rejection on post-refresh 401

- map a 401 that survives forced token refresh to PROVIDER_AUTH_ERROR
  carrying the provider's sanitized message, instead of a misleading
  AUTH_LOGIN_REQUIRED re-login prompt
- propagate provider auth errors through full compaction instead of
  wrapping them as compaction failure
- export sanitizeStatusErrorMessage for reuse

* fix(agent-core-v2): align repeat-reminder and AskUserQuestion copy with v1

- rewrite repeated tool call reminders to redirect instead of prohibit (port of v1 #1518)
- simplify makeReminderText2 to (repeatCount), no longer echoing tool name and args
- treat dismissed AskUserQuestion as no answer, not the recommended pick (port of v1 #1550)
- update toolDedupe test assertions and regenerate affected wire snapshots

* chore: add changeset for v2 reminder and question copy alignment

* fix(kap-server): drain in-flight heartbeat writes before unlinking the instance file

- count writes that passed the released check and await them in release()
  so their atomic rename cannot recreate the file after unlink
- harden the heartbeat test: 1ms cadence plus a post-release re-check
  catches the recreate-after-unlink race (verified failing on old code)

* docs(agent-core-v2): move toProviderAuthError rationale to the file header

Address Codex review: agent-core-v2 keeps comments in the top-of-file
header only, never beside functions
2026-07-13 23:52:22 +08:00
Haozhe
96b83281b2
fix(agent-core-v2): make managed OAuth login env-aware (#1634)
Resolve the login environment via resolveKimiCodeLoginAuth so
KIMI_CODE_BASE_URL / KIMI_CODE_OAUTH_HOST steer the credential slot
login writes to the same way they steer runtime token reads — fixes
"login succeeds but every call 401s" against non-default environments.
The provisioned provider entry now records the login environment and
credential slot explicitly, and logout deletes from the runtime
(env-aware) slot.
2026-07-13 23:49:40 +08:00
Haozhe
a4aae87cd9
fix(agent-core-v2): fall back to protocol default base URL when unconfigured (#1632)
- drop the "missing a base URL" rejection in ModelResolverService so a
  structured provider without base_url resolves to undefined and the wire
  provider applies its protocol default endpoint, matching v1
- relax baseUrl to optional across Model, ModelImplInit, and
  ProtocolAdapterConfig; guard the anthropic /v1 strip against undefined
2026-07-13 23:33:44 +08:00
Haozhe
0303b82c3e
fix: align v1 protocol handshake and tool_result media passthrough (#1630)
* fix(protocol): make server_hello heartbeat_ms optional

kap-server dropped the server-initiated WS heartbeat and no longer emits
heartbeat_ms in server_hello, but the published v1 schema still required
it, so spec-compliant clients rejected the handshake before subscribing.

- mark heartbeat_ms optional in serverHelloPayloadSchema (advisory only)
- add a ws-control test for a server_hello without heartbeat_ms
- align kimi-web WireServerHello and the server-e2e handshake assertion

* fix(agent-core-v2): pass media parts through tool_result projection

- keep raw kosong content-part array for tool results carrying
  image/video/audio parts instead of flattening to text
- restore ReadMediaFile media rendering after session reload/resume

* chore: add changeset for optional server_hello heartbeat_ms

* docs(agent-core-v2): move tool_result media rationale to module header

Per the agent-core-v2 comment conventions, comments live solely in the
top-of-file block — move the media-passthrough rationale out of the
buildProtocolContent JSDoc into the module header.
2026-07-13 23:13:48 +08:00
Haozhe
0527ca2267
fix(agent-core-v2): carry session state across fork and ignite toolDedupe (#1629)
* fix(agent-core-v2): ignite toolDedupe plugin on agent creation

- ignite IAgentToolDedupeService in AgentLifecycleService before external
  hooks so it sits ahead of permission on onBeforeExecuteTool
- mirror the ignition in the test harness AgentTestContext
- add lifecycle test asserting the dedupe hooks are registered

* fix(agent-core-v2): carry session files and cron tasks over on fork

- copy blobs, plans, background-task output, and media originals into the
  forked session dir (excluding state.json, wire logs, and logs/)
- duplicate the source session's cron tasks with fresh ids retagged to the
  target session via the shared CRON_SESSION_TAG
- roll back the half-fork on failure: drop the materialized handle from the
  live registry and delete the target session dir
2026-07-13 22:53:35 +08:00
Haozhe
1c85f94472
feat(agent-core-v2): gate image formats and add media recovery resends (#1626)
* feat(kap-server): drop WS heartbeat; never terminate idle connections

- remove ping interval / pong timeout / socket.terminate() from both the
  v1 (WsConnectionV1) and v2 (WsConnection) WebSocket connections
- v2 protocol: drop pong from the client message schema, remove PingMessage
  and ReadyMessage.heartbeatMs; the ready frame is now bare { type: 'ready' }
- v1 protocol: remove buildPing/PingFrame and ServerHelloPayload.heartbeat_ms;
  server_hello no longer advertises a heartbeat
- drop pingIntervalMs/pongTimeoutMs options from registerWs/registerWsV1

* feat(agent-core-v2): gate image formats and add media-stripped resend

- add image-format-policy as the single source of truth for the
  provider-accepted image MIME set (PNG/JPEG/GIF/WebP), with MIME
  normalization, data-URL parsing, byte sniffing, and refusal notices
- enforce the format gate at every ingestion point: ReadMediaFile refuses
  unsupported formats with per-OS conversion guidance, MCP tool results and
  prompt step requests drop them for a text notice, and kap-server prompt
  routes gate inline, uploaded, and remote-URL images on the sniffed bytes
- resend once with every media part replaced by a text marker when the
  provider rejects an image's format, and keep later steps of the same turn
  on the media-stripped projection (v1 parity)
- commit the WebP decoder wasm as a base64 module for the bundled CLI, with
  a regenerate script

* feat(agent-core-v2): add media-degraded 413 resend and WebP re-encoding

- resend once with the media-degraded projection after an HTTP 413
  body-size rejection: all but the two most recent media parts are replaced
  by text markers, and later steps of the turn stay degraded (stripped
  still wins over degraded)
- generalize stripAllMediaParts into degradeOlderMediaParts; the
  media-stripped resend is now the keepRecent=0 case
- re-encode non-animated WebP through the wasm decoder and the PNG/JPEG
  ladder instead of passing oversized WebP through (animated WebP still
  passes through whole)
- stop lossless PNG rescaling at a 1000px floor and switch to the JPEG
  ladder below it, so small byte budgets stay readable
- add the @jsquash/webp dependency

* feat(agent-core-v2): add flag-gated fault injection for recovery testing

- add the faultInjection domain: a one-shot arm/take latch that raises a
  deterministic provider failure (HTTP 413 body-size or image-format 400)
  before the provider is contacted, so the media-degraded / media-stripped
  recovery resends can be exercised end-to-end against a real provider
- gate arming behind the new fault-injection experimental flag
  (KIMI_CODE_EXPERIMENTAL_FAULT_INJECTION), off by default
- expose IAgentPromptService and IFaultInjectionService over the kap-server
  /api/v2 RPC channel
- add a klient example that drives the REST ingestion gate and both
  recovery resends against a live server
2026-07-13 22:11:46 +08:00
qer
e91a616f21
fix(web): dedupe optimistic user message against snapshot resync (#1620)
* fix(web): dedupe optimistic user message against snapshot resync

* fix(web): match snapshot messages by identity
2026-07-13 21:54:08 +08:00
Luyu Cheng
3c0e368cbd
fix(agent-core-v2): serialize agent startup (#1614)
* fix(agent-core-v2): serialize agent startup
* style(agent-core-v2): align startup comments
2026-07-13 21:44:49 +08:00
Haozhe
4ec2e7fab1
feat(server): default to kap-server and remove the v1 server package (#1617)
* feat(server): default to kap-server and remove the v1 server package

- kimi server run / kimi web now boot kap-server (agent-core-v2 engine)
  unconditionally; the KIMI_CODE_EXPERIMENTAL_FLAG gate on the server
  path is gone (the kimi -p print-mode gate stays)
- move the OS service manager (svc: launchd/systemd/schtasks) from
  packages/server into packages/kap-server and export it there
- repoint the CLI server subcommands, tests, and dev scripts at
  kap-server; relabel the web dev backend presets default/multi
- delete packages/server and update workspace bookkeeping (flake.nix,
  pnpm-lock.yaml, changeset ignore docs, AGENTS.md, agent-core-dev skill)

* test(server-e2e): remove scenarios that depend on v1 debug endpoints

Scenarios 04-stateless-controls, 10-prompt-queue-steer and
12-send-and-cancel assert through the /api/v1/debug/prompts/*
introspection routes, which only the deleted v1 server mounted —
kap-server's --debug-endpoints is a documented no-op, so these
scenarios can only 404 now. The vitest e2e files using the same
surface already skip when it is absent.
2026-07-13 21:43:45 +08:00
Luyu Cheng
32cbd0cf61
fix(web): let workspace picker fit its content (#1611)
* fix(web): size workspace picker from full content

* chore: add web workspace picker changeset

* refactor(web): use intrinsic workspace picker sizing

* fix(web): cap workspace picker to conversation pane
2026-07-13 21:39:16 +08:00
Luyu Cheng
09c05346a8
test(server): stabilize approval teardown (#1621) 2026-07-13 21:35:17 +08:00
7Sageer
b2daa405f0
fix(agent-core-v2): support services.moonshot_search config for WebSearch (#1613)
* fix(agent-core-v2): support services.moonshot_search config for WebSearch

The v2 engine only resolved the WebSearch backend from the managed Kimi OAuth provider, so an environment that configures web search purely via [services.moonshot_search] (api key, no login) got no provider and the WebSearch tool stayed hidden. Register the services config section and resolve the provider config-first, mirroring v1 precedence.

* fix(agent-core-v2): preserve custom services config
2026-07-13 21:18:40 +08:00
liruifengv
83e175399f
feat: auto-background timed-out foreground bash commands (#1591)
* feat: auto-background timed-out foreground bash commands

* fix: discourage blocking TaskOutput waits on background tasks

* test: avoid unsafe string conversion in bash timeout test

* fix: align bash timeout description with auto-background opt-out

* feat(agent-core-v2): auto-background timed-out bash commands

- detach timed-out foreground Bash tasks and re-arm the background deadline
- align TaskOutput guidance with non-blocking background task handling
- add klient SEA end-to-end coverage for the v2 server

* fix(klient): clean up lint errors in auto-background e2e example

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-07-13 21:08:21 +08:00
_Kerman
ae1bf43c7b
ci: split test job into 5 parallel vitest shards (#1618) 2026-07-13 21:04:56 +08:00
liruifengv
098623ed9f
chore(web): drop the /help, /model, /provider, and /permission slash commands (#1615)
* chore(web): drop the /help, /model, /provider, and /permission slash commands

* chore: drop the changeset
2026-07-13 20:52:19 +08:00
qer
e223549a79
fix(web): make mid-turn delta offsets step-relative (#1609)
* fix(web): make mid-turn delta offsets step-relative

Reset in-flight text and client stream alignment at step boundaries so
resync seeds only the current step instead of duplicating prior steps.

* fix(web): dedupe resync-seeded messages by normalized content

The exact-JSON content signature missed duplicates when the two copies
differed by thinking signature, tool progress, part boundaries, or the
tool set (finished parallel tools leave running_tools). Reduce content
to concatenated stream text plus sorted tool-call ids and treat a
covered subset as the duplicate, merging the seed's tool progress into
the existing cards before dropping it.
2026-07-13 20:47:56 +08:00
qer
f338fcdac4
fix(web): restore swarm member list after page refresh (#1589)
* fix(web): restore swarm member list after page refresh

* chore: add changeset for swarm roster refresh fix
2026-07-13 20:40:52 +08:00
liruifengv
3598793b7d
fix(agent-core-v2): append sessions to shared index so v1 TUI can resume web-created sessions (#1612)
* fix(agent-core-v2): append sessions to legacy index so v1 TUI can resume web-created sessions

* chore: remove changeset

* refactor: rename appendLegacySessionIndexEntry to appendSessionIndexEntry

* fix(agent-core-v2): address review feedback on session index append

- Await appendLogStore.flush() so the shared index is durable before create/fork returns
- Append fork entries only after wire copy and agent replay complete
- Move method-level rationale into the file header per package comment conventions
2026-07-13 20:30:55 +08:00
liruifengv
2da45fc419
fix(web): restore the goal card after a page refresh (#1606)
* fix(web): restore the goal card after a page refresh

* fix(web): assert goal endpoint URL without stringification

* fix(web): skip goal recovery write when a live goal event wins the race

* fix(web): track goal events with a per-session version so clears win the recovery race
2026-07-13 19:49:58 +08:00
_Kerman
22ec37b2c0
fix(agent-core-v2): skip deterministic provider validation retries (#1603)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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
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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-13 18:40:25 +08:00
_Kerman
09c1c32960
fix(agent-core-v2): align compaction injections and goal tools with v1 (#1602) 2026-07-13 18:30:12 +08:00
Haozhe
8a4ee05951
fix(v2): detect MSYS2 bash from native toolchain git exec paths (#1590)
- recognize ucrt64/clang64/clangarm64 prefixes in git --exec-path
  alongside legacy mingw32/mingw64 when locating bash on Windows
- port the three MSYS2 regression tests from kaos to the v2 probe
2026-07-13 18:03:10 +08:00
_Kerman
dc309a7dfb
fix(web): keep context usage live on the v2 engine (#1601) 2026-07-13 18:00:58 +08:00
_Kerman
4feca6b073
fix(agent-core-v2): align rate-limit retries with v1 (#1598) 2026-07-13 17:50:23 +08:00
7Sageer
d601847f22
fix: send the product User-Agent on provider registry and catalog fetches (#1597)
* fix: send the product User-Agent on provider registry and catalog fetches

Registry (api.json) and models.dev catalog fetches only carried the
runtime default User-Agent while every other outbound request sends
kimi-code-cli/<version>. Thread an optional userAgent through
fetchCustomRegistry / fetchCatalog and the shared refresh host, pass
the product UA from the CLI, TUI, and both daemons, and seed a default
product UA in kap-server that hosts can override via opts.seeds.

* refactor: use options for registry fetches
2026-07-13 17:45:34 +08:00
_Kerman
5502b90b42
test: cut slow suite runtimes and isolate experimental flag from host env (#1595) 2026-07-13 17:45:22 +08:00
_Kerman
01dd63207d
refactor(agent-core-v2): consolidate the tool domain into src/tool (#1599) 2026-07-13 17:19:37 +08:00
_Kerman
63300ddf0e
refactor(agent-core-v2): derive wire-record scope from agent scope context (#1596) 2026-07-13 16:46:22 +08:00
_Kerman
bc8eb1e417
chore: drop #/ import array fallbacks and custom resolution plugins (#1594) 2026-07-13 16:37:35 +08:00
liruifengv
20615902c2
fix(tui): deliver pasted media in /skill args and Ctrl-S steer (#1588)
* fix(tui): deliver pasted media in /skill and plugin command args via file tags

Skill and plugin command args are a plain-text RPC channel, so pasted image/video placeholders never reached the model. Rewrite matched placeholders into <image|video path="..."> tags pointing at cache-dir copies (the same convention pasted videos already use), and gate them on model media capabilities like the normal prompt path.

* fix(tui): deliver pasted media when steering with Ctrl-S

Ctrl-S steer dropped media: queued messages were reduced to their text and the editor draft never went through placeholder extraction, so session.steer only ever received placeholder strings. Carry the queued items' extracted parts, extract the editor draft on the spot (with the same capability gate as a normal submit), and merge everything into a part list when any item has media.

* fix(tui): avoid empty text separator when steering consecutive media

The '\n\n' item separator could land as a standalone whitespace-only text part between two media parts, which normalizePromptInput rejects — failing the steer after the queue and editor draft were already cleared. Only append the separator onto a trailing text part so it always merges.

* fix(tui): keep blank-line separator between media and text steer items

The separator was only appended onto trailing text parts, so a media item followed by a plain-text steer item lost the historical '\n\n' separation. Always append it — it merges into the adjacent text part — except between two touching media parts, where a standalone whitespace-only text part would be rejected by normalizePromptInput.

* fix(tui): use escape-proof media references in /skill args

Skill args are XML-escaped on render (renderSkillAttributes + expandSkillParameters), which mangled the <image|video path> tags into escaped text. Add a 'plain' reference style (Attached image file: <path> (open it with ReadMediaFile)) with no tag/attribute boundary characters and use it for the skill channel; plugin command args expand verbatim and keep the tag form.

* fix(tui): strip XML boundary chars from plain-style video cache names

Video cache names carry the original label, which permits <>&"; in the plain (/skill) reference style those chars get XML-escaped in args and the reference no longer matches the file on disk. Strip them from the cache name in plain mode; the tag channel is unchanged (its attribute escaping already handles them).

* fix(tui): surface media materialization failures as TUI errors

The cache copy inside media placeholder rewriting can throw (unwritable cache dir, vanished video source) before any RPC .catch is installed, escaping the fire-and-forget slash-command dispatcher as an unhandled rejection. Catch it in sendSkillActivation, activatePluginCommand, and the Ctrl-S draft extraction, show an error, and leave queue/draft/session state untouched.
2026-07-13 16:16:44 +08:00
_Kerman
2185237c2f
refactor(v2): bind op definitions to models and require zod payload schemas (#1593) 2026-07-13 16:16:02 +08:00
qer
924d5c9141
feat(web): dev backend switcher and engine badge for dual-engine debugging (#1592)
Add the plumbing to debug kimi-web against the v1 (server) and v2
(kap-server) engines side by side:

- root dev:v1 / dev:v2 scripts; v2 boots kap-server with the
  multi_server flag on a fixed port so both engines can run at once
- dev-proxy backend switcher: GET/POST /__kimi-dev/backend repoints
  the /api/v1 proxy at runtime (HTTP + WS) without a Vite restart
- dev-only sidebar backend pill reading /meta's backend field, with a
  one-click switcher menu; Settings shows the backend engine too
- re-fetch /meta on every WS (re)connect so the badge stays truthful
  across backend restarts and switches
- strip the browser Origin header on proxied requests: v1's WS upgrade
  path rejected the Vite-origin vs server-Host mismatch with 403
2026-07-13 15:22:02 +08:00
wszqkzqk
83370f17ef
fix(kaos): detect bash when git comes from a native MSYS2 toolchain (#1580)
git --exec-path of a native MSYS2 git returns a path under the
modern toolchain prefixes (ucrt64, clang64, clangarm64), but the root
inference scan only covered the legacy mingw32/mingw64 prefixes, so
detection fell through to KaosShellNotFoundError on hosts without
Git for Windows even though MSYS2 bash exists at <root>\usr\bin.

Extend the segment scan to all current MSYS2 prefixes so the MSYS2
root is inferred correctly. mingw32/mingw64 stay for Git for Windows
compatibility.
2026-07-13 13:45:04 +08:00
qer
49a8c84a49
feat(web): cap markdown table column width at 700px (#1587)
* feat(web): cap markdown table column width at 700px

* fix(web): clamp table column width through the cell content box
2026-07-13 13:35:07 +08:00
Haozhe
ceb158dc54
feat(v2): land agent-core-v2 engine and kap-server behind experimental flag (#1441)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / test-pi-tui (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
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix: adapt grep tool to agent-core-v2

* fix(agent-core-v2): enrich PATH from the user's login shell at startup

- port probeLoginShellPath/mergeLoginShellPath/applyLoginShellPath into
  _base/execEnv/loginShellPath.ts as a pure helper (no DI)
- export execFileText from environmentProbe for reuse by the probe
- run applyLoginShellPathFromNode concurrently with the host probe in
  HostEnvironmentService, mirroring kaos LocalKaos.create()

Aligns agent-core-v2 with kaos 021786f5 so the Bash tool finds
user-installed tools (e.g. Homebrew's gh) when kimi-code is launched
from a GUI or non-login shell.

* fix(agent-core-v2): prefer persisted cwd on resume

* feat(agent-core-v2): support structured response formats

* fix: restore v2 grep telemetry and tests

* fix: preserve v2 compaction boundary

* fix(agent-core-v2): align agent and swarm tool behavior

* feat(ws-v1): add per-agent event subscription filter

- protocol: add optional agent_filter to client_hello and subscribe
- kap-server: carry per-subscription agent allowlists through the broadcaster
  and connection, narrowing live fan-out and replay to selected agents while
  keeping a single global sequence and bypassing the filter for global events
- agent-core-v2: degrade MiniDbQueryStore to a no-op read model when the
  query-store lock is held by another process instead of crashing the host

* feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492)

* ci: release packages (#1468)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(changelog): sync 0.23.2 from apps/kimi-code/CHANGELOG.md (#1496)

* chore: add changeset for agent swarm parity

* fix: align v2 compaction prompt

* fix: recover v2 compaction from plain 413

* fix: report v2 compaction retry telemetry

* fix: align MCP discovery and output with v1

* fix: align v2 compaction auth guards

* fix: align v2 grep behavior with v1

* chore: remove agent swarm changeset

* fix(agent-core-v2): restore task resume parity

* feat(agent-core-v2): record llm request traces

* fix(agent-core-v2): align AskUserQuestion tool chain with v1

- translate wire ids back to question text / option labels when resolving
  a question over REST, joining multi-select labels with ', '
- enforce unique question texts / option labels and non-empty strings at
  both the schema and the execution path
- cancel pending questions on turn abort or background task stop by
  dismissing the parked entry (resolves null, v1 broker semantics)
- restore the unsupported-client fallback and dismissed-error handling
- pass empty header / option description through verbatim and align the
  model-facing tool description byte-for-byte with v1
- drop the synthetic expires_at field from the question wire shape

* fix(agent-core-v2): preserve compaction hook session

* test(agent-core-v2): cover concurrent agent background limit

* fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px (#1460)

* fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px

Image compression now reports original dimensions in the decoded
(EXIF-rotated) space, matching the coordinate system of the sent image
and of ReadMediaFile region readback; previously portrait JPEGs
(orientation 5-8) got swapped width/height in captions. The longest-edge
downscale cap rises from 2000px to 3000px, and the default jimp resize
path is documented as the anti-aliased area-average one so it is not
accidentally switched to a point-sampled interpolation mode.

* test: shrink oversized image fixtures to fit CI timeouts

The 3600x3600 fixtures introduced for the 3000px edge cap nearly doubled
the pixel area jimp has to decode and deflate, pushing the slowest
compression tests past the 5s vitest timeout on CI runners. 3600x1800
keeps every fixture over the cap while restoring roughly the workload of
the old 2600x2600 fixtures that CI handled comfortably.

* test: pin anti-aliased downscale quality with executable guards

A 1px checkerboard probe pins the compressor to full-coverage averaging
at integer and fractional ratios, with jimp's point-sampled BILINEAR
mode kept as the executable aliasing counter-example (it collapses the
50%-gray pattern to solid black at 4:1). Also guards the other classic
downscale bugs: transparent-pixel color bleed, mean-brightness drift,
iterative recompression degradation, and zero-size collapse on extreme
aspect ratios.

* fix(agent-core): report decoded EXIF-rotated dimensions in ReadMediaFile notes

The media note derived its original-dimensions line from the header
sniff, which reports pre-rotation values for EXIF orientation 5-8
JPEGs. The sent image and region readback both live in the decoded
(rotated) space, so portrait photos got axis-swapped coordinate
guidance. Once a decode has happened — compression or crop — its
dimensions now overwrite the sniffed ones.

* fix(agent-core): improve handling of EXIF orientation in image dimensions and metadata

* fix(agent-core): sniff EXIF orientation and step budget fallback through 2000px

Two follow-ups to the EXIF and 3000px-cap changes:

sniffImageDimensions now reads the JPEG EXIF Orientation tag (pure
header parse, both byte orders) and reports display-space dimensions
for orientations 5-8. Passthrough images — never decoded — previously
kept the pre-rotation header size in compression results and media
read notes, disagreeing with the decoded space that region readback
uses.

encodeWithinBudget steps the over-budget fallback through 2000px
before the 1000px last resort. Raising the cap to 3000px had left a
regression window: an image whose 2000px encode fits the byte budget
was sent at 1000px where the old 2000px cap used to send it at
2000px.

* fix(kimi-code): record pasted image dimensions in display space

The TUI paste path recorded attachment and original dimensions from its
raw header parser, which ignores EXIF orientation. For a portrait JPEG
the submit-time caption then contradicted the sent image's aspect and
region readback coordinates were axis-swapped. Dimensions now come from
the compression result, which reports display space on both the
compressed and passthrough paths; parseImageMeta remains only the
format/mime gate.

* feat(agent-core): add image compression and crop telemetry

Every image ingestion path now reports an image_compress event —
outcome (compressed / passthrough fast, guard, unsupported, unhelpful,
error), input/output formats, byte and pixel sizes, EXIF transposition,
and duration — and region readback reports an image_crop event with a
failure classification and the region's share of the original area.

Wiring is per call site via a new CompressImageOptions.telemetry
option, so the outcome split and timing are measured inside the
compressor while each caller only names its source: ReadMediaFile
(tool construction, like GrepTool), MCP tool results (McpOutputOptions),
server prompt ingestion (ICoreProcessService now exposes the host
telemetry client), ACP prompts (session track adapter), and TUI paste
(host.track adapter). Properties are numeric/enum only — never paths
or content — and a throwing client can never affect the compression
result.

* fix(agent-core): run the full JPEG quality ladder at fallback sizes

The fallback rescales encoded only at quality 20, so a JPEG whose
ladder failed at the fitted size collapsed straight to the lowest
quality even when the smaller size left budget headroom for a higher
rung (the realistic window is the 1000px step, where the 4x pixel
drop pays for q80/q60). Each fallback edge now walks the same
q80-to-q20 ladder as the fitted size.

* test: shrink heavy JPEG fixtures and add explicit timeouts

The fallback-ladder test runs ~11 pure-JS JPEG encodes and the EXIF
paste test decodes, rotates, and re-encodes a 6.5MP frame; both sat at
the edge of the 5s vitest timeout on CI runners. Narrower fixtures cut
the pixel area (the ladder test keeps its width above 2000px so the
full fallback chain still runs) and explicit 15s timeouts absorb runner
variance.

* fix(server): scope prompt image compression telemetry to the session

The prompt-ingestion image_compress events were emitted with the bare
host telemetry client, while every agent-side source inherits a
session-scoped client — so prompt_inline/prompt_file events could not
be correlated with their session. The route now wraps the client with
withTelemetryContext({ sessionId }) like rpc/core-impl does for
session telemetry.

* chore(changeset): consolidate image compression changesets

One entry covering the cap raise and the EXIF dimension fix, listed
for both the CLI and the SDK so the SDK changelog's compression
description (previously pinned at 2000px) stays accurate.

* fix: count goal creation turn (#1477)

* feat(kosong): support structured response formats (#1397)

* fix: clarify goal blocked audit guidance (#1481)

* feat(agent-core): discard loaded tool schemas on compaction (#1471)

Align progressive tool disclosure with the discard-on-compaction model:
compaction no longer rebuilds loaded dynamic tool schemas. The boundary
announcement re-lists every loadable name, the model re-selects what it
still needs, and a from-memory call to a no-longer-loaded tool is
rejected by preflight with select guidance.

This removes the keep-all rebuild and its half-trigger budget heuristics
entirely: the post-compaction floor is back to users + summary, which is
structurally outside the auto-compaction trigger band, and the guard
baseline degenerates to summary + reinjected reminders. Every downstream
mechanism already treated the empty loaded set as its consistent base
state (ledger scan, pending clear at the compaction boundary, deferred
extras, preflight wording), so this is a strict simplification.

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>

* fix(kimi-code): exit 1 when a headless (-p) turn fails (#1483)

Headless (`kimi -p`) failures could exit with code 0 when the event loop
drained during the shutdown cleanup (e.g. telemetry's unref'd retry backoff
when the network is blocked), because the rejection never reached the
process.exit(1) call. Set the failure exit code before any await in both
the run-prompt catch and the main catch, and keep the cleanup timeout ref'd
so the loop stays alive long enough for the rejection to propagate.

* feat(plugins): add Vercel plugin to marketplace (#1489)

* feat(web): support Enter key to confirm archive and other dialogs (#1490)

* feat(web): redesign cron reminder as a message bubble (#1480)

* feat(web): redesign cron reminder as a message bubble

Restyle the cron trigger notice as a right-aligned user-style message bubble that shows the scheduled prompt in full (wrapping across lines), with a small meta row beneath it for the schedule, status, job id and run time. Extract a shared MessageTime component used by both user messages and the cron reminder so the timestamp format and click-to-expand behavior stay consistent, and give the CronCreate/CronList/CronDelete tools distinct calendar icons.

* refactor(web): render cron reminders only as standalone turns

Remove the embedded cron block path from the web transcript projector so cron reminder fires always render through the standalone right-aligned bubble path.

* chore(web): simplify cron redesign changeset

* fix(web): composer model switch also updates global default model (#1491)

* fix(web): composer model switch also updates global default model

The composer model switcher still switches the active session's model via
POST /sessions/{id}/profile (awaited, so the model pill reflects the result),
and additionally fires POST /api/v1/config with { default_model } as a
fire-and-forget side effect so new sessions inherit the chosen default. The
config request is skipped when the model already matches the current default.

* fix(web): route ModelPicker overlay selection through the default-model update

The overlay opened from the composer's "More models" row (and /model) is a
continuation of the same switch flow, so its selection now also bumps the
global default model instead of only switching the active session.

* fix(web): only persist the default model after a confirmed session switch

setModel now returns whether the switch was accepted (true for the draft
path), so the composer flow no longer writes a stale or invalid model alias
into the global config when the session-level switch failed and rolled back.

* feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492)

* ci: release packages (#1468)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(agent-core-v2): restore native append for Write append mode

- add IHostFileSystem.appendText backed by fs.appendFile (O_APPEND)
- route WriteTool append through it instead of read-then-rewrite, so
  existing content is never read, truncated, or clobbered by concurrent
  writers and a crash mid-append can only lose the new bytes
- update typed host-fs test fakes and WriteTool append assertions

* fix(agent-core-v2): align task tool prompts with v1

* fix(agent-core-v2): align compaction empty retry

* fix(agent-core-v2): restore web search source site and citation reminders

- surface source site: add WebSearchResult.siteName, map site_name in the
  Moonshot provider, and render the Site: line in tool output
- restore the per-search inline citation reminder alongside the results
- align web-search.md with v1: source-site/result-summary guidance and the
  static citation reminder

* fix(agent-core-v2): route task timeouts through SIGTERM grace + SIGKILL

- add terminateWithGrace shared by stop, timeoutMs, detachTimeoutMs, and
  track deadlines: cancel/SIGTERM -> 5s grace -> forceStop (SIGKILL)
- coerce a post-abort self-settled `killed` to `timed_out` so a deadline
  stays reported as timed_out, matching v1 settlementForOutcome
- add manager tests for SIGTERM-ignored escalation, graceful-exit within
  the grace window, and detachTimeout teardown

* fix(agent-core-v2): restore fs.grep streaming early-kill and symlink reporting

- stream `rg --json` in fs.grep and SIGKILL once max_total_matches/max_files is reached, restoring v1 early-stop instead of buffering the whole output
- report symlinks as kind 'symlink' in fs search/list/stat via lstat and never descend into symlinked directories
- remove the unused os grepSearch helper (dead, non-streaming, bypassed ISessionProcessRunner)

* test(agent-core-v2): align truncated compaction retry

* fix(agent-core-v2): align MCP tool results with v1

* fix(agent-core-v2): align blocked compaction failures

* fix(agent-core-v2): align manual compaction tool projection

* fix(agent-core-v2): preserve tail in windowed compaction

* fix(agent-core-v2): read todos from wire model, sanitize replay

- make SessionTodoService a stateless facade over the main agent's TodoModel:
  getTodos reads wire.getModel(TodoModel) live, setTodos only dispatches a
  todo.set op, and onDidChange is bridged from wire.subscribe(TodoModel); the
  in-memory list copy is gone so the live and post-replay views cannot drift
- sanitize todo.set payloads in apply via readTodoItems, so replayed or
  hand-written records cannot poison the model or downstream renders
- update todo tests to a sanitizing/notifying/replaying wire stub and cover
  malformed todo.set replay and main-absent reads
- record the main-agent-wire persistence debt for the ISessionWireService move

* fix(agent-core-v2): port v1 Bash tool output cap, saved-output reference, and background gating (#1503)

* fix: align v2 task observable behavior

* fix: v2 full compaction

* fix(agent-core-v2): align task wait timeout behavior

* chore: webSearch & FetchUrl Sync #1260

* fix: align background agent guidance

* fix(agent-core-v2): align full compaction with v1

* fix(agent-core-v2): align v1 wire records

* fix(agent-core-v2): remember observed compaction context window

* fix: align Agent / AgentSwarm

* fix(agent-core-v2): port v1 parity fixes for hooks, anthropic, thinking config and add-dir (#1504)

* fix(agent-core-v2): hide console window when running hooks on Windows

Port the v1 hooks runner fix: extract buildHookSpawnOptions and pass
windowsHide:true so hook child processes no longer flash a console
window on Windows, mirroring the node-local process host defaults.
Includes the same regression tests as v1.

* fix(agent-core-v2): port anthropic max_tokens ceiling and override fixes

Port two v1 kosong fixes to the v2 anthropic provider:

- Fall back to the nearest lower catalogued minor when resolving the
  Claude output ceiling, and catalogue Opus 4.8's documented 128k cap,
  so an uncatalogued minor no longer drops to the family baseline.
- Treat an explicit defaultMaxTokens as the final max_tokens value
  instead of clamping it to the built-in ceiling.

Mirrors the v1 regression tests in a new anthropic-max-tokens test file.

* refactor(agent-core-v2): converge thinking config to enabled/effort

Port the v1 thinking-config overhaul (#1132's config side) to v2:

- ThinkingConfigSchema becomes { enabled, effort, keep }; the mode enum,
  the separate defaultThinking section, and the KIMI_MODEL_THINKING_MODE /
  KIMI_MODEL_DEFAULT_THINKING env bindings are removed.
- The effort resolver drops the mode/defaultThinking branches and no
  longer normalizes a requested 'on' to a concrete effort in core; 'on'
  is taken verbatim and normalization stays at the UI boundary.
- OAuth login/refresh and catalog refresh now persist the thinking.enabled
  value computed by the shared oauth apply/restore logic instead of
  dropping it and writing the removed default_thinking key, so
  [thinking] enabled = false actually disables thinking and the login
  default survives on disk.

Mirrors the v1 resolver regression tests and adds a persistence
regression for the refresh path.

* docs(agent-core-v2): fix stale loop-event comments after wire parity

The v1.4 wire-parity alignment switched the v2 live loop to stream turns
as context.append_loop_event records, but three comments still described
the old world (restore-only Op, "v2 never emits loop events"). Update
them to match the actual write path: non-loop appends use append_message,
the loop persists loop events byte-compatible with v1, and the fold runs
both at live dispatch time and on replay.

* fix(agent-core-v2): load workspace additional dirs on session create and resume

The /add-dir command persisted remembered dirs to .kimi-code/local.toml,
but session materialization never read them back and offered no caller
additionalDirs entry point — a remembered dir silently stopped applying
to new, resumed, and forked sessions.

Mirror v1's createSession/resumeSession: merge the project-local
local.toml dirs with caller-supplied additionalDirs (relative paths
resolve against workDir) and seed the session workspace context in
materializeSession, so create/resume/fork all pick them up. A broken
local.toml fails the create loudly with CONFIG_INVALID, same as v1.
Tests mirror v1's runtime coverage for the load/merge/dedupe/resume/fork
scenarios.

* fix(kimi-code): forward create-session additional dirs from the v2 harness

The in-process v2 print-mode harness dropped the SDK CreateSessionOptions
additionalDirs when calling ISessionLifecycleService.create, so --add-dir
never reached the v2 resolver. Pass it through.

* fix(agent-core-v2): align full compaction observability

* fix(agent-core-v2): remove compact hook trigger state

* feat(agent-core-v2): enhance agent lifecycle with context size tracking and concurrency checks

* fix(agent-core-v2): align media reads with v1 note channel and EXIF handling (#1505)

* fix(agent-core-v2): align media reads with v1 note channel and EXIF handling

Port two agent-core changes into agent-core-v2:

- Move the ReadMediaFile media summary from an inline <system> text part
  onto the tool result's note side channel, so raw <system> markup never
  renders in UIs (matching the MCP output path).
- Report image dimensions in the decoded EXIF-rotated space: the header
  sniff now reads the JPEG Orientation tag, and once a decode happened
  (compression or crop) its dimensions overwrite the sniffed ones, so
  portrait photos no longer get axis-swapped coordinate guidance.
- Raise the longest-edge downscale cap from 2000px to 3000px, step the
  over-budget fallback through 2000px before the 1000px last resort, and
  run the full JPEG quality ladder at fallback sizes.
- Report image_compress / image_crop telemetry for media reads (source
  read_media), with EXIF transposition and crop failure classification.

The tool description also regains the downsampling recovery guidance
(region / full_resolution readback) that the v2 copy predated.

* fix(agent-core-v2): align v1 wire records

* fix(agent-core-v2): hide compression captions and register media tools in production

Port the remaining v1 media gaps into agent-core-v2:

- Reroute inline image-compression captions out of user messages: the
  prompt service splits them at the append chokepoint (prompt and steer
  flush) and delivers them through the built-in system-reminder
  injection (origin {kind: 'injection', variant: 'image_compression'}),
  which every UI hides. Session titles/lastPrompt strip the caption the
  same way. The model still receives the full note.
- Register ReadMediaFile in production: media tools cannot use the
  module-level contribution table (capabilities are unknown until a
  model binds), so a new Eager agent-scope registrar re-runs
  registerMediaTools on every agent.status.updated where the model
  alias or its media capabilities changed, rebinding the video uploader
  and dropping the tool when the model loses media input.

* fix(agent-core-v2): port v1 parity fixes for hooks, anthropic, thinking config and add-dir (#1504)

* fix(agent-core-v2): hide console window when running hooks on Windows

Port the v1 hooks runner fix: extract buildHookSpawnOptions and pass
windowsHide:true so hook child processes no longer flash a console
window on Windows, mirroring the node-local process host defaults.
Includes the same regression tests as v1.

* fix(agent-core-v2): port anthropic max_tokens ceiling and override fixes

Port two v1 kosong fixes to the v2 anthropic provider:

- Fall back to the nearest lower catalogued minor when resolving the
  Claude output ceiling, and catalogue Opus 4.8's documented 128k cap,
  so an uncatalogued minor no longer drops to the family baseline.
- Treat an explicit defaultMaxTokens as the final max_tokens value
  instead of clamping it to the built-in ceiling.

Mirrors the v1 regression tests in a new anthropic-max-tokens test file.

* refactor(agent-core-v2): converge thinking config to enabled/effort

Port the v1 thinking-config overhaul (#1132's config side) to v2:

- ThinkingConfigSchema becomes { enabled, effort, keep }; the mode enum,
  the separate defaultThinking section, and the KIMI_MODEL_THINKING_MODE /
  KIMI_MODEL_DEFAULT_THINKING env bindings are removed.
- The effort resolver drops the mode/defaultThinking branches and no
  longer normalizes a requested 'on' to a concrete effort in core; 'on'
  is taken verbatim and normalization stays at the UI boundary.
- OAuth login/refresh and catalog refresh now persist the thinking.enabled
  value computed by the shared oauth apply/restore logic instead of
  dropping it and writing the removed default_thinking key, so
  [thinking] enabled = false actually disables thinking and the login
  default survives on disk.

Mirrors the v1 resolver regression tests and adds a persistence
regression for the refresh path.

* docs(agent-core-v2): fix stale loop-event comments after wire parity

The v1.4 wire-parity alignment switched the v2 live loop to stream turns
as context.append_loop_event records, but three comments still described
the old world (restore-only Op, "v2 never emits loop events"). Update
them to match the actual write path: non-loop appends use append_message,
the loop persists loop events byte-compatible with v1, and the fold runs
both at live dispatch time and on replay.

* fix(agent-core-v2): load workspace additional dirs on session create and resume

The /add-dir command persisted remembered dirs to .kimi-code/local.toml,
but session materialization never read them back and offered no caller
additionalDirs entry point — a remembered dir silently stopped applying
to new, resumed, and forked sessions.

Mirror v1's createSession/resumeSession: merge the project-local
local.toml dirs with caller-supplied additionalDirs (relative paths
resolve against workDir) and seed the session workspace context in
materializeSession, so create/resume/fork all pick them up. A broken
local.toml fails the create loudly with CONFIG_INVALID, same as v1.
Tests mirror v1's runtime coverage for the load/merge/dedupe/resume/fork
scenarios.

* fix(kimi-code): forward create-session additional dirs from the v2 harness

The in-process v2 print-mode harness dropped the SDK CreateSessionOptions
additionalDirs when calling ISessionLifecycleService.create, so --add-dir
never reached the v2 resolver. Pass it through.

* fix(agent-core-v2): report video_upload telemetry for media reads

Port the v1 video-upload telemetry wrapper into createVideoUploader:
every upload emits a video_upload event with outcome (success/error),
byte size, mime type, duration, and the caller's static props (model
alias, protocol tags), and a throwing telemetry client never affects
the upload outcome. The media-tools registrar supplies the sink and
props from the bound model.

Also restores two v1 rationale comments in ReadMediaFile (original-size
reporting and the full_resolution hard refusal) that were dropped
during the earlier port.

---------

Co-authored-by: 7Sageer <7sageer@djwcb.cn>
Co-authored-by: liruifengv <liruifeng1024@gmail.com>

* refactor(agent-core-v2): remove the microCompaction domain

- delete the microCompaction domain (service, wire model/op, config section,
  experimental flag) and its dedicated tests
- stop truncating old tool results in the context projector and drop the
  projector's now-unused instantiation dependency
- remove the domain from the layer map, package exports, and the DI x Scope
  dependency diagram
- retarget the flag-registry test and skill examples at a neutral flag

* fix(contextProjector): surface projection repairs via log warning

- add ProjectionAnomaly + onAnomaly sink through the project / projectStrict
  passes (reorder, synthesize, orphan / duplicate drop, leading drop, merge,
  blank-text drop) so the pure projection reports every wire-repair it applies
- AgentContextProjectorService injects ILogService and emits a single
  signature-deduped 'repaired the request to keep it wire-valid' warning,
  excluding trailing-tail synthesis, matching agent-core parity
- cover the trace and its dedup in the projector tests

* fix(agent-core-v2): fix cron killswitch, lost deliveries, id clashes

- killswitch: read KIMI_DISABLE_CRON live by re-applying the ConfigService
  env overlay on every get(); CronCreate reads it via ISessionCronService
  instead of a value frozen at tool registration
- delivery: resolve fire delivery on promptService.steer().launched so a
  rejected launch retains one-shot tasks for retry instead of deleting
  them; tick() is now async and awaits delivery before advancing cursors
- ids: switch cron task ids to ULIDs (from 32-bit hex) so two sessions
  sharing a workspace cannot overwrite each other's persisted task;
  CronDelete and persistence accept both ULID and legacy 8-hex ids
- display: CronCreate reports nextFireAt through the service so it honors
  KIMI_CRON_NO_JITTER and matches the scheduler and CronList
- migration: adopt shape-valid tasks with no sessionId tag on
  loadFromStore and stamp the tag back to disk
- persistence: create cron directories 0700 and files 0600 via
  FileStorageService dirMode/fileMode

Gate SessionCronService startup on config.ready and resolve clocks after
ready so config is never read before it is loaded; start() is now async.

* feat(fs-watch): add workspace fs watch with v1-compatible WS delivery

- os layer: add IHostFsWatchService over chokidar (raw create/modify/delete, .git ignored)
- session layer: add ISessionFsWatchService, a workspace-confined, debounced, .gitignore-aware FsChangeEvent feed
- kap-server: add FsWatchBridge pushing event.fs.changed over /api/v1/ws (watch_fs_add/remove, volatile, per-connection filter), byte-compatible with v1
- tests: os/session unit tests and kap-server fs-watch e2e

* refactor(agent-core-v2): run external hooks through IHostProcessService

- inject IHostProcessService into ExternalHooksRunnerService and thread it
  through runMatchedHooks to runHook instead of spawning node:child_process
- route hook termination through the service's cross-platform process-tree kill
- settle on the exit code plus drained stdout/stderr so fast-exiting hooks
  keep their trailing output
- hide the child console window on Windows via the service default
- update externalHooks tests for the new dependency

* fix(agent-core-v2): strict-decode Edit reads, align with v1

- read the Edit target with errors:'strict' so a non-UTF-8 file fails the
  edit instead of being silently rewritten as U+FFFD (matches v1 kaos)
- declare readWriteFile access since Edit reads before it writes, matching v1
- render edit.md directly instead of through renderPrompt: it has no template
  vars, and raw avoids treating literal {{ }} as a template
- restore the replace_all usage example in edit.md (v1 #1102)
- add a regression test asserting a non-UTF-8 file fails the edit and keeps
  its bytes untouched

* fix(agent-core-v2): dedupe AgentMeta legacy field declarations

* refactor(agent-core-v2): persist wire records natively in the v1 vocabulary

Remove the persist-time v1 rewrite layer (serializeV1WireRecord): ops now
write v1-shaped records directly, live-only state is declared persist:false
on the op instead of being stripped at write time, and the swarm-exit
reminder pop replays from the swarm_mode.exit record via a cross-model
reducer. Fixes resumed sessions losing the todo list, drifting turn
counters after retries, and removed reminders reappearing on resume.

* refactor(agent-core-v2): move ReadTool status block to note side channel

- ReadTool.finishReadResult now returns rendered lines as `output` only and
  rides the `<system>` status block on the model-only `note` side channel
- drop the finishOutput helper that concatenated content and status
- update read.test.ts expectations to assert `note` separately from `output`

* refactor(agent-core-v2): split blob service helpers, rewrite tests

- extract rewriteMediaUrls and blobref parse/format helpers to dedupe URL rewriting
- move the byte-bounded LRU cache into a module-private ByteLruCache with focused unit tests, dropping the protected maxCacheSize test seam
- rewrite blob service tests against the contract on in-memory storage, removing cache-internals cases

* fix: make release-e2e scenarios pass under agent-core-v2

Three independent fixes for release-e2e failures that only appeared with the experimental v2 engine (KIMI_CODE_EXPERIMENTAL_FLAG):

- agent-core-v2: register the KIMI_MODEL env overlay statically so it takes effect even when ModelService is not instantiated (the DI layer does not auto-instantiate Eager services). Fixes wire-llm-request-trace.

- cli: omit the leading system.version meta line in stream-json prompt mode so the role sequence stays clean. Fixes stream-json-cron.

- agent-core-v2: honor --skills-dir via a new explicit skill source seeded from the host. Fixes interactive-skills-dir.

Cherry-picked from 2a7232737 (v2-migration), excluding the node-sdk V2Host change (not applicable on this branch).

* refactor(agent-core-v2): drop replay-only wire ops

Remove the three replay-only Ops that were kept for pre-alignment / 1.5 sessions, now that v2 persists natively in the v1 vocabulary:

- turn.launch (replaced by turn.prompt)

- todo.set (replaced by tools.update_store with key 'todo')

- context.splice (replaced by context.append_message / append_loop_event)

Also drop the dead code that handled them (transcript reducer, task-origin extraction, blob dehydration, harness helpers) and migrate the affected tests to the v1 record types. The live write path already emitted only v1 records, so wire.jsonl output is unchanged.

* Revert "fix: make release-e2e scenarios pass under agent-core-v2"

This reverts commit ec9dae72ab.

* fix(agent-core-v2): use a fresh TextDecoder per append-log read

The module-level TextDecoder is stateful in stream mode: it buffers a
trailing incomplete multi-byte sequence until the next decode. Sharing it
across reads let leftover state from an earlier read that returned early
(e.g. ensureWireMetadata bailing on the leading metadata record) leak into
the next read and prepend a U+FFFD to its first line, corrupting the
metadata envelope and breaking session fork with "corrupted line 1".

Give each read its own TextDecoder so decoder state never leaks between
reads.

* fix(agent-core-v2): register KIMI_MODEL env overlay statically

The KIMI_MODEL_* effective overlay was registered by ModelService on construction, but the DI layer does not auto-instantiate Eager services, so the overlay never took effect when nothing resolved IModelService. This broke the release-e2e wire-llm-request-trace scenario, where KIMI_MODEL_NAME must synthesize the env model and its thinking capability.

Move registration to module load via a new configOverlayContributions collector, drained by ConfigRegistry on construction — mirroring the existing configSectionContributions pattern. ModelService no longer depends on IConfigRegistry.

* docs(agent-core-v2): clarify live-only op semantics

* fix(agent-core-v2): preserve oversized tool results

* chore: remove full compaction complete data type

* fix(agent-core-v2): align foreground output cap

* fix(agent-core-v2): gate skill prompt injection

* chore(skills): bundle review and test lenses into kc-review

- add agent-core-review umbrella skill with slop and test sub-skills
- move write-tests rules into agent-core-review/test and drop the standalone skill

* feat(v2): auto-mint session ids and harden print-mode background drain

- make CreateSessionOptions.sessionId optional; SessionLifecycleService.create and fork now mint `session_<lowercase-uuid>` via a shared createSessionId helper, so edge layers stop minting their own ids (drop randomUUID in the v2 harness, ulid in kap-server)
- rework V2Session.waitForBackgroundTasksOnPrint to re-enumerate each round, suppress terminal notifications while waiting, and bound the drain by [task].print_wait_ceiling_s (default 1h) instead of a hardcoded 30s cap, so kimi -p can run long tasks to completion without being steered into a new turn
- add v2-session unit tests; seed session/agent/bootstrap context in the tool-dedupe harness for the real executor

* fix(agent-core-v2): refresh system prompt after compaction

* docs(agent-core-review): limit kc-review skill to agent-core-v2

Clarify that the kc-review lenses apply only to packages/agent-core-v2
(the DI x Scope engine), not to the legacy packages/agent-core or other
packages.

* fix(agent-core-v2): align model-facing prompts

* fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px (#1460)

* fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px

Image compression now reports original dimensions in the decoded
(EXIF-rotated) space, matching the coordinate system of the sent image
and of ReadMediaFile region readback; previously portrait JPEGs
(orientation 5-8) got swapped width/height in captions. The longest-edge
downscale cap rises from 2000px to 3000px, and the default jimp resize
path is documented as the anti-aliased area-average one so it is not
accidentally switched to a point-sampled interpolation mode.

* test: shrink oversized image fixtures to fit CI timeouts

The 3600x3600 fixtures introduced for the 3000px edge cap nearly doubled
the pixel area jimp has to decode and deflate, pushing the slowest
compression tests past the 5s vitest timeout on CI runners. 3600x1800
keeps every fixture over the cap while restoring roughly the workload of
the old 2600x2600 fixtures that CI handled comfortably.

* test: pin anti-aliased downscale quality with executable guards

A 1px checkerboard probe pins the compressor to full-coverage averaging
at integer and fractional ratios, with jimp's point-sampled BILINEAR
mode kept as the executable aliasing counter-example (it collapses the
50%-gray pattern to solid black at 4:1). Also guards the other classic
downscale bugs: transparent-pixel color bleed, mean-brightness drift,
iterative recompression degradation, and zero-size collapse on extreme
aspect ratios.

* fix(agent-core): report decoded EXIF-rotated dimensions in ReadMediaFile notes

The media note derived its original-dimensions line from the header
sniff, which reports pre-rotation values for EXIF orientation 5-8
JPEGs. The sent image and region readback both live in the decoded
(rotated) space, so portrait photos got axis-swapped coordinate
guidance. Once a decode has happened — compression or crop — its
dimensions now overwrite the sniffed ones.

* fix(agent-core): improve handling of EXIF orientation in image dimensions and metadata

* fix(agent-core): sniff EXIF orientation and step budget fallback through 2000px

Two follow-ups to the EXIF and 3000px-cap changes:

sniffImageDimensions now reads the JPEG EXIF Orientation tag (pure
header parse, both byte orders) and reports display-space dimensions
for orientations 5-8. Passthrough images — never decoded — previously
kept the pre-rotation header size in compression results and media
read notes, disagreeing with the decoded space that region readback
uses.

encodeWithinBudget steps the over-budget fallback through 2000px
before the 1000px last resort. Raising the cap to 3000px had left a
regression window: an image whose 2000px encode fits the byte budget
was sent at 1000px where the old 2000px cap used to send it at
2000px.

* fix(kimi-code): record pasted image dimensions in display space

The TUI paste path recorded attachment and original dimensions from its
raw header parser, which ignores EXIF orientation. For a portrait JPEG
the submit-time caption then contradicted the sent image's aspect and
region readback coordinates were axis-swapped. Dimensions now come from
the compression result, which reports display space on both the
compressed and passthrough paths; parseImageMeta remains only the
format/mime gate.

* feat(agent-core): add image compression and crop telemetry

Every image ingestion path now reports an image_compress event —
outcome (compressed / passthrough fast, guard, unsupported, unhelpful,
error), input/output formats, byte and pixel sizes, EXIF transposition,
and duration — and region readback reports an image_crop event with a
failure classification and the region's share of the original area.

Wiring is per call site via a new CompressImageOptions.telemetry
option, so the outcome split and timing are measured inside the
compressor while each caller only names its source: ReadMediaFile
(tool construction, like GrepTool), MCP tool results (McpOutputOptions),
server prompt ingestion (ICoreProcessService now exposes the host
telemetry client), ACP prompts (session track adapter), and TUI paste
(host.track adapter). Properties are numeric/enum only — never paths
or content — and a throwing client can never affect the compression
result.

* fix(agent-core): run the full JPEG quality ladder at fallback sizes

The fallback rescales encoded only at quality 20, so a JPEG whose
ladder failed at the fitted size collapsed straight to the lowest
quality even when the smaller size left budget headroom for a higher
rung (the realistic window is the 1000px step, where the 4x pixel
drop pays for q80/q60). Each fallback edge now walks the same
q80-to-q20 ladder as the fitted size.

* test: shrink heavy JPEG fixtures and add explicit timeouts

The fallback-ladder test runs ~11 pure-JS JPEG encodes and the EXIF
paste test decodes, rotates, and re-encodes a 6.5MP frame; both sat at
the edge of the 5s vitest timeout on CI runners. Narrower fixtures cut
the pixel area (the ladder test keeps its width above 2000px so the
full fallback chain still runs) and explicit 15s timeouts absorb runner
variance.

* fix(server): scope prompt image compression telemetry to the session

The prompt-ingestion image_compress events were emitted with the bare
host telemetry client, while every agent-side source inherits a
session-scoped client — so prompt_inline/prompt_file events could not
be correlated with their session. The route now wraps the client with
withTelemetryContext({ sessionId }) like rpc/core-impl does for
session telemetry.

* chore(changeset): consolidate image compression changesets

One entry covering the cap raise and the EXIF dimension fix, listed
for both the CLI and the SDK so the SDK changelog's compression
description (previously pinned at 2000px) stays accurate.

* fix: count goal creation turn (#1477)

* feat(kosong): support structured response formats (#1397)

* fix: clarify goal blocked audit guidance (#1481)

* feat(agent-core): discard loaded tool schemas on compaction (#1471)

Align progressive tool disclosure with the discard-on-compaction model:
compaction no longer rebuilds loaded dynamic tool schemas. The boundary
announcement re-lists every loadable name, the model re-selects what it
still needs, and a from-memory call to a no-longer-loaded tool is
rejected by preflight with select guidance.

This removes the keep-all rebuild and its half-trigger budget heuristics
entirely: the post-compaction floor is back to users + summary, which is
structurally outside the auto-compaction trigger band, and the guard
baseline degenerates to summary + reinjected reminders. Every downstream
mechanism already treated the empty loaded set as its consistent base
state (ledger scan, pending clear at the compaction boundary, deferred
extras, preflight wording), so this is a strict simplification.

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>

* fix(kimi-code): exit 1 when a headless (-p) turn fails (#1483)

Headless (`kimi -p`) failures could exit with code 0 when the event loop
drained during the shutdown cleanup (e.g. telemetry's unref'd retry backoff
when the network is blocked), because the rejection never reached the
process.exit(1) call. Set the failure exit code before any await in both
the run-prompt catch and the main catch, and keep the cleanup timeout ref'd
so the loop stays alive long enough for the rejection to propagate.

* feat(plugins): add Vercel plugin to marketplace (#1489)

* feat(web): support Enter key to confirm archive and other dialogs (#1490)

* feat(web): redesign cron reminder as a message bubble (#1480)

* feat(web): redesign cron reminder as a message bubble

Restyle the cron trigger notice as a right-aligned user-style message bubble that shows the scheduled prompt in full (wrapping across lines), with a small meta row beneath it for the schedule, status, job id and run time. Extract a shared MessageTime component used by both user messages and the cron reminder so the timestamp format and click-to-expand behavior stay consistent, and give the CronCreate/CronList/CronDelete tools distinct calendar icons.

* refactor(web): render cron reminders only as standalone turns

Remove the embedded cron block path from the web transcript projector so cron reminder fires always render through the standalone right-aligned bubble path.

* chore(web): simplify cron redesign changeset

* fix(web): composer model switch also updates global default model (#1491)

* fix(web): composer model switch also updates global default model

The composer model switcher still switches the active session's model via
POST /sessions/{id}/profile (awaited, so the model pill reflects the result),
and additionally fires POST /api/v1/config with { default_model } as a
fire-and-forget side effect so new sessions inherit the chosen default. The
config request is skipped when the model already matches the current default.

* fix(web): route ModelPicker overlay selection through the default-model update

The overlay opened from the composer's "More models" row (and /model) is a
continuation of the same switch flow, so its selection now also bumps the
global default model instead of only switching the active session.

* fix(web): only persist the default model after a confirmed session switch

setModel now returns whether the switch was accepted (true for the draft
path), so the composer flow no longer writes a stale or invalid model alias
into the global config when the session-level switch failed and rolled back.

* feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492)

* ci: release packages (#1468)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: surface provider auth error for unavailable models (#1506)

* fix: surface provider auth error for unavailable models

When an OAuth-managed model returns 401 after a forced token refresh, the token is valid but the provider rejected it for that model (the account lacks access). Emit provider.auth_error carrying the provider's message instead of auth.login_required with a misleading "OAuth login expired. Send /login" prompt.

* fix(agent-core): preserve provider auth errors through compaction

Treat provider.auth_error like auth.login_required in the compaction path so an auth rejection during compaction surfaces the provider's message instead of being wrapped as a generic compaction failure.

* ci: release packages (#1507)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* docs(changelog): sync 0.23.3 and shorten OAuth error entry (#1509)

* feat(kimi-web): add status-aware browser notifications (#1479)

* feat(kimi-web): add approval notification storage key and i18n copy

* feat(kimi-web): add approval notification helpers and tests

* feat(kimi-web): wire approval notifications and guard completion alerts

* fix(kimi-web): extract shouldNotifyCompletion helper and add tests

* feat(kimi-web): add approval notification settings toggle

* chore(kimi-web): add changeset and tidy notification module comment

- Align approval notification tag with spec (kimi-approval-${approvalId})

- Update module header to describe all three notification kinds

* fix(kimi-web): make notifications fire reliably

- Key completion notification tags by turn (sid + promptId) and question
  tags by request id, so a stale notification left in the notification
  center no longer swallows every follow-up alert in the same session
- Suppress notifications only while the window is actually focused, not
  merely visible (document.hasFocus() on top of visibilityState)
- Play the attention sound when a tool needs approval, matching the
  completion and question sounds

* chore(kimi-web): simplify changeset

* fix(agent-core-v2): serialize concurrent model catalog refreshes

Port v1 #1207's _refreshChain so a scheduled refresh and a manual one (or two overlapping manual ones) never race on reading/patching the persisted config.

Applied to both refresh entry points: ModelCatalogService.refreshProviderModels (scheduler + all/single-provider) and OAuthService.refreshOAuthProviderModels (OAuth-only, a separate service in v2).

* fix(agent-core-v2): dedupe workspace registry entries by root

Port v1 #1221: collapse registered workspaces that share a root in list(), preferring the entry whose id matches the current canonical encodeWorkDirKey, so a legacy workspaces.json (v1-compatible) does not render the same folder twice through GET /workspaces.

* fix(agent-core-v2): apply KIMI_CODE_CUSTOM_HEADERS and host identity headers

Port v1's provider-manager outbound header logic to agent-core-v2 so
`KIMI_CODE_CUSTOM_HEADERS` and host identity headers are applied to
outbound LLM requests, closing the migration gap from #1186:

- env `KIMI_CODE_CUSTOM_HEADERS` is the lowest-precedence header layer;
- host identity headers (User-Agent + X-Msh-*) are sent for Kimi
  providers, only the User-Agent for every other provider — a Kimi
  provider routed through the Anthropic protocol still gets the full
  set, matching v1;
- provider `customHeaders` always win on conflict.

Host headers are seeded by the CLI via `createKimiDefaultHeaders` and a
new `IHostRequestHeaders` App-scope token (defaulting to empty), so the
model resolver can layer them without the host threading them through
every call site.

* chore(agent-core-review): rename skill from kc-review to agent-core-review

* feat(kap-server): surface originating stack trace on error envelopes

- add optional `stack` field to errEnvelope and the envelope schema/interface; omitted when undefined so the wire shape stays byte-identical for callers without a stack
- thread `err.stack` through route error mappers plus the global and transport error handlers
- preserve `details` on `session.undo_unavailable` while adding its stack
- update tests to assert stacks are surfaced, reversing the prior no-leak contract

* fix: align v2 media and task compaction handling

* feat(agent-core-v2): sync shell mode and skill config parity (#1514)

* feat(agent-core-v2): record shell command context

- add ShellCommandOrigin and compaction handoff disposition
- extract IAgentShellCommandService from AgentRPCService
- keep AgentRPCService as a thin shell:run facade

* fix(agent-core-v2): align skill priority and sync docs

- restore project > user > plugin > builtin skill precedence
- sync Skill tool description and parameter docs from v1
- update write-goal and custom-theme builtin skill copy

* fix(agent-core-v2): restore undo and thinking telemetry

- track conversation_undo after undoHistory
- emit thinking_toggle with enabled/effort/from payload
- add coverage for both telemetry events

* feat(agent-core-v2): add skill directory config

- add extraSkillDirs and mergeAllAvailableSkills config sections
- introduce extra skill source and shared source priorities
- align kap-server workspace skill preview with session catalog

* feat(agent-core-v2): support explicit skill dirs

- add ISkillCatalogRuntimeOptions for SDK-style explicit skill dirs
- suppress default user/project discovery when explicitDirs are set
- resolve explicit dirs per session workDir via explicitFileSkillSource

* fix(agent-core-v2): fix configured skill dir resolution

- expand ~ using OS home for configured skill dirs
- honor explicitDirs in kap-server workspace skill preview

* fix(agent-core-v2): await config ready before skill discovery

- wait for config.ready before reading extraSkillDirs
- wait for config.ready before reading mergeAllAvailableSkills
- cover extra skill dir loading behind config readiness

* fix(agent-core-v2): keep skill config live after changes

- await config.ready in kap-server workspace skill preview
- reload user and workspace skill sources when mergeAllAvailableSkills changes

* fix(agent-core-v2): align goal budget handling

* fix(agent-core-v2): forbid model goal pauses

* fix(agent-core-v2): cap detached process output

* fix(kimi-code): drain v2 print subagents before exit

* fix: restore kap-server video upload compatibility

* fix(agent-core-v2): charge only output tokens against goal token budgets

Goal parity gap G2: v1 charges only per-step output tokens against a
goal's tokenBudget, while v2 summed all four usage buckets (cache read,
cache creation, other input, output), exhausting budgets orders of
magnitude faster under prompt caching and skewing persisted tokensUsed
counters. Align goal token accounting to output-only and drop the
unused tokenUsageTotal helper.

* fix: align server-v2 media file handling

* feat(agent-core-v2): allow coder profile to use MCP tools

* refactor(agent-core): introduce activity kernel and migrate turn lane

- add `activity` domain: `IAgentActivityService` (Agent turn lane machine),
  `ISessionActivityKernel` (Session admission, PR1 placeholder), and the
  `ActivityLease` that owns the turn `AbortSignal`
- turnService launches and cancels through the kernel lease; `Turn` now
  exposes `signal` instead of `abortController`
- agentLifecycle.remove drives `beginDisposal`/`settled` and waits for the
  in-flight turn to drain before releasing the agent scope
- add `activity.*` error codes; deprecate `turn.agent_busy` in favor of
  `activity.agent_busy`

* refactor(sessionLegacy): remove fork/compact/abort/archive pass-throughs

These four legacy session actions were thin delegations to the native v2
services (ISessionLifecycleService.fork/archive,
IAgentFullCompactionService.begin, IAgentRPCService.cancel) with no v1-only
projection to centralize. Drop them from ISessionLegacyService and call the
native services directly from the kap-server sessions route. updateProfile,
createChild, listChildren, undo and status stay in the adapter since they
carry real v1 adaptation logic.

* refactor(cli): run print-mode v2 on native agent-core-v2 services

- add native v2 print runner (v2/run-v2-print.ts) that consumes agent-core-v2
  DI services and awaits Turn.result directly
- extract shared print-mode rendering into prompt-render.ts for v1 and v2
- remove the V2PromptHarness/V2Session shim and v2->v1 event translation
- decouple initializeCliTelemetry from PromptHarness (homeDir/auth/track)
- add IAgentPromptLegacyService.submitAndSettle for authoritative completion

* refactor(session): serve v1 undo and children via native v2 services

- make IAgentPromptService.undo throw session.undo_unavailable with a structured
  reason; move the precheck into contextMemory
- add ISessionLifecycleService.createChild (fork + child markers) and
  ISessionIndex.list({ childOf })
- slim ISessionLegacyService to updateProfile/status (drop createChild,
  listChildren, undo)
- rewire kap-server session routes to the native services and map
  SESSION_UNDO_UNAVAILABLE

* refactor(cli): drop v1 sdk and telemetry deps from v2 print

- run-v2-print: use core ITelemetryService + CloudAppender instead of
  kimi-telemetry; remove kimi-code-sdk import (auth via IOAuthToolkit,
  config path from bootstrap, hook result via structural type)
- prompt-render: replace SDK HookResultEvent with a structural type so
  the shared renderer does not depend on the v1 SDK event shape
- telemetry: revert initializeCliTelemetry to its original signature now
  that v2 no longer calls it; keep v1 callers and assertions untouched
- update run-prompt and v2-run-print tests for the new wiring

* feat(activity): add session lane machine and agent snapshot projector

- implement SessionActivityKernel lane machine (restoring→active⇄quiescing→closing→disposed) with admission table, atomic quiesce+drain, beginClosing/settled, markActive
- start AgentActivityService lane at initializing; add markReady driven by agentLifecycle.create after bootstrap
- project LaneModel + EventBus facts into structured AgentActivitySnapshot (ActivityModel / setActivitySnapshot Op) with pending-approval and active-tool-call sets; emit agent.activity.updated
- add IAgentTurnService.launchWithLease; goal continuation acquires the lane before appending its prompt
- resolve pending interactions on turn.ended to avoid stranded awaiting_approval
- fullCompaction registers a background activity and checks the activity lane
- extract contextMemory publishSplice / isFullyUndoable / recoverFoldedLength helpers
- kap-server: map activity snapshot into legacy status and sessionEventBroadcaster

* fix(agent-core-v2): truncate over-long goal completion criteria

Goal parity gap G11: v1 silently truncates a goal's completionCriterion
to 4000 characters (the objective cap) before persisting, so an
over-long criterion never fails creation and cannot bloat every goal
reminder and record. v2 only trimmed whitespace and persisted arbitrary
lengths verbatim. Cap the normalized criterion at
MAX_GOAL_COMPLETION_CRITERION_LENGTH to match v1.

* fix(agent-core-v2): add goal error catalog info metadata

Align the GoalErrors domain with V1 by attaching the info block for the
seven goal.* error codes (title, retryable, public, action hints) so
errorInfo() surfaces them. Entries copied verbatim from the V1 error
catalog.

Gap: G43

* chore(nix): update pnpm deps hash

* fix(agent-core-v2): retain queued steers when a turn ends cancelled or failed

Align the prompt layer with V1's steer-buffer semantics: buffered steer
input now survives a turn that ends cancelled or failed and is flushed
into the next launched turn by the existing beforeStep hook, instead of
being silently dropped. The turn-result observation in the prompt
service existed only to perform that discard, so it is removed along
with the now-trivial launch wrapper; explicit clear() still discards
the queue.

Gap: G24

* fix(agent-core-v2): remove ask-user background mode

* fix(kap-server): align archived session restore

* chore(lint): fix type-aware lint errors

* chore(agent-core-v2): drop stray doResume debug log

* fix(agent-core-v2): defer prompts and steers while a full compaction is in flight

Align with V1's compaction gating: input arriving while a full compaction
holds the context (and no turn is active) used to launch a turn
immediately, appending assistant output that forced the in-flight
compaction to cancel. The prompt service now buffers such input and
replays it from a new onDidFinishCompaction hook that the compaction
worker runs in a finally, so the buffer drains on completion,
cancellation, and failure alike — the first deferred item launches a
turn and the rest join the steer queue.

The compaction service is resolved lazily instead of constructor-
injected: materializing it during prompt-service construction reorders
loop-hook registration and moves the full-compaction beforeStep hook
ahead of the hooks that let a freshly launched prompt land in context
before the auto-compaction check snapshots history.

Gap: G23

* fix(agent-core-v2): re-inject the goal reminder after full compaction

Align with V1: after a compaction rewrites the context, re-arm the
per-turn context injectors and run them before the compaction is marked
complete, so the first post-compaction request — including a replayed
deferred prompt's — already carries the goal reminder the summary
folded away. The injector service exposes injectAfterCompaction, which
re-arms the new-turn flag and injects immediately; the compaction
worker calls it after the system-prompt refresh and raises the
post-compaction token floor to include the re-injected reminders (the
pre-injection floor stays as the fallback when reinjection throws), so
the nothing-new-since-compaction guard does not re-trigger against a
shape that cannot shrink.

The injector is resolved lazily from the compaction service to keep
loop-hook registration order untouched across the dependency cascade.

Matches V1 verbatim including the existing quirk where an idle manual
compact yields a second reminder copy on the next turn's per-turn
injection; the parity test pins that behavior.

Gap: G14

* test(agent-core-v2): cover goal pause classification for provider errors

Port the missing end-to-end coverage: goal-driven turn failures pause
the goal with the exact per-class reason strings — provider rate limit,
provider connection error, provider authentication error, provider
safety policy block, and model configuration error (including the
forced 'LLM not set' substitution). Failures are driven through a real
turn with a throwing generate stub so the raw-error classification
feeding the pause reason is exercised, not just the mapper.

No source changes: the existing classification already matches the
reference strings verbatim.

Gap: G35

* feat: add progressive tool disclosure

* feat(cli): gate print-mode v2 behind KIMI_MODEL_EXPERIMENT_FLAG

- add KIMI_PRINT_V2_ENV / isPrintV2Enabled so `kimi -p` routes to the
  native agent-core-v2 runner through its own switch
- keep `kimi server run` server-v2 routing on isKimiV2Enabled
  (KIMI_CODE_EXPERIMENTAL_FLAG), decoupling the two
- update print-mode tests and comments to reference the new switch

* feat(cli): add KIMI_MODEL_OUTPUT_FORMAT for print-mode default

- resolve the effective `-p` format via resolveOutputFormat: the
  --output-format flag wins, then KIMI_MODEL_OUTPUT_FORMAT (prompt mode
  only), then text
- ignore the env outside prompt mode and reject invalid values eagerly
  through the friendly validation path
- apply the resolver on both the v1 and v2 print runners

* fix(agent-core-v2): count the goal-creating turn as the first goal turn

Goal parity gap G5: when the model creates or resumes a goal mid-turn,
v1 counts that ordinary turn as goal turn 1 at turn end (with a budget
re-check before the continuation driver takes over) and charges its
remaining step output tokens against the token budget. v2 only flagged
turns whose goal was already active at launch, leaving turnsUsed and
tokensUsed off by one turn in the model-initiated flow. Adopt the live
turn as a goal starter turn on activation: charge its post-creation
step output, count it once at turn end via incrementTurn, and block
instead of launching a continuation when that count exhausts the turn
budget.

* fix(agent-core-v2): remove model-initiated paused status from UpdateGoal

Goal parity gap G6: v1 reserves pausing for the user and runtime — its
UpdateGoal tool only accepts active/complete/blocked and rejects other
statuses with an invalid-status error. v2 still carried a leftover
'paused' enum option, a model pauseGoal branch, and matching tool
description wording from before v1 removed them. Drop the paused
option, port v1's runtime invalid-status guard, and align the tool
description with v1's.

* fix(agent-core-v2): deliver goal outcome prompts through the UpdateGoal tool result

Goal parity gap G7: when the model completes or blocks a goal, v1
returns the outcome prompt (stats plus final-message instructions) as
the UpdateGoal tool result with stopTurn, keys the one-shot final-
message continuation on that terminal tool result, and guards it with
the per-turn step budget so a capped turn ends 'completed' instead of
dying on max steps. v2 still used a pre-change leftover channel: terse
tool outputs plus goal_completion_summary / goal_blocked_reason system
reminders and a last-message-reminder continuation with no step-budget
check. Return the outcome prompts as tool output, drop the reminder
appends and their detection, key the continuation on the terminal
UpdateGoal result observed via the tool executor hook, and mirror v1's
hasStepBudgetRemaining guard. Also closes audit gaps G17 (max-steps
death) and G27 (actor-conditional reminders).

* fix(agent-core-v2): fail UpdateGoal as a tool error when no goal matches

Goal parity gap G8 (with user modification): v1 returns friendly
success-flagged no-op outputs when UpdateGoal targets a missing or
non-active goal, while v2 either let GOAL_NOT_FOUND escape from
resumeGoal or reported false success with stopTurn for complete and
blocked on a non-active goal. Per the user's decision these cases now
return error-flagged tool results in the same shape as the Edit tool's
old-string-not-found failure - v1's message texts ('Goal not resumed:
no current goal.', 'Goal not completed: no active goal.', 'Goal not
blocked: no active goal.') with isError and no stopTurn, so the model
sees a non-fatal failure and the turn continues normally.

* fix(agent-core-v2): settle active goals when the continuation relaunch fails

Goal parity gap P-B: the turn-ended subscriber that relaunches goal
continuation turns discarded every rejection, so a failed launch (for
example losing a race to a queued prompt) stranded the goal in status
active with nothing driving it. Keep the event-driven per-turn
continuation model but settle deterministically on failure: any
rejection out of the turn-ended handling now pauses the active goal as
actor system with reason 'Paused after goal continuation failure:
<message>', emitting the normal goal.updated event; the settle itself
never throws into the event bus. The busy-skip needs no settle: the
turn service clears its active turn before publishing turn.ended, so
the other live turn's own end reliably re-runs the relaunch check.

* fix(agent-core-v2): restore the fork-cleared goal system reminder

Goal parity gap G12: after a session fork, v1 tells the model the fork
has no current goal so it ignores stale active-goal reminders copied
from the source session; v2 cleared the goal silently through the
forked wire op and dropped the reminder. Track the fork boundary in a
derived (never persisted) wire model folded on both dispatch and
replay: a forked record that clears a copied goal marks the reminder
pending, and the post-replay pass appends v1's verbatim reminder text
with origin goal_fork_cleared exactly once - the appended reminder
record acknowledges the pending flag on later replays, so resumes never
duplicate it. Forks of sessions without a goal append nothing. The
forkGoal op itself stays pure.

* fix(agent-core-v2): preserve turn result details (#1531)

* feat(agent-core-v2): align defaultProvider fallback and clear-on-delete

- ModelResolverService falls back to the top-level defaultProvider config
  when a model pins neither providerId/provider nor an inline baseUrl
  (v1 parity).
- ProviderService.delete clears defaultProvider when removing the provider
  it points at, replacing v1's scattered call-site cleanups.
- defaultProvider rides as an unregistered top-level scalar config section,
  mirroring defaultModel (no schema, generic snake/camel passthrough).

* feat(agent-core-v2): synthesize legacy prompt lifecycle events

- emit prompt.completed/aborted/steered on the per-agent IEventBus so the v1-compatible WS edge can forward them (v2 core only emits turn.ended)
- bridge per-agent turn.ended into SessionInteractionService.cancelPendingForTurn via AgentLifecycleService (bus is Agent-scoped, no direct injection)
- extract agent create() helpers: assertCanCreate, buildAgentScopeExtras, igniteEagerServices, bindBootstrap
- finish assistant writer on PromptTranscriptWriter.flushAssistant
- ungate live session status idle->running->idle e2e test (v2 backend pulls real status)

* chore(agent-core-v2): align cron and skill prompts with agent-core

Drop the KIMI_CRON_NO_JITTER / KIMI_CRON_NO_STALE notes from the
CronCreate and CronList descriptions and the MAX_SKILL_QUERY_DEPTH
sentence from the Skill description, matching agent-core (v1) where
these were trimmed from the model-facing text in #1102. Code behavior
is unchanged; the env bypasses and the depth cap still exist in both
implementations. ULID/8-hex wording is left as-is for now.

* chore(agent-core-v2): drop legacy 8-hex mention from cron prompts

CronDelete and CronList descriptions now describe the task id as a ULID
only, removing the "(or legacy 8-hex)" qualifier from the model-facing
text. Code and tests are unchanged.

* chore(agent-core-v2): reorganize tests to mirror src layout

Move every file under test/ so its path mirrors the corresponding file
under src/ one-to-one (agent/, session/, app/, os/, persistence/,
_base/, activity/, wire/), and rename test files to match the basename
of the source file they cover. Test-only infrastructure with no src
counterpart (harness, snapshot, lint, dep-graph, tools/fixtures) stays
at the top level.

Rewrite imports after the move: src references use the #/ alias, while
test-to-test and cross-package relative imports are recomputed for the
new locations. No behavior changes.

* feat(config): add default permission/plan mode and yolo alias

- register `defaultPermissionMode` and `defaultPlanMode` config sections
- apply `defaultPermissionMode` when creating the main agent
- enter plan mode on fresh sessions when `defaultPlanMode` is true
- fold `yolo: true` into `default_permission_mode` on kap-server config write, derive `yolo` on read (yolo stays wire sugar, never a persisted domain)

* feat(agent-core-v2): add background mode to AskUserQuestion

Align with v1: the model can pass `background: true` to get a task_id
immediately while the question waits in the background for the user's
answer; completion is delivered to the agent automatically through the
task service's terminal notification.

- New QuestionBackgroundTask (AgentTask kind 'question') that runs the
  question request on a detached task and settles completed/failed/killed.
- AskUserQuestionTool gains the optional `background` schema field, the
  description suffix, an IAgentTaskService dependency, and a background
  execution branch whose output block matches v1 verbatim.
- Tests: harness injects a task-service stub, two legacy 'no background'
  assertions are flipped to the v1-aligned behavior, and three background
  cases (immediate task_id, settle completed, abort killed) are added.

* fix(agent-core-v2): synthesize default baseUrl for env-model provider

Align with v1: when KIMI_MODEL_NAME is set without KIMI_MODEL_BASE_URL,
the reserved __kimi_env__ provider now gets a per-type default baseUrl
(kimi -> api.moonshot.ai/v1, openai -> api.openai.com/v1; anthropic is
left unset so the SDK picks its default). Previously v2 left baseUrl
empty and later threw "missing a base URL" for the openai env-model
path, regressing v1's out-of-the-box behavior. An explicit
KIMI_MODEL_BASE_URL still wins.

* fix(agent-core-v2): restore UpdateGoal completion/blocked prompts and no-goal fallbacks

Align with v1: completing or blocking a goal now returns the dynamic
summary/blocked-reason prompt (buildGoalCompletionSummaryPrompt /
buildGoalBlockedReasonPrompt, already present in outcome-prompts.ts but
unused) instead of the static "Goal marked complete/blocked." text, and
all three statuses report the "no active/current goal" fallback when
there is nothing to transition.

* feat(agent-core-v2): add [image] config section for image compression

- add media-owned `image` config section (`max_edge_px`, `read_byte_budget`)
  with `KIMI_IMAGE_MAX_EDGE_PX` / `KIMI_IMAGE_READ_BYTE_BUDGET` env bindings
  (env > config.toml > default)
- add Agent-scope `ImageConfigBridge` that pushes the env-resolved section
  into the image-compress resolver seam on load and on change, so all call
  sites honor config without per-call wiring
- resolve `maxEdge` / read-byte-budget defaults in image-compress via the new
  seam; keep the support module config-agnostic
- apply the read-image byte budget in ReadMediaFile's default downscale path
  (previously fell back to the 3.75 MB provider ceiling)

* fix(agent-core-v2): align image compression defaults with v1 (#1508)

Port v1 #1508 into v2: lower the longest-edge downscale cap back to
2000px (v2 was stuck on the 3000px it had ported from an earlier v1
change) and make it overridable, add the 256 KB read-image byte budget
used by ReadMediaFile, and widen the over-budget fallback ladder to
[2000, 1000, 768, 512, 384, 256].

- MAX_IMAGE_EDGE_PX 3000 -> 2000, with KIMI_IMAGE_MAX_EDGE_PX env and a
  config-pushed value resolved via resolveMaxImageEdgePx.
- READ_IMAGE_BYTE_BUDGET=256KB with KIMI_IMAGE_READ_BYTE_BUDGET env and
  resolveReadImageByteBudget; ReadMediaFile's default compress path now
  uses it (region / full_resolution still honor IMAGE_BYTE_BUDGET).
- Test expectations that hard-coded 3000px / 1500px updated to 2000 /
  1000 to match the v1 behavior.

The [image] config.toml section is intentionally not added: the env
vars already cover the override path, and wiring a config section plus
a runtime push would add v2-specific scaffolding beyond v1.

* fix(agent-core-v2): restore HEIC/HEIF conversion guidance in ReadMediaFile

Align with v1: a HEIC/HEIF read is now refused up front with an
os-specific conversion command (sips / heif-convert / ImageMagick) so the
unsupported format never reaches the provider (which would reject the
whole session once it lands in history). The two guidance builders are
ported verbatim from v1, and the check sits after the image-capability
guard (using IHostEnvironment.osKind).

Also give the existing EXIF-rotation test a longer timeout: it does
heavy jimp encode/decode and was flaking around the default 5s boundary.

* feat(agent-core-v2): wire startBtw into the agent RPC API

Align with v1: expose `startBtw` on AgentAPI and delegate it to the
existing ISessionBtwService (already implemented and DI-registered as
SessionBtwService), so the /btw slash command can fork a side-question
child agent once the server runs on v2.

* chore(agent-core-v2): tidy misplaced and throwaway test files

- Remove resume-debug.test.ts: a one-off diagnosis script with a
  hardcoded local path, not a real test.
- Move streamTiming.test.ts to app/model/modelImpl.test.ts: it only
  exercises buildStreamTiming in app/model/modelImpl.ts.
- Rename wire/store.test.ts to wire/wireServiceImpl.test.ts to match the
  module it covers (there is no store.ts in src).

* fix(agent-core-v2): restore JSON Schema format validation in tool-args

Align with v1: replace the hand-rolled subset validator with v1's Ajv-
based implementation (draft-07/2019/2020 + ajv-formats), so tool-call
argument validation once again honors the JSON Schema `format` keyword
(and the full keyword set), not just the previously hard-coded subset.

- args-validator.ts is now byte-identical to v1 (93 lines, replacing the
  289-line hand-rolled subset).
- Adds ajv@^8.18.0 and ajv-formats@^3.0.1 (same versions as v1) plus the
  pnpm-lock.yaml update.
- The two call sites (compileToolArgsValidator -> validateToolArgs) keep
  working unchanged; a small test locks in format / required /
  additionalProperties / subset behavior.

* fix(agent-core-v2): restore JSON Schema format validation in tool-args

Align with v1: replace the hand-rolled subset validator with v1's Ajv-
based implementation (draft-07/2019/2020 + ajv-formats), so tool-call
argument validation once again honors the JSON Schema `format` keyword
(and the full keyword set), not just the previously hard-coded subset.

- args-validator.ts is now byte-identical to v1 (93 lines, replacing the
  289-line hand-rolled subset).
- Adds ajv@^8.18.0 and ajv-formats@^3.0.1 (same versions as v1) plus the
  pnpm-lock.yaml update.
- The two call sites (compileToolArgsValidator -> validateToolArgs) keep
  working unchanged; a small test locks in format / required /
  additionalProperties / subset behavior.

* chore(agent-core-v2): drop legacy 8-hex mention from CronDelete/CronList tool source

Match the prompt change in 5cc8e520f: the CronDelete parameter
description and invalid-id error now say "ULID" only (not
"ULID or legacy 8-hex"), and the CronList id doc comment likewise.
The validation regex is unchanged so loading any legacy 8-hex tasks
from disk still works.

* chore(agent-core-v2): remove resume-roundtrip test

It round-tripped restore over a hardcoded local dataset
(kimi-code-mini-bench/.vitest-results) that is not in the repo, so it
vacuously passed everywhere else. Removed at the original author's
request.

* test(agent-core-v2): raise timeout for slow image-compress invariant test

The fuzz-style invariant test now drives the full v1 fallback ladder
([2000, 1000, 768, 512, 384, 256]) for over-budget inputs, which takes
longer than the default 5s boundary in this environment. Give it 30s.

* chore(agent-core-v2): rename ambiguous test files

- agent/task/manager.test.ts -> taskManager.test.ts (no manager.ts in
  agent/task; disambiguate from taskService.test.ts).
- app/cron/persist.test.ts -> cronTaskPersistenceService.test.ts (mirrors
  the module it covers; agent/task/persist.test.ts mirrors
  agent/task/persist.ts, so it is left as-is).
- agent/contextMemory/message.test.ts -> message-history.test.ts (its
  describe is 'message history (IAgentContextMemoryService)'; the dir
  already names the domain).

* fix(agent-core-v2): preserve usage for aborted steps

* fix(agent-core-v2): resume-safe session reads and in-memory transcript

- sessionLifecycle: get/list no longer return a session whose cold resume is
  still in flight, so callers never observe a half-initialized handle; resume
  remains the way to await a fully restored handle
- messageLegacy: reduce the transcript from the main agent's in-memory wire
  journal instead of re-reading wire.jsonl; AgentWireRecordService now keeps
  the journal current with live dispatch so cold and live sessions both read a
  consistent, full transcript

* chore(agent-core-v2): drop stale micro-compaction references in comments

micro-compaction only exists in legacy agent-core; v2 has no such mechanism. Update two comments that still cited it:

- fullCompactionService: the real reason not to project here is that llmRequester already projects once.

- swarmService: context.spliced consumers no longer include micro-compaction bookkeeping.

* fix(agent-core-v2): report skill discovery diagnostics

* test(agent-core-v2): align plan mode parity fixtures

* fix: distinguish task output timeout from cancellation

* chore: fix test name

* fix(agent-core-v2): flush the wire persist queue before the record log

Since d5e1d76fc every wire append rides the async persist queue whenever
a blob service is registered, but AgentWireRecordService.flush() only
awaited the log store. Callers - including the session-close path -
could complete a flush while records were still in flight on the queue,
and record-order assertions in tests raced it. Await the wire service
flush, which drains the persist queue, before flushing the log.

* test(agent-core-v2): align the goal reminder boundary test with the continuation driver

The per-turn-boundary reminder test predates the goal continuation
driver and never passed: its second explicit prompt raced the
auto-launched continuation turn, which correctly holds the turn lane.
Treat the continuation turn as the second boundary and end it
deterministically by completing the goal through UpdateGoal, keeping
the once-per-boundary (never per-step) assertion.

* fix(agent-core-v2): stop goal turns gracefully when a hard budget is exhausted

Previously a goal whose hard budget was reached mid-turn was only
flipped to blocked while the turn kept running unbounded: the loop
continues unconditionally after a tool-calls step, and steer flushes or
Stop hooks could extend the turn indefinitely past the budget.

Now, when the over-budget step requested tool calls, a goal_budget_stop
system reminder is appended after the tool results telling the model the
goal is blocked (resumable via /goal resume), to stop immediately, that
further tool calls will be rejected, and to write a brief final status
message. The model gets exactly one grace step, during which tool calls
are answered with a soft rejection instead of executing. After the grace
step - or when the over-budget step had no pending tool calls - a new
AfterStepContext.stopTurn flag ends the turn, honored by the loop with
precedence over tool_calls and hook-set continue so nothing can extend
past the stop. The grace grant also respects maxStepsPerTurn so it can
never turn a budget stop into a max-steps turn failure.

Turn launch is gated as well: a prompt arriving while the active goal is
already over budget (e.g. after resuming an exhausted goal) blocks the
goal before the turn is marked goal-driven, so turnsUsed no longer
drifts, no spurious goal_continued telemetry fires, and the prompt runs
as an ordinary turn with the blocked-goal note injected.

This deliberately diverges from agent-core v1, which hard-stops with
zero grace and answers a prompt on an exhausted goal with a synthetic
model-less turn: budget overshoot is now bounded at one closing step in
exchange for consumed tool results and a user-facing wrap-up message.

* fix(agent-loop): stop looping on bare tool_calls signal

- remap tool_calls finishReason to 'other' when the provider emitted no tool call structure
- prevents re-issuing the model call until maxSteps on a bare tool_calls signal
- add loop test covering the v1 'unknown' turn-lifecycle behavior

* fix(kap-server): emit legacy background.task.* alias on /api/v1 ws

The v2 engine emits background-task lifecycle as `task.started` /
`task.terminated`, but v1 consumers (kimi-code TUI / `kimi -p`, node-sdk)
only handle `background.task.*` and silently dropped every task event when
talking to server-v2, while kimi-web handles the native spelling and has the
legacy one registered as known-but-unhandled.

Fan the legacy spelling out next to the native event in
SessionEventBroadcaster, reusing the same volatility so replay, journal and
the per-agent filter stay coherent between the two. kimi-web keeps the native
event and ignores the alias; the native /api/v2 stream is left unchanged.

Add a SessionEventBroadcaster test asserting both spellings are emitted.

* feat(agent-core-v2): port /init command from v1

- add Session-scope ISessionInitService that spawns the coder subagent,
  mirrors the run onto the main agent, reloads AGENTS.md and appends an
  init-variant system reminder, then flushes records
- add SESSION_INIT_FAILED error code and register the sessionInit domain in
  the layer map, package index and DI dependency graph
- drop the stale "flat (no subdirectories)" rule from the agent-core-dev skill

* feat(api-v2): add klient SDK and reflection-based channel registry

- replace per-method actionMap (resource:action) with a channel registry: each Service registers once by decorator id and all methods are invoked by reflection
- routes move from /api/v2/:sa to /api/v2/:service/:method across HTTP routes and the WS protocol
- add @moonshot-ai/klient: typed core/session/agent client over the HTTP channel that reuses agent-core-v2 service interfaces
- register klient in flake.nix and pnpm-lock; refresh kap-server tests, e2e, and the apiSurface snapshot

* refactor(agent-core-v2): drive turns by draining a StepRequest queue

- add StepRequest / StepRequestQueue: the loop drains one batch per step,
  folding mergeable requests (steers) into the driver's step; a step that
  ran tools enqueues a ContinuationStepRequest, a plain message enqueues
  none, so the turn completes when the queue empties
- replace AfterStepContext.continue with explicit enqueue; a failed step is
  retried by head-inserting its driver request
- move prompt's private steer queue onto the loop via PromptStepRequest /
  SteerStepRequest / RetryStepRequest, which materialize their context
  messages at pop time (image-compression captions reroute to reminders
  on materialization)
- goal and externalHooks orchestrate continuations by enqueueing requests
  instead of setting ctx.continue; a request's message only lands when the
  loop pops it, so skipped or aborted launches leave no orphan messages
- remove the now-unused CancellationError
- delete the reworked turn tests (turn.test.ts, turn-ready.test.ts) and the
  cron test suites

* refactor(agent-core-v2): translate provider errors at the model boundary

- add translateProviderError in app/protocol/errors and apply it once in
  ModelImpl.request: raw provider failures become coded KimiErrors with the
  raw error preserved as cause and HTTP fields in details; abort shapes pass
  through untouched
- move provider-error mapping out of _base/errors/serialize so _base no
  longer imports llmProtocol; toErrorPayload/fromErrorPayload now round-trip
  cause chains recursively, capped at depth 8
- move context.overflow from LoopErrors to ProtocolErrors (wire code
  unchanged); move LoopError and the max-steps helpers into loop/loop.ts
- consolidate isAbortError into _base/utils/abort, dropping the duplicates in
  retry, cloudTransport, and the question/subagent task tools
- add unwrapErrorCause; classify retryability and HTTP status on the
  unwrapped cause in llmRequester and full-compaction
- align task-notification tests with enqueue-only delivery: drain the loop
  queue with one turn and assert on task.notified instead of prompt steer
- protocol: add recursive cause to KimiErrorPayload with a lazy zod schema

* docs(agent-core-dev): add commit-align workflow, drop DI dependency map

- add commit-align.md subskill: triage one main-branch commit against v2
  (aligned / partial / missing / not-applicable) and link it from SKILL.md
- delete docs/di-scope-domains.puml and the rendered svg, and drop the
  keep-the-map-in-sync requirement from verify.md, align.md, commit-align.md,
  and packages/agent-core-v2/AGENTS.md

* refactor(agent-core-v2): move turn lifecycle into agent loop

- make loop admission, cancellation, and completion own turn execution
- extract step retry into a loop error recovery service
- preserve failed-step context and expose retry delay events

* refactor(agent-core-v2): centralize loop turn scheduling

- add queued turn and step lifecycle handles with explicit admission modes
- move continuation and retry scheduling behind the loop service
- consolidate legacy prompt scheduling into the prompt domain
- align kap-server routes and tests with the new loop contract

* feat(klient): add WebSocket transport for calls and event streams

- add WsSocket: persistent /api/v2/ws transport with hello handshake,
  heartbeat answers, per-call timeouts, and auto-reconnect that
  re-subscribes active listens; bearer token rides the
  kimi-code.bearer.<token> subprotocol for browser compatibility
- add WsKlient / WsChannel exposing core/session/agent scopes and
  listen(event, handler) over the shared socket
- add Klient#ws() lazy singleton with WebSocketImpl injection
- bind global fetch in HttpChannel to avoid "Illegal invocation" in browsers

* refactor(agent-core-v2): unify hook names to on{Will,Did}Xxx convention

- loop: beforeStep -> onWillBeginStep, afterStep -> onDidFinishStep
- toolExecutor: onWillExecuteTool -> onBeforeExecuteTool (ToolWillExecuteContext -> ToolBeforeExecuteContext)
- prompt: onWillSubmitPrompt -> onBeforeSubmitPrompt
- permissionMode: onChanged -> onDidChangeMode
- wireRecord: onRestoredRecord -> onDidRestoreRecord, onResumeEnded -> onDidFinishResume
- terminal: onData -> onProcessData, onExit -> onProcessExit

* feat(kap-server): synchronously refresh all providers before listing models

- GET /api/v1/models now awaits refreshProviderModels({ scope: 'all' })
  before returning the model list, so the response always reflects the
  latest provider model metadata
- refresh failures are logged and swallowed, falling back to the
  persisted catalog instead of failing the request

* refactor(agent-core-v2): convert one-way notification hooks to Events

Replace fire-and-forget OrderedHookSlot hooks with Emitter/Event-based
notifications for consumers that only observe, never intercept:

- usage: hooks.onDidRecord -> onDidRecord event
- permissionMode: hooks.onDidChangeMode -> onDidChangeMode event
- fullCompaction: hooks.onDidFinishCompaction -> onDidFinishCompaction event
- wireRecord: hooks.onDidFinishResume -> onDidFinishResume event
- agentLifecycle: hooks.onDidStopAgentTask -> onDidStopAgentTask event,
  announced via new notifyAgentTaskStopped() called by mirrorAgentRun

Interception-capable slots (onWillStartAgentTask, onWillCompact,
onDidRestoreRecord) stay as ordered hooks.

* fix(agent-core-v2): route FetchURL through the Moonshot fetch service when logged in

When the managed Kimi provider has an oauth ref, WebFetchService builds a MoonshotFetchURLProvider (bearer token + host identity headers) with the local fetcher as fallback, re-reading login state on every call; logged-out setups keep the local fetcher.

* fix(agent-core-v2): forward host identity headers with WebSearch requests

WebSearchProviderService now passes the host's IHostRequestHeaders (User-Agent + X-Msh-* device identity) as default headers to the Moonshot search provider, mirroring v1's kimiRequestHeaders.

* fix(cli): seed host identity headers into the experimental v2 server

The v2 boot path (kimi server run with the experimental flag) now seeds the CLI's Kimi identity headers (User-Agent + X-Msh-* device identity) into the engine through kap-server's seeds option, so outbound model, WebSearch, and FetchURL requests carry the same identity as direct CLI runs. kap-server's own package version is 0.0.0, so the identity has to come from the CLI.

* feat: validate workspace roots and auto-launch task notification turns

- task: idle terminal notifications now use activeOrNewTurn admission,
  launching their own turn instead of waiting for the next user prompt
  (matches v1 turn.steer)
- workspaceRegistry: createOrTouch rejects missing or non-directory roots
  with fs.path_not_found, so a phantom cwd never reaches session creation
- kap-server: map FS_PATH_NOT_FOUND to protocol error 40409 on the session
  and RPC surfaces
- server-e2e: migrate the v2 smoke test from the local ServerClient to the
  typed Klient
- misc: switch loop/prompt clear() iteration to .slice(), align terminal
  event handler names, and tidy klient examples

* fix(kap-server): emit idle/aborted session status on turn end

The v1 WS broadcaster only re-emitted event.session.status_changed(running)
on turn.started and never emitted the idle/aborted transition on turn.ended.
kimi-web treats that event as the single source of session status (its
turn.ended projector deliberately does not synthesize idle), so a session
stuck at 'running' after the turn finished — most visibly for background
tasks, where ISessionActivity keeps reporting non-idle while the detached
task lives and even a REST pull never corrected it.

Re-emit event.session.status_changed after turn.ended on the same dispatch
queue, mapping reason cancelled/failed/blocked to aborted and otherwise
idle (previous_status 'running'), matching v1's _computeStatus. Update the
broadcaster tests and harden the wsV1Resync test helper so non-matching
frames no longer strand a waiter's timeout.

* feat(ws): add Service event streaming with waitUntil handshake

- listen messages accept a service name; kap-server resolves the Service
  via resolveService and subscribes through its onUpperCase member
- add listen_result acknowledgement and per-listen error reporting
  (onDidListenError) so failed subscriptions surface to the client
- support onWill-style events: payload carries eventId/signal/waitUntil,
  the client replies with event_result and the server can event_cancel
- klient proxy maps onUpperCase members to channel.listen; WsChannel
  shares one remote subscription across first/last listeners
- channel.call now forwards the complete argument array

* feat(agent-core-v2): add typed telemetry event registry

- register business telemetry events with compile-time property contracts
- redact sensitive values and reject invalid cloud properties
- centralize cloud appender construction and version context

* feat(kap-server): add channel introspection endpoint

- add describeChannels() to channelRegistry: scope derived from the scoped
  DI registry, public methods/getters enumerated from the prototype chain
  (framework plumbing and events excluded)
- export ChannelDescriptor / ChannelMethodDescriptor from the contract
- serve GET /api/v2/channels so clients (kimi-inspect) can render a
  dynamic service browser without handwritten method lists
- cover with rpc test, e2e channel registry test, and API surface snapshot

* feat(kap-server): expose declared parameter names in channel descriptors

- add `params` field to ChannelMethodDescriptor, parsed from function source
- extract declared parameter list via Function#toString with paren-depth tracking
- cover param introspection in rpc and server-e2e channel registry tests

* fix: adapt v2 print runner and tests to enqueue prompt API

- run-v2-print: drive turns via IAgentPromptService.enqueue() and
  handle.launched; detect hook-blocked prompts via handle.completion;
  read the LoopRunResult type discriminator in formatNativeTurnFailure
- bootstrap stubs: add clientVersion required by IBootstrapService
- v2-run-print test: mock enqueue, stub IBootstrapService for
  createCloudAppender, add track2 to the telemetry stub
- node-sdk test: cover prompt.completed/aborted/steered in the
  exhaustive event switch

* feat(agent-core-v2): introduce graded error taxonomy for os, storage, and wire layers

- add `os.fs.*` codes with `HostFsError` and the `toHostFsError` boundary translator
- add `os.process.*`, `storage.*`, and `wire.*` domains with coded error classes
- register new codes in the protocol `KimiErrorCode` union
- translate raw OS and parse failures into domain codes across services, persistence backends, and kap-server transport
- rename `KimiError` to `Error2` in the v2 base errors
- remove obsolete kimi-csdk init example

* test(agent-core-v2): wait for MCP connectAll instead of a fixed tick

The MCP initial-connect assertion used a single setTimeout(0) tick, but
connectAll is gated on Promise.all([resolveSessionMcpConfig(...),
enabledMcpServers()]); the session-config side walks the real filesystem
(project-root search + mcp.json reads), which does not settle within one
macrotask under CI load. Use vi.waitFor so the test is robust on CI.

* fix(minidb): publish WAL value pointers only after the frame is durable

In valueMode 'disk' the write path installed a disk ValueLoc using the
predicted WAL offset before the frame's bytes were flushed (appendLoc
returns the offset synchronously; the writev lands on a later tick). Under
load a concurrent compaction snapshot could read a pointer past the WAL end
and fail with a short read. Apply the record as an in-memory ref first and
only publish the disk pointer after appended.done resolves, guarded against
WAL rotation and stale record seqs.

* fix(kap-server): report missing agents as agent.not_found

- resolveScope now throws Error2 for missing session/agent instead of
  returning undefined, distinguishing agent.not_found from session.not_found
- map AGENT_NOT_FOUND onto the session-not-found protocol envelope for v1
  parity

* refactor(cli): gate print-mode v2 on KIMI_CODE_EXPERIMENTAL_FLAG

- remove KIMI_PRINT_V2_ENV / isPrintV2Enabled and the dedicated
  KIMI_CODE_EXPERIMENT_FLAG switch
- route `kimi -p` to the agent-core-v2 runner via isKimiV2Enabled,
  the same master switch that gates server-v2

* fix(agent-core-v2): map hostFs/storage error codes at server boundaries

- unwrap the HostFsError cause before matching EISDIR in FileEditService,
  restoring the "is not a file" edit output broken by the error taxonomy
- map os.fs.* codes to the closest v1 wire codes in the kap-server fs
  route and /api/v2 transport instead of collapsing to INTERNAL_ERROR
- map storage.io_failed / storage.locked to PERSISTENCE_FAILURE in the
  /api/v2 transport

* fix(agent-core-v2): serialize config writes and reloads

A User-target set/replace mutates raw/rawSnake, awaits persist(), then
rebuilds effective, while a reload() replaces all three wholesale from disk.
Without serialization a reload whose file read resolves inside a write's
persist window (before the atomic rename lands) restores the stale pre-write
state, so the write's post-persist rebuild drops the just-written domain from
effective. This surfaced as POST /config responses missing the field they had
just written when the startup model-catalog refresh's reload() raced the
write. Run User-target writes and reloads through a promise chain so they can
no longer interleave.

* feat(agent-core-v2): align telemetry with the v1 wire format

- rename tool_call_dedupe_detected to tool_call_dedup_detected
- emit turn_ended on every turn end; add mode/provider/protocol tags
  and interrupt_reason to turn_started/turn_interrupted
- enrich api_error with alias, protocol tags, and input_tokens
- tag tool_call with dup_type via an executor-side map (avoids the
  executor/dedupe DI cycle)
- rename compaction usage fields to input_tokens/output_tokens
- add context_projection_repaired, session_started, and
  session_load_failed events

* feat(agent-core-v2): add lifecycle transition machine

- add guarded synchronous and asynchronous state transitions
- support commit, rollback, cleanup, and compensation actions
- cover transition conflicts, action ordering, and failure aggregation

* fix(agent-core-v2): harden plugin load, install, and update check paths

- Degrade plugin consumption reads to empty when installed.json fails to
  load, surface plugin.load_failed with a repair hint on management calls,
  and recover after an explicit reload; serialize the initial load and
  mutations so concurrent first callers share one load.
- Clean up zip temp dirs on every failure path, report the original
  source in zip/github manifest errors, and roll back to the previous
  managed copy when an install or persist fails.
- Restore managed Kimi endpoint env injection for stdio plugin MCP
  servers.
- Check plugin updates concurrently with per-repo failure isolation and
  10s timeouts, track branch installs by commit SHA, and stop false
  update reports for tag/SHA pins.
- Throw plugin.not_found from getPluginInfo and the manager's
  not-installed paths.
- Count plugin skills through the real skill discovery path.
- Re-sync context injection positions after silent wire replay so cold
  resumes do not duplicate injections, and fire plugin session-start
  reminders only when the plugin skill source finishes refreshing.

* fix(agent-core-v2): use the v2 coded error type for plugins

* fix(cli): align print session with background completion API

* fix(kap-server): stabilize catalog and session status updates

- keep model catalog reads free of provider refresh side effects
- broadcast deduplicated session lifecycle and interaction statuses
- cover catalog loops and global session status fan-out

* test: stabilize CI integration cleanup

---------

Co-authored-by: _Kerman <kermanx@qq.com>
Co-authored-by: 7Sageer <7sageer@djwcb.cn>
Co-authored-by: qer <wbxl2000@outlook.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kaiyi <me@kaiyi.cool>
Co-authored-by: Luyu Cheng <2239547+chengluyu@users.noreply.github.com>
Co-authored-by: STAR-QUAKE <99738745+starquakee@users.noreply.github.com>
Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-07-12 21:44:04 +08:00
qer
f3dd006ab4
docs: update subagent timeout docs for configurable timeout_ms (#1582) 2026-07-12 21:09:17 +08:00
qer
febe030244
docs(changelog): sync 0.23.6 from apps/kimi-code/CHANGELOG.md (#1581) 2026-07-12 21:06:40 +08:00
github-actions[bot]
b5c236d00f
ci: release packages (#1546)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-12 20:28:38 +08:00
qer
47cd5f14c5
chore: downgrade print-goal-cron-turns changeset to patch (#1578) 2026-07-12 20:25:24 +08:00
qer
6fc1deb453
fix(web): wide markdown tables scroll internally and break out on desktop (#1577)
* fix(web): scroll wide markdown tables inside their own wrapper

* feat(web): break wide markdown tables out of the reading column

* fix(web): sample the full TOC rail for wide-table occlusion

* refactor(web): use rect overlap for the TOC occlusion check
2026-07-12 20:08:57 +08:00
qer
b1942bd571
fix(web): keep the connecting splash and retry the first-load auth check (#1574)
* fix(web): retry the first-load auth check behind the connecting splash

* fix(web): fall back instead of retrying deterministic 4xx auth-check failures

* fix(web): show the connection error on the splash while retrying

* fix(web): keep the first quick auth-check failure silent on the splash

* chore: remove accidentally committed dist-web symlink

* fix(web): hold onboarding until first load settles; drop unsupported 4xx auth fallback
2026-07-12 19:54:21 +08:00
qer
3a7aad653f
fix(web): finish local prompt state from session snapshot after a reconnect (#1572) 2026-07-12 19:01:30 +08:00
qer
9d96b538bf
fix(web): scroll wide markdown tables inside their own wrapper (#1575) 2026-07-12 18:52:26 +08:00
qer
5a208cb041
fix(web): auto-enable default thinking effort when switching to an effort-capable model (#1475)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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
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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(web): auto-enable default thinking effort when switching to an effort-capable model

* chore: add changeset

* fix(web): preserve thinking off when reselecting current model
2026-07-12 18:44:39 +08:00
qer
f901b9e1da
fix(web): persist server access token across tabs and browser restarts (#1567)
* fix(web): persist server access token across tabs and browser restarts

The web UI kept the server bearer credential in sessionStorage, which is
tab-scoped and cleared on tab close, so users had to re-enter the token
for every new tab and after every mobile tab eviction. Mirror it to
localStorage instead: the token already lives on disk at
<KIMI_CODE_HOME>/server.token and rides in the launch URL fragment, so
browser-profile persistence does not materially widen exposure for this
local tool. Existing sessionStorage copies are migrated on first boot,
and a 401 (e.g. after `kimi server rotate-token`) still clears the
stored credential.

* fix(web): avoid clearing a fresh shared token from a stale tab

localStorage is shared across tabs, so the unconditional removal let a
tab holding a stale in-memory credential erase a newer token another
tab had just persisted (e.g. after `kimi server rotate-token` one tab
stores the fresh fragment token, then an older tab's delayed 401 wiped
it). Only clear the persisted copy when it still matches the rejected
credential this tab was using.

* test(web): fix type-aware lint findings in server-auth tests

Drop return-await on the dynamic import, brace the void arrow in the
toThrow assertion, and remove a redundant String() conversion — CI runs
oxlint --type-aware, which flags these where the plain local run does
not.

* fix(web): expire persisted server credentials after 7 days

* fix(web): clear legacy session token even when localStorage is blocked

setCredential ran persistCredential and the legacy sessionStorage
cleanup in one try, so a localStorage.setItem failure (private mode,
quota) skipped the cleanup: a stale session-scoped credential left
behind was re-migrated on the next reload and 401'd into another token
prompt. Split the session cleanup into its own best-effort block.
2026-07-12 18:36:55 +08:00
qer
1d3dba5683
fix(web): match current model by id in model picker dropdown (#1565)
Model names and display names can collide across providers, so the
composer dropdown's checkmark matched by name and lit up every
same-named entry. Resolve the current model through its unique id
everywhere (dropdown check, thinking controls, status resolution).
2026-07-12 14:47:47 +08:00
qer
d2c2c33f3e
feat(web): type absolute paths directly in the workspace picker (#1556)
* feat(web): type absolute paths directly in the workspace picker

The add-workspace dialog's fuzzy-search box now doubles as an absolute
path entry: input starting with "/" or "~" is validated live (missing
path / missing parent get specific errors plus prefix-matched
candidates), and a valid path live-follows the folder browser so the
existing "Open this folder" button submits it. Enter accepts the first
candidate or opens a valid path; Esc clears the box. The collapsed
paste-path section at the bottom is removed, and the degraded
(no-browse) mode reuses the same box with format-only validation.

* fix(web): recognize Windows absolute paths and gate Open button in path mode

Two review fixes on the workspace picker's path entry:

- PATH_LIKE now also matches Windows drive (C:\x, C:/x) and UNC
  (\\srv\x) forms, matching node:path.isAbsolute on the daemon side.
  Without this, Windows users in degraded (no-browse) mode had no way to
  submit a path at all. Parent-dir splitting and trailing-separator
  trimming are now separator-aware (drive roots are preserved).
- "Open this folder" is disabled while path mode has no validated target.
  Previously, after typing a valid prefix and then an invalid path, the
  button still submitted the stale followed prefix.

* fix(web): submit the typed lexical root when adding a workspace in path mode

fs:browse canonicalizes via realpath, but workspace/session ids are based
on the lexical root. For a symlinked cwd (/tmp/project -> /private/tmp/
project on macOS), live-follow stored the resolved target in currentPath
and "Open this folder" emitted it, so sessions under the typed cwd would
not group under the workspace. Keep the typed normalized path for the add
action and use the browse result only to populate the visible browser.

* fix(web): handle workspace path input edge cases
2026-07-12 14:35:25 +08:00
Haozhe
cc03816ee0
feat(oauth): parse support_efforts/default_effort in custom registry import (#1564)
* feat(oauth): parse support_efforts/default_effort in custom registry import

- parse support_efforts / default_effort from api.json model entries
- map them onto model aliases as supportEfforts / defaultEffort
- treat both fields as upstream-owned in CUSTOM_REGISTRY_MODEL_FIELDS so
  refreshes sync and stale values are dropped
2026-07-12 13:27:50 +08:00
qer
c98238699c
fix(web): avoid repeated session list scans in sidebar computeds (#1563) 2026-07-12 11:51:55 +08:00
Haozhe
faefad0e29
feat(agent-core): configurable subagent timeout with 2h default (#1562)
* feat(agent-core): make subagent timeout configurable and raise default to 2h

- add `[subagent] timeout_ms` config (env `KIMI_SUBAGENT_TIMEOUT_MS` overrides) to replace the hardcoded 30-minute cap for Agent / AgentSwarm subagents
- raise the default subagent timeout from 30 minutes to 2 hours
- thread the value through tool construction so foreground and background subagents use it, with the timeout message reflecting the effective value
2026-07-12 11:48:06 +08:00
qer
264525eb51
fix(web): prevent chat scroll yank while browsing history (#1553)
* fix(web): preserve scroll position while loading older messages

* fix(web): prevent chat scroll yank while browsing history

* fix(web): stop stale auto-follow writes

* fix(web): anchor long tool histories

* fix(web): stabilize scroll anchor cancellation
2026-07-12 11:35:58 +08:00
Haozhe
c6e02daf42
feat(background): add print_background_mode steer for multi-turn -p runs (#1497)
* feat(background): add print_background_mode with steer for multi-turn -p runs

- add `[background].print_background_mode` (`exit`/`drain`/`steer`) and
  `print_max_turns`; when unset, falls back to `keep_alive_on_exit = true`
  mapping to `drain`, preserving existing behavior
- core: `Session.handlePrintMainTurnCompleted()` returns `finish`/`continue`;
  in `steer` mode the run stays alive so a background-task completion
  `turn.steer`s the main agent into a new turn (matching background
  subagents), bounded by `print_wait_ceiling_s` and `print_max_turns`
- cli: print driver follows every main turn instead of only the first and
  defers `finish()` until the run quiesces or a limit is hit
- plumb `handlePrintMainTurnCompleted` through node-sdk RPC; docs + tests
2026-07-12 10:47:35 +08:00
Haozhe
2f97917bb5
feat(cli): keep kimi -p running while a goal is active or cron tasks are pending (#1555)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / lint (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (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 / Publish native release assets (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 / Desktop release artifact (push) Blocked by required conditions
A kimi -p run settled the moment the main agent's turn ended (end_turn),
so a goal created mid-run was cancelled during cleanup and a scheduled
cron task never fired in the same run.

- runPromptTurn now re-evaluates completion when the main agent goes idle
  and stays alive while a goal is still active (the goal driver runs the
  continuation turns) or while cron tasks with a future fire remain (their
  fire steers a fresh turn). A ref'd handle keeps the event loop alive
  during the wait since the cron scheduler tick is unref'd.
- a terminal goal.updated (e.g. the driver blocking a goal on a hard
  budget, which emits no further turn.ended) also re-evaluates so the run
  cannot hang.
- add getCronTasks RPC and Session.getCronTasks() so the print flow can
  enumerate pending cron tasks.
2026-07-11 21:16:27 +08:00
qer
37bb4b870e
fix(web): keep ReadMediaFile media rendering after session resume (#1552)
Tool-role messages reached the snapshot/messages REST projection with
their content flattened to text, dropping image/video/audio parts, so a
ReadMediaFile result rendered as an image while streaming but fell back
to a generic tool card after a reload. Pass the raw content parts
through when a tool result carries media, matching the live tool.result
event shape the web client already parses.
2026-07-11 21:11:29 +08:00
qer
f17a6ecb52
fix(agent-core): treat dismissed AskUserQuestion as no answer, not recommended pick (#1550)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Release / Release (push) Waiting to run
2026-07-11 16:01:13 +08:00
qer
19c5aa64eb
fix: update the WebBridge install link in the /plugins panel (#1547)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
Release / Desktop release artifact (push) Blocked by required conditions
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
* fix: update the WebBridge install link in the /plugins panel

* fix: drop the zh-cn segment from the WebBridge link
2026-07-10 23:33:34 +08:00
qer
c2f27b2a22
docs(changelog): sync 0.23.5 from apps/kimi-code/CHANGELOG.md (#1545)
* docs(changelog): sync 0.23.5 from apps/kimi-code/CHANGELOG.md

* chore: update config model doc

* docs: update config-files example with new models and services

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-07-10 22:24:19 +08:00
STAR-QUAKE
7bd29ab011
refactor(kosong): rename select_tools capability to dynamically_loaded_tools (#1488)
* refactor(kosong): rename select_tools capability to dynamically_loaded_tools

Rename the `ModelCapability` bit from `select_tools` to `dynamically_loaded_tools` everywhere it is declared, detected, catalogued, and forwarded: kosong `ModelCapability`/catalog, agent-core capability resolution and the `toolSelectEnabled` gate, the SDK catalog-to-alias mapping, and the built-in catalog pruner's keep list.

The old `select_tools` spelling is removed outright rather than kept as an alias — no catalogued model or shipped configuration used the capability, so there is nothing to migrate. Client-side vocabulary (the `select_tools` builtin tool and the `tool-select` experimental flag) is intentionally untouched.

* chore: shorten changeset description

---------

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-10 22:12:54 +08:00
github-actions[bot]
352a449240
ci: release packages (#1533)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-10 21:44:24 +08:00
qer
f80b2eaf04
fix(kimi-web): single-source session status to stop duplicate turn-end notifications (#1542)
The web client received two sessionStatusChanged events per turn
transition: one projected client-side from the raw turn.started/turn.ended
stream, one mapped from the daemon's event.session.status_changed. After
the tag scheme in #1479 keyed the completion notification by prompt id,
the second (redundant) idle event lost the cached prompt id and fell back
to a Date.now() tag, so every turn end popped a second "Turn finished"
notification and replayed the completion sound.

Stop projecting sessionStatusChanged from the raw turn stream (turn.started,
turn.ended, and the in-flight snapshot seed). The daemon's
event.session.status_changed is the single source of status transitions:
it is computed from live daemon state (covering awaiting-approval /
awaiting-question / aborted), carries the authoritative previousStatus and
currentPromptId, and is deduped per real transition server-side. The turn
stream keeps its content responsibilities (message finalization, usage,
duration); seedInFlight keeps seeding the partially-streamed message while
status comes from the snapshot's authoritative session record.
2026-07-10 21:18:08 +08:00
Kai
db61c9e2dd
fix: refuse unsupported image formats instead of poisoning sessions (#1536)
* fix: refuse unsupported image formats instead of poisoning sessions

Images in formats providers reject (AVIF, HEIC, BMP, TIFF, ICO) used to
pass through to the API, and the resulting HTTP 400 repeated on every
later turn because the image_url stayed in the session history.

Add a single format policy (accepted set: PNG/JPEG/GIF/WebP) enforced at
every ingestion point: ReadMediaFile refuses with a per-OS conversion
command; MCP tool results, REST uploads, and ACP prompts replace the
image with a text notice; and turn.prompt/steer gates as the last-funnel
backstop so the SDK/RPC path cannot poison a session either. Accepted
MIME aliases (image/jpg, case/whitespace) are forwarded in canonical
form, and data URLs carrying MIME parameters can no longer slip past the
gate. Remote image URLs pass through (no bytes to inspect).

* fix: canonicalize accepted data URLs with MIME parameters

The format gate compared only the MIME token when deciding whether to
rebuild a data URL, so an accepted image carrying MIME parameters
(`data:image/jpeg;charset=utf-8;base64,...`) was forwarded with its
original header. The Anthropic provider splits the data URL and
exact-matches the full header against its whitelist, so the part still
poisoned the session. Rebuild to the byte-exact canonical URL whenever
the original differs, covering aliases, case/whitespace, and parameters
with one comparison.

Addresses review feedback on PR #1536.

* fix: parse data URLs case-insensitively in the image format gate

An uppercase `;BASE64,` marker is legal (RFC 2045 encoding names are
case-insensitive), but the parser required a lowercase match and
returned null, so the gate treated the URL as remote and forwarded it:
an unsupported image could still land in the session history, and the
Anthropic provider's lowercase-only split then threw on every turn.
Match the scheme and marker case-insensitively; the canonical rebuild
emits the lowercase form.

Addresses review feedback on PR #1536.

* fix: harden image format handling against mislabeled and legacy images

Two more ways an unsupported image could reach the provider are closed:

- Bytes, not labels, decide the format. A data-URL image whose declared
  MIME disagrees with its magic bytes (e.g. AVIF bytes an image search
  tool labels image/png) is now gated on the sniffed format at every entry
  point (MCP results, ACP, SDK/RPC prompt, REST inline and file uploads),
  so a mislabel cannot slip past the gate.
- A poisoned image already in the session history no longer kills the
  session: a server image-format 400 (or kosong's client-side image
  rejection) now retries once with every media part replaced by a text
  marker, mirroring the 413 media-degraded recovery. The recovery also
  fires during compaction, and the transient-retry fallback no longer
  burns the retry budget on image-format errors before the dedicated
  recovery can run.

* fix: reject remote image URLs ending in an unsupported extension

Remote image URLs (MCP resource_link, REST `kind: 'url'`) carry no bytes
to sniff, so a link ending in `.avif` (or `.heic`, `.bmp`, `.tiff`,
`.ico`) would pass through and be fetched server-side — and rejected.
Reject such URLs by their path extension instead (query/fragment
ignored, case-insensitive); extensionless or accepted-extension URLs
still pass through to the provider and the 400 recovery.

* fix: tighten image format handling for parameterized MIMEs and recovery scope

Address two review findings on PR #1536:

- A declared media type with parameters (e.g. image/jpeg; charset=utf-8)
  is no longer misread as unsupported: normalizeImageMime now strips
  parameters, matching the data-URL parser, so an accepted image with
  parameters is forwarded instead of dropped.
- The image-format recovery predicate is narrowed to specific
  format/data rejection phrases, so a 400 about image count, size, or
  image-input support no longer triggers a media-stripped resend that
  would let the model answer blind to the user's images.

* fix

* fix: scope image format recovery to images and flag remote SVG URLs

- The media_type/mime_type recovery match now requires the message to
  mention an image, so a video/audio media_type rejection surfaces
  instead of triggering a blind media-stripped resend.
- unsupportedImageMimeFromUrl flags .svg URLs as image/svg+xml without
  touching the shared suffix map (SVG stays text for the file tools),
  so remote SVG images get the intended notice instead of a provider
  rejection.

Addresses review feedback on PR #1536.

* fix: reject remote MCP images by their declared MIME type

An MCP resource_link with an extensionless or signed URL gives the
extension gate nothing to work with, and convertMCPContentBlock was
discarding the declared mimeType — an honestly-declared AVIF/HEIC link
from an image search tool still became an image_url and poisoned the
session. Reject on the declared MIME when the server provides one:
unsupported declarations become a text notice that keeps the URL so the
model can fetch and convert it; accepted declarations pass through as
before.

Addresses review feedback on PR #1536.

* fix: keep image format recovery image-specific and preserve dropped URLs in notices

- Drop the bare `media` alternative from the image-format recovery
  patterns so audio/video media rejections ("unsupported media type",
  "invalid media type") can never be misclassified as image errors and
  blindly media-stripped; every pattern now mentions "image" literally.
- Remote image URLs rejected by their extension now keep the URL in the
  replacement notice (gateImageFormatParts and the REST url path), so the
  model can still fetch and convert the image — matching the declared-MIME
  resource_link path.

Addresses review feedback on PR #1536.

* fix: drop malformed data URLs at ingestion instead of letting them poison the session

A `data:` URL that fails to parse (missing `;base64,` separator, empty
MIME, …) was treated like a remote URL and passed through the format
gate; the provider then rejects it on every turn, and the read-side
media-stripped recovery keeps paying that round-trip until compaction.
Detect unparseable `data:` URLs in gateImageFormatParts and replace them
with a (truncated) notice at ingestion, covering the MCP/ACP/SDK/turn
paths that share the gate.

Addresses review feedback on PR #1536.
2026-07-10 19:36:00 +08:00
qer
04041eb998
fix(web): hide injected system asides in user message bubbles (#1535)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(web): hide injected system asides in user message bubbles

* fix(web): preserve literal <system> tags in user prompts

* chore: fold duplicate web changeset into caption-hiding entry
2026-07-10 17:02:08 +08:00
Haozhe
9f66ec416c
feat: harden LLM API fault tolerance against 429 and overload (#1530)
* feat(retry): harden LLM API fault tolerance against 429/overload

- retry more transient errors: 408/409/429/5xx/529, an embedded upstream
  status_code=429 in OpenAI Responses stream errors, and unclassified
  provider errors as a last-resort fallback
- honor server Retry-After (parsed into APIStatusError.retryAfterMs by the
  OpenAI and Anthropic providers); chatWithRetry prefers it over its backoff
- align app-level backoff with claude-code (500ms base, 32s cap, factor 2,
  up to 25% jitter) so high-attempt configs ride out multi-minute overload
- emit a turn.step.retrying meta line in -p --output-format stream-json
2026-07-10 15:47:12 +08:00
qer
0ad568436a
docs(changelog): sync 0.23.4 from apps/kimi-code/CHANGELOG.md (#1528)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-10 00:48:47 +08:00
qer
5a90d9d7f1
chore(agents): place Polish before Bug Fixes in changelog sections (#1527) 2026-07-10 00:08:25 +08:00
qer
7ce3c2dc31
fix(ci): pin npm to 11.x in release workflow (#1526) 2026-07-10 00:08:20 +08:00
github-actions[bot]
9f9324cdab
ci: release packages (#1513)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-09 23:45:47 +08:00
qer
ec8dc3456c
fix(web): stop sending prompts into a busy turn on the web UI (#1522)
* fix(web): prevent duplicate first prompts and keep goal drives from looking idle

- Guard startSessionAndSendPrompt with a per-workspace reentry lock so a
  double-click / repeated Enter during draft-session creation cannot fire
  two concurrent first prompts into the same new session.
- Track goal.active in the agent event projector so turn.ended between
  goal-driven continuation turns keeps the session 'running' instead of
  projecting a false 'idle' that drains the local queue into a still-busy
  core (turn.agent_busy).
- Show a 'starting conversation…' loading state on the empty-session
  landing while the first prompt is being created and submitted.
- Persist the resolved model in startSessionAndActivateSkill so the first
  skill turn on a fresh session does not fail with 'Model not set'.

* chore: add changeset for web first-prompt fixes

* fix(web): close remaining first-prompt and goal-settle gaps

- Pass the starting guard through the dock composer: draft-session
  creation selects the new session before submit, which swaps the empty
  composer for the dock; disabling both composers closes the last path
  to a concurrent first POST. Also take the workspace lock in
  startSessionAndActivateSkill / startSessionAndOpenSideChat.
- Emit the owed idle when a goal settles (blocked/paused/completed) in
  the inter-turn gap after a turn.ended was projected as 'running', so
  sending state, in-flight flags and queued prompts flush instead of
  the session staying 'running' forever.

* style(web): fix eqeqeq lint error in first-prompt guard

* fix(web): clear owed idle when a new goal turn starts

The idle debt from a 'running' projection survived turn.started, so an
UpdateGoal('complete'|'blocked') landing mid-turn in the NEXT goal turn
synthesized an early idle. onSessionIdle could then drain queued prompts
into a core that was mid-turn again, re-opening the turn.agent_busy race
for multi-turn goals. Clear the debt on turn.started: from that point the
turn's own turn.ended carries the idle with goalActive already false.

* fix(web): make first-prompt starting state workspace-id-agnostic

isStartingFirstPrompt now reads from the lock set directly (size > 0)
instead of the current activeWorkspaceId. createDraftSession can swap
activeWorkspaceId to a registered id mid-flight; a workspace-keyed read
would then return false while the first prompt is still in the create/
select/submit window, re-enabling the composer and reopening the
duplicate first-submit race.

* revert(web): drop goal-aware idle projection from agentEventProjector

The goalActive / idleOwed shadow state machine grew through multiple
review rounds and still leaves edge cases (snapshot-seeded turns, mid-
turn goal updates). Roll it back to the simple 'turn.ended projects idle'
behavior. Goal-driven sessions can once again race a queued prompt into a
busy core; this is accepted as a known limitation to be resolved properly
in a follow-up that has the core emit an authoritative idle signal.

* chore: align changeset with actual fix scope

* test(web): update profile-patch expectation for model field
2026-07-09 23:37:10 +08:00
Kai
046b6c4175
fix(agent-core): scope [image] config limits to the owning core (#1521)
* fix(agent-core): scope [image] config limits to the owning core

* fix(agent-core): thread harness [image] max_edge_px to TUI paste and ACP ingestion

* chore(changeset): simplify entry to user-facing wording
2026-07-09 20:55:52 +08:00
qer
a3548035a8
feat(tui): add Kimi WebBridge install entry to /plugins panel (#1494)
* feat(tui): add Kimi WebBridge install entry to /plugins panel

Surface a hardcoded Kimi WebBridge entry at the top of the Official tab in the /plugins panel. Selecting it opens the WebBridge install page in the user's browser instead of going through the plugin install flow, since WebBridge is a browser extension plus local daemon rather than an installable plugin package.

* fix(tui): restrict WebBridge open-url shortcut to the pinned row

Match the hardcoded pinned WebBridge entry by object reference instead of by id. A curated or custom marketplace entry on the Third-party tab can legitimately reuse the kimi-webbridge id; routing by id hijacked Enter on those rows and opened the WebBridge page instead of installing. The Official tab still dedupes a same-id official catalog entry so the pinned row is not duplicated.

* fix(tui): label WebBridge plugins row as "open in browser"

The previous "webpage" status did not make it clear that selecting this row opens an external page rather than installing in-app. "open in browser" states the action directly and contrasts with the install label on regular plugin rows.

* test(tui): navigate past pinned WebBridge row in marketplace install tests

Two message-flow tests pressed Enter on the Official tab assuming index 0 was the Kimi Datasource entry. The hardcoded Kimi WebBridge row now leads that tab, so move down one row before installing.
2026-07-09 19:51:53 +08:00
liruifengv
170ae44205
style(web): polish the session sidebar (#1519)
* feat(web): use sidebar fold/unfold icons for sidebar toggle

* feat(web): move settings entry to a sidebar footer row

* feat(web): fully collapse sidebar with animated width transition

* feat(web): redesign sidebar colors, spacing and macos desktop chrome

* feat(desktop): center traffic lights on the 48px header row

* fix(web): restore webkit thin scrollbars and unify sidebar icon sizes

* feat(web): add Kbd keycap component and justify sidebar search shortcut

* style(web): rework sidebar palette and pin a resident sidebar toggle

* fix(desktop): sync window appearance with web UI theme so dimmed traffic lights stay visible

* feat(web): adopt Kimi design icons in the sidebar via a local icon collection

* style(web): mute workspace group title color in the sidebar

* style(web): refine sidebar typography, unify shortcut keycaps, float workspace row actions

* style(web): cap sidebar draggable width at 480px

* style(web): derive sidebar row height from type and padding, float the kebab

* chore: add changeset for sidebar UI polish

* fix(nix): update pnpmDeps hash

* style(web): put the sidebar collapse button inside the header on non-mac

* fix(nix): update pnpmDeps hash
2026-07-09 19:39:20 +08:00
Kai
1bf2c9afee
feat: keep image-heavy sessions within provider request-size limits (#1508)
* feat(kosong): classify HTTP 413 request-body-too-large as a dedicated error type

* feat(agent-core): lower default image downscale cap to 2000px and make it configurable

* feat(agent-core): strip media to text markers and retry when the compaction request is too large

* feat(agent-core): cap model-initiated image reads with a configurable byte budget

* feat(agent-core): resend with degraded media when the provider rejects the request body as too large

* test(agent-core): add explicit timeouts to encode-heavy image budget tests

* feat: add WebP decoding support with wasm integration

- Introduced a new WebP decoding module using @jsquash/webp's wasm decoder.
- Implemented functions to decode WebP images and check for animated WebP formats.
- Updated image compression tests to include scenarios for WebP handling, including encoding and decoding.
- Enhanced error handling for API request size limits to accommodate various error messages.
- Updated pnpm lockfile to include new dependencies for WebP encoding and decoding.

* chore(changeset): consolidate this PR's entries into one

* fix(nix): update pnpmDeps hash for merged lockfile

* feat(agent-core): refuse HEIC/HEIF reads with platform-matched conversion guidance
2026-07-09 18:05:14 +08:00
liruifengv
b91099ed7a
feat: display Extra Usage fuel pack balance in /usage and /status (#1501)
* feat(oauth): parse boosterWallet extra usage from /usages

* feat(oauth): expose extraUsage on AuthManagedUsageResult

* feat(kimi-code): render Extra Usage section in /usage panel

* fix(kimi-code): address Task 3 review feedback for extra usage section

* feat(kimi-code): render Extra Usage section in /status panel

* feat(kimi-code): wire extraUsage into /usage and /status commands

* chore(extra-usage): address final review findings for fuel pack feature

- Update changeset to cover both kimi-code and kimi-code-sdk packages

- Add parser clamp tests and toolkit null-case test

- Replace 'as never' casts in usage-panel tests

- Wrap long import line in status-panel

* chore: temporarily log /usages raw response for debugging

* fix(oauth): accept BOOSTER balance type and drop debug log

* fix(oauth): drop reset hint from Extra Usage and revert periodEnd parsing

* fix(oauth): treat missing amountLeft as zero extra usage and drop debug log

* revert: keep missing amountLeft defaulting to 0 (fully used)

* feat(extra-usage): show monthly cap usage bar and balance in /usage and /status

* fix(extra-usage): show balance and unlimited marker when no monthly cap

* fix(extra-usage): show monthly used with unlimited marker and balance

* fix(extra-usage): label Used and use English Unlimited

* feat(extra-usage): render balance, monthly used and monthly limit as labeled rows

* fix(extra-usage): move Balance row to the bottom

* fix(extra-usage): format currency values with two decimals for column alignment

* fix(extra-usage): right-align currency values so numbers line up

* fix(extra-usage): align currency symbol and decimal point in usage rows
2026-07-09 17:44:59 +08:00
Kai
fe9479d89a
fix: rewrite repeated tool call reminders to redirect instead of prohibit (#1518)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
The r1/r2/r3 reminders injected into repeated tool results led with
prohibition verdicts and, in r2, echoed the repeated tool name and full
arguments back into the context, reinforcing the very pattern they were
meant to break. Rewrite them to state the situation factually and hand
the model a concrete next action: an expectation-setting sentence for
the next call (r1), a forced decision menu of falsify / ask-user /
conclude (r2), and a final hand-off summary without further tool calls
(r3). Detection, thresholds (3/5/8/12), force-stop, and telemetry are
unchanged.
2026-07-09 17:06:19 +08:00
Luyu Cheng
173bdfdab1
fix: resume sessions with missing workdir (#1517) 2026-07-09 16:46:57 +08:00
Luyu Cheng
9fb19154ac
fix: keep prompt goals running until terminal (#1516)
* fix: keep prompt goals running until terminal

* fix: reject invalid prompt goal commands

* fix: ignore stale prompt goal status checks
2026-07-09 15:54:50 +08:00
Luyu Cheng
ad30a1c632
style(web): polish web UI typography and controls (#1502)
* style(web): polish sidebar and tool typography

- Use UI font and medium weight for sidebar and composer controls

- Add reusable shortcut and tool output blocks

- Cap long tool output at 50 lines with a scrollbar

- Update sidebar show-more copy and muted styling

* style(web): refine workspace picker sizing

* style(web): align composer mode menus

* style(web): tune list and question typography

* style(web): reuse shortcut keys in approvals

* style(web): size workspace picker from content

* feat(web): localize chat status labels

* style(web): refine composer toolbar controls

* style(web): use complete Inter variable font

* style(web): tune sidebar workspace typography

* style(web): polish composer and workspace picker

* style(web): refine markdown and thinking typography

* chore: add web UI polish changeset

* fix(web): pin Inter package for Nix build

* style(web): polish goal tool calls

* style: polish goal mode display

* fix: layer latest message pill below menus

* style: align goal strip content
2026-07-09 14:55:58 +08:00
liruifengv
735922c291
feat(kimi-web): add status-aware browser notifications (#1479)
* feat(kimi-web): add approval notification storage key and i18n copy

* feat(kimi-web): add approval notification helpers and tests

* feat(kimi-web): wire approval notifications and guard completion alerts

* fix(kimi-web): extract shouldNotifyCompletion helper and add tests

* feat(kimi-web): add approval notification settings toggle

* chore(kimi-web): add changeset and tidy notification module comment

- Align approval notification tag with spec (kimi-approval-${approvalId})

- Update module header to describe all three notification kinds

* fix(kimi-web): make notifications fire reliably

- Key completion notification tags by turn (sid + promptId) and question
  tags by request id, so a stale notification left in the notification
  center no longer swallows every follow-up alert in the same session
- Suppress notifications only while the window is actually focused, not
  merely visible (document.hasFocus() on top of visibilityState)
- Play the attention sound when a tool needs approval, matching the
  completion and question sounds

* chore(kimi-web): simplify changeset
2026-07-09 12:30:15 +08:00
liruifengv
b89fc1a4fb
docs(changelog): sync 0.23.3 and shorten OAuth error entry (#1509)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-08 23:33:29 +08:00
github-actions[bot]
93c0b7bb78
ci: release packages (#1507)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-08 23:18:00 +08:00
7Sageer
e83511a711
fix: surface provider auth error for unavailable models (#1506)
* fix: surface provider auth error for unavailable models

When an OAuth-managed model returns 401 after a forced token refresh, the token is valid but the provider rejected it for that model (the account lacks access). Emit provider.auth_error carrying the provider's message instead of auth.login_required with a misleading "OAuth login expired. Send /login" prompt.

* fix(agent-core): preserve provider auth errors through compaction

Treat provider.auth_error like auth.login_required in the compaction path so an auth rejection during compaction surfaces the provider's message instead of being wrapped as a generic compaction failure.
2026-07-08 21:16:44 +08:00
qer
2394d013bc
docs(changelog): sync 0.23.2 from apps/kimi-code/CHANGELOG.md (#1496) 2026-07-08 17:02:00 +08:00
github-actions[bot]
67b2147d8e
ci: release packages (#1468)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-08 15:37:29 +08:00
qer
b0809ddac8
feat(web): prefix skill slash commands with skill: to distinguish them from built-in commands (#1492) 2026-07-08 15:03:30 +08:00
qer
0cc9831a2f
fix(web): composer model switch also updates global default model (#1491)
* fix(web): composer model switch also updates global default model

The composer model switcher still switches the active session's model via
POST /sessions/{id}/profile (awaited, so the model pill reflects the result),
and additionally fires POST /api/v1/config with { default_model } as a
fire-and-forget side effect so new sessions inherit the chosen default. The
config request is skipped when the model already matches the current default.

* fix(web): route ModelPicker overlay selection through the default-model update

The overlay opened from the composer's "More models" row (and /model) is a
continuation of the same switch flow, so its selection now also bumps the
global default model instead of only switching the active session.

* fix(web): only persist the default model after a confirmed session switch

setModel now returns whether the switch was accepted (true for the draft
path), so the composer flow no longer writes a stale or invalid model alias
into the global config when the session-level switch failed and rolled back.
2026-07-08 14:53:29 +08:00
liruifengv
2ad0120c2a
feat(web): redesign cron reminder as a message bubble (#1480)
* feat(web): redesign cron reminder as a message bubble

Restyle the cron trigger notice as a right-aligned user-style message bubble that shows the scheduled prompt in full (wrapping across lines), with a small meta row beneath it for the schedule, status, job id and run time. Extract a shared MessageTime component used by both user messages and the cron reminder so the timestamp format and click-to-expand behavior stay consistent, and give the CronCreate/CronList/CronDelete tools distinct calendar icons.

* refactor(web): render cron reminders only as standalone turns

Remove the embedded cron block path from the web transcript projector so cron reminder fires always render through the standalone right-aligned bubble path.

* chore(web): simplify cron redesign changeset
2026-07-08 13:42:22 +08:00
qer
b30a45efec
feat(web): support Enter key to confirm archive and other dialogs (#1490) 2026-07-08 13:24:44 +08:00
qer
2206d21327
feat(plugins): add Vercel plugin to marketplace (#1489) 2026-07-08 12:22:28 +08:00
qer
f30781bb27
fix(kimi-code): exit 1 when a headless (-p) turn fails (#1483)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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
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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Headless (`kimi -p`) failures could exit with code 0 when the event loop
drained during the shutdown cleanup (e.g. telemetry's unref'd retry backoff
when the network is blocked), because the rejection never reached the
process.exit(1) call. Set the failure exit code before any await in both
the run-prompt catch and the main catch, and keep the cleanup timeout ref'd
so the loop stays alive long enough for the rejection to propagate.
2026-07-07 23:38:20 +08:00
STAR-QUAKE
9b76e5bff6
feat(agent-core): discard loaded tool schemas on compaction (#1471)
Align progressive tool disclosure with the discard-on-compaction model:
compaction no longer rebuilds loaded dynamic tool schemas. The boundary
announcement re-lists every loadable name, the model re-selects what it
still needs, and a from-memory call to a no-longer-loaded tool is
rejected by preflight with select guidance.

This removes the keep-all rebuild and its half-trigger budget heuristics
entirely: the post-compaction floor is back to users + summary, which is
structurally outside the auto-compaction trigger band, and the guard
baseline degenerates to summary + reinjected reminders. Every downstream
mechanism already treated the empty loaded set as its consistent base
state (ledger scan, pending clear at the compaction boundary, deferred
extras, preflight wording), so this is a strict simplification.

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-07 23:06:29 +08:00
Luyu Cheng
131700097a
fix: clarify goal blocked audit guidance (#1481) 2026-07-07 22:29:40 +08:00
_Kerman
6c9abe8cf7
feat(kosong): support structured response formats (#1397) 2026-07-07 22:02:29 +08:00
Luyu Cheng
150206a6f7
fix: count goal creation turn (#1477) 2026-07-07 21:51:29 +08:00
Kai
474ce289dd
fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px (#1460)
* fix(agent-core): report EXIF-rotated image dimensions and raise edge cap to 3000px

Image compression now reports original dimensions in the decoded
(EXIF-rotated) space, matching the coordinate system of the sent image
and of ReadMediaFile region readback; previously portrait JPEGs
(orientation 5-8) got swapped width/height in captions. The longest-edge
downscale cap rises from 2000px to 3000px, and the default jimp resize
path is documented as the anti-aliased area-average one so it is not
accidentally switched to a point-sampled interpolation mode.

* test: shrink oversized image fixtures to fit CI timeouts

The 3600x3600 fixtures introduced for the 3000px edge cap nearly doubled
the pixel area jimp has to decode and deflate, pushing the slowest
compression tests past the 5s vitest timeout on CI runners. 3600x1800
keeps every fixture over the cap while restoring roughly the workload of
the old 2600x2600 fixtures that CI handled comfortably.

* test: pin anti-aliased downscale quality with executable guards

A 1px checkerboard probe pins the compressor to full-coverage averaging
at integer and fractional ratios, with jimp's point-sampled BILINEAR
mode kept as the executable aliasing counter-example (it collapses the
50%-gray pattern to solid black at 4:1). Also guards the other classic
downscale bugs: transparent-pixel color bleed, mean-brightness drift,
iterative recompression degradation, and zero-size collapse on extreme
aspect ratios.

* fix(agent-core): report decoded EXIF-rotated dimensions in ReadMediaFile notes

The media note derived its original-dimensions line from the header
sniff, which reports pre-rotation values for EXIF orientation 5-8
JPEGs. The sent image and region readback both live in the decoded
(rotated) space, so portrait photos got axis-swapped coordinate
guidance. Once a decode has happened — compression or crop — its
dimensions now overwrite the sniffed ones.

* fix(agent-core): improve handling of EXIF orientation in image dimensions and metadata

* fix(agent-core): sniff EXIF orientation and step budget fallback through 2000px

Two follow-ups to the EXIF and 3000px-cap changes:

sniffImageDimensions now reads the JPEG EXIF Orientation tag (pure
header parse, both byte orders) and reports display-space dimensions
for orientations 5-8. Passthrough images — never decoded — previously
kept the pre-rotation header size in compression results and media
read notes, disagreeing with the decoded space that region readback
uses.

encodeWithinBudget steps the over-budget fallback through 2000px
before the 1000px last resort. Raising the cap to 3000px had left a
regression window: an image whose 2000px encode fits the byte budget
was sent at 1000px where the old 2000px cap used to send it at
2000px.

* fix(kimi-code): record pasted image dimensions in display space

The TUI paste path recorded attachment and original dimensions from its
raw header parser, which ignores EXIF orientation. For a portrait JPEG
the submit-time caption then contradicted the sent image's aspect and
region readback coordinates were axis-swapped. Dimensions now come from
the compression result, which reports display space on both the
compressed and passthrough paths; parseImageMeta remains only the
format/mime gate.

* feat(agent-core): add image compression and crop telemetry

Every image ingestion path now reports an image_compress event —
outcome (compressed / passthrough fast, guard, unsupported, unhelpful,
error), input/output formats, byte and pixel sizes, EXIF transposition,
and duration — and region readback reports an image_crop event with a
failure classification and the region's share of the original area.

Wiring is per call site via a new CompressImageOptions.telemetry
option, so the outcome split and timing are measured inside the
compressor while each caller only names its source: ReadMediaFile
(tool construction, like GrepTool), MCP tool results (McpOutputOptions),
server prompt ingestion (ICoreProcessService now exposes the host
telemetry client), ACP prompts (session track adapter), and TUI paste
(host.track adapter). Properties are numeric/enum only — never paths
or content — and a throwing client can never affect the compression
result.

* fix(agent-core): run the full JPEG quality ladder at fallback sizes

The fallback rescales encoded only at quality 20, so a JPEG whose
ladder failed at the fitted size collapsed straight to the lowest
quality even when the smaller size left budget headroom for a higher
rung (the realistic window is the 1000px step, where the 4x pixel
drop pays for q80/q60). Each fallback edge now walks the same
q80-to-q20 ladder as the fitted size.

* test: shrink heavy JPEG fixtures and add explicit timeouts

The fallback-ladder test runs ~11 pure-JS JPEG encodes and the EXIF
paste test decodes, rotates, and re-encodes a 6.5MP frame; both sat at
the edge of the 5s vitest timeout on CI runners. Narrower fixtures cut
the pixel area (the ladder test keeps its width above 2000px so the
full fallback chain still runs) and explicit 15s timeouts absorb runner
variance.

* fix(server): scope prompt image compression telemetry to the session

The prompt-ingestion image_compress events were emitted with the bare
host telemetry client, while every agent-side source inherits a
session-scoped client — so prompt_inline/prompt_file events could not
be correlated with their session. The route now wraps the client with
withTelemetryContext({ sessionId }) like rpc/core-impl does for
session telemetry.

* chore(changeset): consolidate image compression changesets

One entry covering the cap raise and the EXIF dimension fix, listed
for both the CLI and the SDK so the SDK changelog's compression
description (previously pinned at 2000px) stays accurate.
2026-07-07 21:38:43 +08:00
qer
11c6a37ce0
fix(web): dismiss stale connection error toast after ws reconnect (#1474) 2026-07-07 20:47:56 +08:00
7Sageer
2065ae4615
fix(telemetry): gate unsafe numeric telemetry values (#1478)
- Drop numeric telemetry properties whose absolute value exceeds Number.MAX_SAFE_INTEGER before enqueue, and reject them again during payload assembly as a backstop.

- Omit constrained_memory_bytes when process.constrainedMemory() is not a safe non-negative integer so Linux no-cgroup sentinels do not overflow int64 parsing downstream.

- Add telemetry tests for unsafe numeric properties and constrained_memory_bytes filtering.
2026-07-07 20:32:02 +08:00
Luyu Cheng
d1a964fba9
fix: forbid model-driven goal pauses (#1476) 2026-07-07 20:02:43 +08:00
7Sageer
ebd25a4a55
chore(telemetry): sample system metrics every 5 minutes (#1472)
Raise SystemMetricsCollector's default sampling interval from 30s to 5min to reduce the frequency of system_metrics telemetry events.
2026-07-07 18:33:24 +08:00
liruifengv
ee385456d0
refactor(web): migrate icons to unplugin-icons (#1467)
* refactor(web): migrate icons to unplugin-icons

Replace the hand-written gen-icon-data.mjs + @iconify/utils runtime
rendering with unplugin-icons build-time imports. The public API
(<Icon name>, iconSvg, IconName, SIZE_PX, NAME_TO_REMIX) is unchanged;
127+ call sites are untouched.

- add unplugin-icons@^23.0.0 (devDep) + Vite Icons() plugin (compiler: vue3)
- rewrite src/lib/icons.ts: static ~icons/ri/* imports (component + ?raw)
  for 56 distinct Remix icons across 59 IconName entries
- Icon.vue renders <component :is> with unknown-name fallback
- append ICON_GROUPS export for DesignSystemView catalog
- DesignSystemView: v-for catalog, remove legacy-script references
- delete gen-icon-data.mjs, gen-icon-catalog.mjs, icon-data.ts,
  gen:icons script
- remove @iconify/vue and @iconify/utils from dependencies; move
  @iconify-json/ri to devDependencies
- drop Icon.vue from check-style ICON_EXEMPT (no hand-written <svg>)

* refactor(web): drop unused NAME_TO_REMIX icon mapping

NAME_TO_REMIX was a Record<IconName, string> table introduced to map
internal icon names to their ri: ids. After the unplugin-icons migration
it has no production consumers — only icons.test.ts imported it (for two
drift tests) and DesignSystemView mentioned it in descriptive copy. The
ICONS table already conveys the same ri: id via each entry's paired
component + ?raw imports (e.g. RiFolderOpenLine / RawFolderOpenLine).

- remove NAME_TO_REMIX const from src/lib/icons.ts (-63 lines)
- remove NAME_TO_REMIX import + describe block from icons.test.ts
- update DesignSystemView §02 copy: describe the import-pair idiom and
  stop claiming ICON_GROUPS is sourced from NAME_TO_REMIX

* chore: add changeset for web icon migration

* chore(nix): bump pnpmDeps hash for unplugin-icons

Adding unplugin-icons changed pnpm-lock.yaml, so the fixed-output
pnpmDeps derivation hash is stale. Update to the hash reported by the
Nix Build CI run.
2026-07-07 16:44:56 +08:00
qer
2a44ae075a
chore: update 0.23.1 changelog (#1469)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-07 16:22:40 +08:00
liruifengv
063bce2a2f
fix(agent-core): hide console window when running hooks on Windows (#1466)
Pass windowsHide:true when spawning the hook process so a visible console
no longer flashes and steals focus on Windows. The Bash-tool path was
already hardened (KAOS buildLocalSpawnOptions); the hook runner missed the
flag even though its own taskkill helper already set it.

Extract the spawn options into a pure builder and add a regression test
asserting windowsHide, mirroring the existing KAOS spawn-options test.

Relates to #1298.
2026-07-07 15:55:47 +08:00
github-actions[bot]
08b3c3fc53
ci: release packages (#1444)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-07 15:36:15 +08:00
Luyu Cheng
e9ef9399d0
fix(agent-core): harden goal-mode budget and outcome flow (#1456) 2026-07-07 15:23:57 +08:00
liruifengv
bfdbce593f
feat(kosong): honor explicit anthropic max output override (#1465)
* feat(kosong): honor explicit anthropic max output override

Add claude-opus-4-8 output ceiling and treat explicit max_output_size/KIMI_MODEL_MAX_OUTPUT_SIZE as the final Anthropic max_tokens value. Sync configuration and environment variable docs.

* feat(kosong): drop claude-opus-4-8 ceiling

Revert the newly added claude-opus-4-8 default output ceiling while keeping explicit max_output_size overrides for Anthropic.
2026-07-07 15:23:42 +08:00
Luyu Cheng
03e78ae190
fix(kosong): fall back to nearest lower catalogued minor for max tokens (#1463)
* fix(kosong): fall back to nearest lower catalogued minor for Claude max tokens

An uncatalogued Claude minor version (e.g. claude-opus-4-8) previously
dropped straight to the family/major baseline ceiling, so Opus 4.8
resolved to 32k max output tokens instead of the 128k it supports.
Walk the minor version down to the nearest catalogued entry before
using the family baseline, so a newer minor inherits its predecessor's
documented ceiling.

* fix(kosong): catalogue Opus 4.8's documented 128k output ceiling

Known models should resolve from explicit table entries; the
nearest-lower-minor fallback now only covers minors that are not yet
catalogued. Fable 5 already has an explicit entry.
2026-07-07 15:11:12 +08:00
qer
928726ace1
chore: correct wire-request-trace changeset (#1464)
PR #1448 records a per-request trace in wire.jsonl for debugging, with
no user-facing behavior change. Downgrade the CLI bump from minor to
patch, drop the unpublished private internal packages (agent-core,
kosong), and trim the entry to a single honest sentence.
2026-07-07 14:59:19 +08:00
qer
16dc940834
fix(web): recover from stale background websocket (#1451) 2026-07-07 14:48:22 +08:00
qer
809a88cb34
fix(web): run new-session slash actions (skills, /goal, /btw) from the empty composer (#1445)
* fix(web): activate slash skills on the new-session screen

The composer now lists workspace skills before a session exists, but
activating one from the empty-session composer silently did nothing:
activateSkill() short-circuits when there is no active session id, so
the command was cleared with no turn started.

Mirror the first-prompt path: when a slash skill is activated with no
active session (but a workspace is active), create the session first and
activate on the new session id. Extract the shared session-creation
block out of startSessionAndSendPrompt into createDraftSession and add a
startSessionAndActivateSkill counterpart; activateSkill now also accepts
an explicit session id so a concurrent session switch can't redirect it.

Unrecognized '/cmd' text still falls through to a plain-text message as
before.

* fix(web): persist draft modes when a new session is opened via skill

The skill-activate request carries only `args`, so for a skill launched
from the new-session composer createDraftSession's local plan/swarm maps
never reached the daemon, and the first skill turn ran at default modes
while the UI showed them enabled.

Persist the draft plan/swarm modes to the new session's profile inside
createDraftSession (by the new session id), and teach persistSessionProfile
to take an explicit session id so a concurrent session switch during the
snapshot load can't write the patch to the wrong session. Plain prompts
already send planMode/swarmMode on the prompt request itself, so this is
redundant but harmless on the first-prompt path. Goal mode is a one-shot
flag consumed per send, not a profile field, so there is nothing to
persist for it.

Add coverage that the profile is persisted before activation when draft
plan/swarm modes are enabled.

* fix(web): await draft-mode profile POST before activating a skill

The previous fix wrote the draft plan/swarm modes to the new session's
profile but fire-and-forget: persistSessionProfile returned immediately
after starting POST /profile, so startSessionAndActivateSkill sent
:activate without waiting. Since skill activation carries only args, the
daemon could process :activate (and start the turn) before
applyAgentState from /profile finished, running the first skill turn at
default modes while the UI showed them enabled.

Make persistSessionProfile return the update promise and await it inside
createDraftSession, so any origin that follows (the skill activation, or
a plain prompt) only starts after the profile is applied. Existing
callers still fire-and-forget via `void persistSessionProfile(...)`.

Test coverage now blocks activation behind a deferred profile POST and
asserts it is issued only after the profile resolves.

* refactor(web): persist draft modes on the skill path only

Move the awaited plan/swarm profile write out of createDraftSession and
into startSessionAndActivateSkill. The create path now only builds the
session and mirrors draft modes into the per-session maps, matching the
old first-prompt behavior: plain prompts already send planMode/swarmMode
on the prompt request itself, so a /profile write there was redundant.

The profile write stays on the one path that needs it: skill activation
carries only args, so the draft modes must be stored on the new session
and applied before :activate is sent.

* fix(web): persist draft permission and thinking for new-session skill

/auto, /yolo, and /thinking on the new-session composer only update
rawState (there is no session to persist to yet). A plain first prompt
still honors them because submitPromptInternal sends permissionMode and
thinking on the request, but skill activation carries only args, so the
first skill turn was running at daemon defaults.

Include permissionMode and thinking in the awaited profile patch alongside
planMode/swarmMode before activating, so the skill turn matches the
controls the UI shows.

* fix(web): start /goal from the new-session composer

createGoal() short-circuited when there was no active session, so
`/goal <objective>` from the empty-session composer silently cleared and
ran nothing — the same bug class as the slash-skill activation fixed
earlier in this PR.

Mirror startSessionAndSendPrompt: when no session exists but a workspace
is active, create one first then target it with the goal profile update
and the objective prompt. Send via submitPromptInternal with the explicit
sid so a concurrent session switch during creation can't redirect it.
Plain prompts already carry their own permissionMode / thinking / plan /
swarm, so no profile fallback is needed here.

* fix(web): use active-workspace fallback for empty-composer /goal

On a fresh-booted empty workspace, load() never writes
rawState.activeWorkspaceId (no most-recent session to anchor it). The
UI still has a usable workspace via the client-wide activeWorkspaceId
computed, which falls back to the first sidebar-visible workspace — but
createGoal read the raw value directly and silently no-op'd when it was
null.

Normal first prompts and skill activations didn't hit this because
App.vue passes the computed activeWorkspaceId in. Make createGoal use
the same fallback so a first-session `/goal <objective>` works in empty
workspaces too.

* fix(web): start /btw from the new-session composer

openSideChat() reads rawState.activeSessionId directly, so `/btw
[<question>]` from the empty-session composer silently no-oped — it
still set detailTarget to 'btw', leaving the side chat panel open but
empty.

Add startSessionAndOpenSideChat(workspaceId, prompt?) that creates the
parent session first, then calls a new sideChat.openSideChatOn(sid,
prompt) which targets the explicit parent session id (race-safe against
a concurrent session switch, like the skill activation case). Route
through it from the empty-composer branch in openSideChatTab.

Side-chat prompts now also carry model / thinking / permissionMode /
plan / swarm (via a shared sendSideChatPromptOn), so a BTW first turn
matches the UI even when the parent /profile is still in flight. Unlike
skill activation, this means the BTW path needs no profile fallback.

Tests: startSessionAndOpenSideChat creates a session then opens BTW on
the new id, works without an initial question, and is a no-op for an
unknown workspace; sendSideChatPromptOn carries the runtime controls
on the submitted prompt.

* fix(web): preserve send queue when creating goals

Switching createGoal from sendPrompt to submitPromptInternal avoided the
activeSessionId race during the empty-composer create window, but it also
bypassed sendPrompt's queue guard: when a goal is created against an
already-active session that is running another turn, the prompt posted
immediately instead of being locally queued.

Restore the guard for the overwhelmingly common case (the goal still
targets the active session): route through sendPrompt when activeSessionId
still matches the resolved sid, which enqueues when the session is
running or a prompt is already in flight. Only fall back to the explicit-
session submitPromptInternal(sid) when activeSessionId moved during the
create window, so a concurrent session switch can't redirect the goal
prompt. The newly-created session is idle+not-in-flight in that branch,
so the explicit submit does not race another turn.

Add a regression test: createGoal against a running existing session
enqueues instead of submits.

* fix(web): coerce thinking for skill/BTW and handle goal creation failures

Three follow-ups from review:

- createGoal now wraps createDraftSession in a try/catch: App.vue invokes
  it fire-and-forget, so a rejection from session creation previously
  leaked as an unhandled rejection with no operation failure surfaced.
  Mirrors the skill / BTW / first-prompt paths that already wrap it.

- startSessionAndActivateSkill coerces the draft thinking level against
  the new session's model before persisting the profile (via
  coercePromptThinking), matching what the first-prompt path submits.
  A value carried over from another/default model (e.g. 'max' from an
  effort model) would otherwise be persisted verbatim and the first
  skill turn would run at a level the UI wouldn't send for this model.

- sendSideChatPromptOn coerces thinking against the parent session's
  model the same way normal prompts do (coerceThinkingForModel against
  the model catalog). Model catalog threaded in via a new 'models' dep
  on useSideChat. Same reasoning: stale rawState.thinking must not be
  submitted raw into a BTW first turn.

* fix(web): clear staged goal mode when submitting explicit /goal

When the empty composer has goal mode staged (e.g. the user runs bare
`/goal`, then `/goal <objective>`) and an explicit objective is then
submitted, createDraftSession copies draftModes.goalMode into
goalModeBySession[sid]. createGoal then updateSession(goalObjective) for
the explicit goal, and sendPrompt(trimmed) re-enters submitPromptInternal
which sees goalModeBySession[sid] still set and POSTs a second
goalObjective. The daemon rejects that as an existing goal, the catch in
submitPromptInternal rolls back the optimistic message, and the user's
objective prompt never lands.

Clear the staged goalModeBySession[sid] flag right after the explicit
update, since `/goal <objective>` has exactly the same effect as the
flag's consumption.
2026-07-07 14:18:40 +08:00
Kai
65d30177ad
feat(agent-core): record llm request trace in wire.jsonl (#1448)
* feat(agent-core): record llm request trace in wire.jsonl

Add three observability record types so every request sent to the model
can be reconstructed from the wire log at the logical-request level:

- llm.tools_snapshot: content-addressed snapshot of the top-level tools
  table as sent (post deferred-strip), written once per unique table
- llm.request: one record per outbound request (retries, strict resends,
  and compaction rounds included) carrying the effective request params
  and hash links to the system prompt and tools snapshot
- mcp.tools_discovered: the server's verbatim tools/list result plus the
  agent's gating (allow-list, collisions), deduplicated by content hash

Observability records never feed state rebuild; replay only restores the
write-dedup cursors. The records/types.ts contract now documents the two
record classes explicitly (persisted is not the same as replayed).

Recording happens at the single Agent.generate choke point. The
LLMRequestLogFields side channel gains kind/projection/maxTokens/
droppedCount, chatWithRetry preserves caller-set fields, and compaction
tags its requests. The vis wire view renders the new record kinds.

* fix(agent-core): record the provider-clamped completion cap in the request trace

The llm.request trace recorded the client-requested budget cap, but
chat-completions providers tighten the actual wire value inside
withMaxCompletionTokens (remaining-context sizing, transport ceilings,
model-default resolution) — with the default budget the clamp is active
on nearly every non-empty-context request, so the recorded value did not
match what was sent.

Providers now expose the effective cap they computed as a readonly
maxCompletionTokens field on the clone, and the recorder reads it from
the effective provider at the Agent.generate choke point. This replaces
the side-channel recomputation, which is removed along with the
appliedCompletionBudgetCap helper.

* fix(agent-core): park pre-replay MCP discovery records and hash the collision outcome

Two wire-hygiene fixes for the mcp.tools_discovered trace:

Parking: the real Session ordering connects MCP servers concurrently with
agent construction, so ToolManager can observe a connected server before
agent.resume() has replayed the wire. Recording at that point bypassed
the restored dedup cursor (duplicating a 1-50KB record on every resume)
and appended a stray metadata record ahead of replay. AgentRecords now
exposes a one-shot opened latch — set when replay completes (after the
migration rewrite flushes) or when the first live record is logged — and
ToolManager parks discoveries until then, re-running the dedup check at
drain time. A frozen range-limited replay never opens; those agents are
transient previews.

Collision hashing: the dedup hash now covers the collision outcome, not
just the raw list and allow-list. Collisions depend on which other
servers hold a sanitized qualified name at registration time, so a
server can re-register with identical tools but a flipped outcome; that
gating change must produce a new record instead of being suppressed.

* fix(agent-core): skip the request trace for pre-flight-aborted calls

Mirror kosong generate()'s pre-flight abort check at the Agent.generate
choke point: a call whose signal is already aborted never reaches the
wire (generate throws before dispatching), so it must not leave an
llm.request/llm.tools_snapshot trace or a diagnostic log line claiming a
request was sent. Recording stays before dispatch for every call that
passes the gate, preserving the crash-safety of the trace.

* chore(agent-core): remove a leftover adaptive-thinking override hook

The adaptiveThinkingOverride option was a temporary local hook explicitly
marked for removal before commit. Nothing passes it, so resolution falls
back to the alias-level adaptiveThinking value in all cases; drop the
option and the dead indirection.

* fix(kosong): derive the exposed completion cap from generation kwargs

maxCompletionTokens was a field stored only by withMaxCompletionTokens,
so caps that reach the wire through other paths were invisible to the
request trace: with completion budgeting disabled via env, Anthropic
still sends the constructor-resolved max_tokens (required by the
Messages API), and constructor-level kwargs like OpenAILegacyOptions
maxTokens were likewise unreported.

Replace the stored field with a getter derived from each provider's
generation kwargs — the single source the request body reads — covering
constructor defaults, direct withGenerationKwargs configuration, and
budget application in one place. Kimi mirrors its request-time legacy
max_tokens alias normalization; openai-legacy reuses the same
normalizeGenerationKwargs the request path uses.

* feat(agent-core): add thinkingKeep passthrough for Kimi providers and update tests
2026-07-07 14:09:19 +08:00
qer
9b18dc46cf
fix(kimi-web): shrink collapsed thinking block on mobile (#1461) 2026-07-07 14:04:00 +08:00
liruifengv
8d58d6ad4c
chore(dev): open monorepo root when running make dev (#1458)
* chore(dev): open monorepo root when running make dev

The dev runner spawned the tsx child process with cwd pinned to
apps/kimi-code, so the TUI always opened apps/kimi-code as its working
directory. Switch the child cwd to the monorepo root and pass the
tsconfig, raw-text loader, and entry point as absolute paths so their
resolution does not depend on cwd.

This only affects the local dev runner; the published CLI is unchanged.

* fix(dev): pass --import preload as a file URL

Node resolves `--import` preloads as ESM specifiers. On Windows, the
absolute path produced by path.resolve() looks like a `c:` URL scheme
and crashes the dev CLI with ERR_UNSUPPORTED_ESM_URL_SCHEME before
src/main.ts loads. Convert the preload to a file:// URL via
pathToFileURL so it resolves on every platform.

Addresses codex review on PR #1458.
2026-07-07 13:37:40 +08:00
qer
166e404a92
chore: downgrade #1432 changeset to patch (#1453) 2026-07-07 13:03:11 +08:00
liruifengv
260a80793a
fix(cli): respect --skills-dir in interactive mode (#1457)
The interactive shell entry point dropped opts.skillsDirs when building the harness, so --skills-dir only took effect in prompt mode. Forward it so the flag works in the TUI as documented.
2026-07-07 13:02:57 +08:00
liruifengv
6916240ae3
docs(changelog): remove contributor thanks credit from docs and update skills (#1454) 2026-07-07 12:49:27 +08:00
Haozhe
244ec077f9
fix(agent-core): remove print-mode subagent drain deadline (#1452)
- drop the session-wide absolute drain deadline that gated print-mode turn holds
- hold the turn until background subagents reach a terminal state, bounded by each subagent's own timeout
- fixes late or long-running subagents being abandoned (results suppressed) in long `kimi -p` runs
2026-07-07 12:37:14 +08:00
liruifengv
7a65e0d1c0
feat(tui): update permission mode copy and reorder /auto /yolo (#1450) 2026-07-07 12:26:25 +08:00
Kai
743f66e547
refactor: move tool-result metadata into a structured note side channel (#1437)
* fix: stop rendering <system> notes from tool results in the terminal and web UIs

Tool results carry <system> blocks as side-channel notes for the model (ReadMediaFile summaries, Read status, MCP image captions, error/empty sentinels). Keep them in history for the model, but strip them at every core-to-UI boundary so they no longer render as plain text. vis is intentionally left untouched to preserve the model's-eye view for debugging.

* fix: keep error/empty status text visible when stripping tool-result <system> tags

Unwrap the tool error/empty sentinels (<system>ERROR: ...</system>, <system>Tool output is empty.</system>) instead of deleting them: keep the human-readable text and drop only the tags. Otherwise a failed or empty tool result rendered as a blank output, indistinguishable from a rendering bug. The model still reads the wrapped form in history.

* refactor: move tool-result metadata into a structured note side channel

Tool-produced model-facing metadata (ReadMediaFile summaries, Read status
lines, MCP image-compression captions) was baked into tool output as
<system> text, so every UI had to strip it back out and three copies of
the model-view normalization had silently drifted apart.

- ExecutableToolResult gains `note`: content rendered to the model but
  never to UIs; records and history now store the raw output plus the
  structured isError/note fields
- the model view is rendered exactly once at the LLM projection boundary
  by renderToolResultForModel; the transcript and vis hand-copies are
  deleted (vis now calls the same function for its model view, fixing
  their drifted empty-output checks)
- ReadMediaFile / Read / MCP captions write `note`; tool outputs stay
  pure data, and text-only results keep a single text part (note joined
  with a newline) so provider tool content stays a plain string
- all UI-side <system> stripping is removed; failed tools show their own
  error text with the structured isError flag
- wire protocol 1.4 -> 1.5 migrates existing records' tool-produced
  <system> blocks into `note` on resume

* fix: provider-neutral wording, no wire migration, direct optional fields

- "The attached image was downsampled" replaces directional wording that
  depended on provider serialization order (inline media vs flatten-and-
  re-attach)
- drop the 1.4 -> 1.5 wire migration: legacy records replay verbatim, so
  the model view of old sessions stays byte-identical to what the model
  originally saw and UIs show the legacy <system> text as-is; this also
  removes the risk of the migration misclassifying user data that quotes
  tool metadata, and the additive note field needs no version bump
- pass optional result fields as undefined instead of conditional spreads
  (repo convention)

* fix: enforce the note contract at the trust boundary; narrow the TUI system-tag guard

- normalizeToolResult now keeps a note only when it is a non-empty string:
  tools and finalize hooks are arbitrary JS, and a malformed note (null,
  number, object) would previously persist into the record and crash every
  subsequent LLM projection of the session. Everything downstream now
  trusts note to be string | undefined.
- the TUI tool body suppression matches the full <system-reminder> tag
  instead of any <system prefix: reminder piggy-backing stays hidden,
  while real output that merely starts with a literal <system> tag (file
  contents, MCP text) stays visible, covered through the real
  ToolCallComponent path.

* fix: return MCP compression captions as data instead of extracting them from text

compressImageContentParts now returns { parts, captions } — captions come
back from the compressor as structured data and are never inserted into
the parts, so the MCP pipeline no longer pattern-matches text to move
them into the note side channel. Tool output that merely quotes a
caption (a doc, a log, a test fixture) stays verbatim in the output.
Also corrects the stale claim that prompt ingestion uses this helper
(it compresses per image while constructing the part).

* docs: correct the image-compression re-export comment; export CompressedContentParts

The package-root comment still described compressImageContentParts as the
input-stage helper every ingestion site calls; prompt ingestion compresses
per image with compressBase64ForModel / compressImageForModel, and the MCP
pipeline is the walker's only caller. Also export the walker's
CompressedContentParts return type so public-API consumers can name it.

* feat: wrap tool status sentinels in <system> so the model can tell harness verdicts from tool output

The error/empty status text is model-only after the note refactor (UIs
render the raw output and style failures via the structured isError
flag), so the earlier plain-text wording served no remaining audience.
Wrapping the statuses in <system> gives every piece of system-generated
text inside a tool result the same marker:

- failed calls get '<system>ERROR: Tool execution failed.</system>'
  unconditionally — the ERROR:-prefix guard is removed, so the harness
  verdict can no longer be confused with tool output that happens to
  start with error-like text
- empty outputs render as '<system>Tool output is empty.</system>'; the
  plain placeholder the loop layer bakes into records is still
  recognized and upgraded at projection time

* style: collapse an internal helper docstring per the services subtree convention
2026-07-07 11:40:27 +08:00
qer
4aeb33637f
docs(changelog): sync 0.23.0 from apps/kimi-code/CHANGELOG.md (#1446)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-06 23:59:08 +08:00
Kai
25a655cf88
feat(agent-core): enable Preserved Thinking by default on the Anthropic provider (#1432)
* feat(agent-core): enable Preserved Thinking by default on the Anthropic provider

Default thinking.keep to "all" for the Anthropic provider (Claude and Kimi in Anthropic-compatible mode) while Thinking is on, via a context_management clear_thinking_20251015 edit, mirroring the Kimi default. Reuses [thinking] keep and KIMI_MODEL_THINKING_KEEP (env > config > default "all"); off-values disable it.

* feat(kosong): route Anthropic Preserved Thinking through the beta Messages API

Force the beta endpoint (client.beta.messages.create) when thinking.keep is enabled, since clear_thinking_20251015 is only honored there. Also prepend clear_thinking to any existing context-management edits (for example clear_tool_uses) instead of replacing them, keeping it first as Anthropic requires when combining edits.

* docs: clarify Anthropic beta endpoint and compaction keep behavior

Note in code comments and bilingual docs that enabling Anthropic Preserved Thinking routes requests to the beta Messages API (client.beta.messages.create), with keep=off as the escape hatch back to the standard endpoint. Correct the resolveThinkingKeep comment to reflect that compaction shares ConfigState.provider and intentionally carries the same keep.

* test(kosong): cover Anthropic beta endpoint (streaming and forced betaApi)

Add a streaming beta-endpoint capture and a test that withThinkingKeep forces the beta endpoint even when constructed with betaApi: false, pinning down the documented behavior.
2026-07-06 23:45:17 +08:00
github-actions[bot]
379bc57ef0
ci: release packages (#1378)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-06 23:18:32 +08:00
qer
c4c2fef162
chore(skills): merge similar changelog entries and credit external contributors (#1442) 2026-07-06 23:18:09 +08:00
qer
903e8ed93a
fix(web): clarify archive session confirmation copy (#1428)
* fix(web): clarify archive session confirmation copy

* fix(web): drop delete mention from archive confirmation copy

* feat(web): add archived-sessions restore entry to mobile settings

Mobile used a separate settings bottom-sheet with no archived-sessions
restore surface, so the archive confirmation pointing users to Settings
to restore was untrue on mobile. Add an Archived sessions sub-view to
the mobile settings sheet with search, sort and restore, mirroring the
desktop Settings tab.

* fix(web): refresh mobile archived list and use Input primitive

Refresh the archived-sessions list each time the mobile sub-view opens so
sessions archived from the mobile switcher show up without remounting the
sheet, and replace the hand-rolled search input plus inline svg with the
shared Input primitive.

* fix(web): use Button primitive for mobile archived restore

Render the mobile Archived sessions restore action with the shared Button
primitive instead of a hand-rolled button and scoped CSS, matching the
desktop archived list and the app's design-system rule.
2026-07-06 22:48:01 +08:00
liruifengv
2374bc41c3
feat(kimi-web): render cron fire notices in the web chat (#1426)
* feat(kimi-web): render cron fire notices as in-transcript cards

Show scheduled-reminder fires as distinct notice cards in the web chat,
both live and after a reload, instead of hiding them. Each card carries
a humanized schedule, a dimmed job id, and a collapsible prompt body.

* fix(kimi-web): isolate cron notice prompt id and route prefixed events

- Give synthesized cron messages a fresh promptId so a fire mid-turn
  cannot be reconciled into the optimistic user echo and hide it.
- Add cron.fired to KNOWN_AGENT_CORE_TYPES so event.-prefixed frames
  reach the projector and render live too.

* fix(kimi-web): truncate long one-line cron prompts when collapsed

Slice the first line to the collapse limit with an ellipsis when a
single-line prompt exceeds it, so the collapsed state actually truncates
and the expand toggle is not a no-op for common one-line reminders.

* fix(kimi-web): keep cron notices from reconciling into optimistic user echoes

Skip optimistic-echo reconciliation for user messages whose origin is
cron_job or cron_missed: their prompt text can coincide with a still-
optimistic user message, and the loose content match would otherwise
replace the user's turn with the cron notice instead of appending.

* fix(kimi-web): keep in-turn cron injections from breaking tool results

A cron injection steered into an active turn lands inside that turn's
message sequence, between a tool use and its result. Treating it as a
hard user-turn boundary flushed the pending assistant group, so the next
tool result had no group to fold into and the tool rendered without
output. Embed such in-turn cron notices as a block inside the assistant
group instead, and only render a cron at a turn boundary as its own turn.

* fix(kimi-web): only embed cron notices while a tool is in flight

Embedding whenever a group was pending was too broad: on REST snapshots
without prompt ids the whole transcript shares one group, so an idle cron
fire merged into the previous assistant answer and its own reply kept
going in that group. Embed only while the group has a running tool (a
cron sandwiched between a tool use and its result); flush to its own
turn otherwise.

* fix(kimi-web): omit synthetic prompt id on cron notices

The synthesized cron message carried a cron_pr_ promptId that the web
client caches into promptIdBySession for Stop/abort. Because it is not a
real daemon prompt id, it clobbered the active promptId, so Stop first
aborted a nonexistent prompt and only recovered via the error fallback.
Omit the promptId; the reducer already skips optimistic-echo
reconciliation for cron-origin messages, so it is not needed for de-dup.
2026-07-06 22:25:19 +08:00
liruifengv
ac5b5e4cbf
fix(kimi-web): keep tool components from jumping on expand or collapse (#1433)
* fix(kimi-web): keep tool components from jumping on expand or collapse

Also show the scroll-to-bottom button whenever scrolled up, and render a
fallback icon for tools without a dedicated glyph.

* fix(kimi-web): preserve bottom follow during content-only resizes

Late-loading media can grow after scrollKey has run; keep chasing the
bottom on content growth, and only suppress follow during the pinned
expand/collapse window.
2026-07-06 22:19:02 +08:00
qer
d86fa38e11
fix(kimi-web): disable text hyphenation and code ligatures (#1438)
Set hyphens: none (with the -webkit- prefix) on body so chat and markdown text never gain a hyphen glyph at a line break; components still control where lines wrap via word-break/overflow-wrap.

Disable the ligatures that coding fonts enable by default (liga/calt/ss01) on native code elements so code renders literally, e.g. != stays as two characters.
2026-07-06 22:12:43 +08:00
qer
0824e7b668
chore: ignore and remove throwaway scratch files (#1439)
Agent working notes (HANDOVER/handoff) and one-off UI prototype HTML files were committed by mistake. Remove the already-tracked ones, add .gitignore patterns for these classes of files and a .tmp/ scratch dir, and document the rule plus a pre-commit self-check in AGENTS.md so future mistakes are caught mechanically.

No publishable package is affected, so this PR needs no changeset.
2026-07-06 22:12:14 +08:00
qer
c5e3e80041
feat(web): render AgentSwarm as an inline tool card (#1425)
* feat(web): render AgentSwarm as an inline tool card

Replace the bottom SwarmCard footer and the messagesToTurns live-skip
with one dedicated inline tool card for AgentSwarm. The card shows a
phase overview plus a per-subagent accordion: live progress while it
runs, parsed aggregated result once it completes (and after a refresh
that has already dropped the live tasks).

Refresh and resync keep member identity metadata (swarmIndex,
parentToolCallId, subagentType, runInBackground) stable across skeleton
task replacement in the reducer, and the .content-wrap flex layout is
hardened against the overflow compression that previously displaced the
footer.

* fix(web): handle swarm review feedback

- SwarmTool: when AgentSwarm fails before producing a structured
  agent_swarm_result (e.g. argument validation), render the raw tool
  output instead of the "waiting for subagents" placeholder so the
  failure cause is visible.
- resolveSwarmMembers: source live members from the AppTask store keyed
  by parentToolCallId rather than buildSwarmGroups, which filters out
  single-member groups. A resume-only AgentSwarm now streams its live
  progress before the final result arrives. The badge counter still
  relies on buildSwarmGroups's filter.

* fix(web): carry streamed subagent text into swarm rows

Swarm subagents that stream normal assistant output accumulate it on
AppTask.text (text-kind taskProgress), not outputLines. The new live
member map was dropping `text`, so a still-composing subagent rendered
an empty / stale row until the structured result arrived.

- Add `text` to SwarmMember and thread it through buildSwarmGroups and
  swarmMembersByToolCall.
- SwarmTool: prefer member.text for both the row activity preview and
  the expanded body; fall back to outputLines / summary.
- Tests cover text propagation through both helpers.

* fix(web): merge swarm result rows and fall back to raw output

Address the two latest swarm review comments:

- Rows: when a parsed agent_swarm_result coexists with live AppTasks
  (which the detail panel also depends on), the inline card previously
  only rendered the live members. Interrupted swarms can carry
  state="not_started" / outcome="aborted" result entries for items that
  never spawned a task; those rows were dropped until a refresh cleared
  the live tasks. Extract the row model into buildSwarmCardRows and
  merge result-only aborted/not-started rows with the live member rows.
- Fallback: when the tool is no longer running but produced no
  structured result (argument validation, parser miss, or legacy
  legacy transcript), render the raw tool output instead of
  "Waiting for subagents…" so the final text / failure cause is
  visible to the user.

* fix(web): parse swarm result subagent bodies defensively

Producer writes subagent body unescaped, so a subagent that analyzes or
emits an AgentSwarm snippet can include a literal "</subagent>" inside
its body. The non-greedy regex treated that as the row close and truncated
the body in the result-only path (post-refresh where the AppTask store is
gone).

Rewrite the parser to scan opening tags, then resolve each row's body as
everything up to the last "</subagent>" before the next row's opening tag
(or document end), preserving embedded close-tag strings. Add tests for a
literal "</subagent>" within a single body and across sibling rows.

* fix(web): only count top-level subagent result tags

A subagent body that contains a literal `<subagent ...>` tag — for
example emitting an AgentSwarm/XML snippet — was being pre-collected as
another result row, splitting the real body and producing duplicate /
bogus subagents after refresh where the AppTask store is gone.

Rewrite parseSubagents with a depth-tracking tokenizer: scan every
`<subagent ...>` / `</subagent>` token in order, push a real row frame
only at the outermost level, and treat openings / closings while nested
inside another body as body text. Drop the now-inaccurate "literal
</subagent> without matching open" regression tests; replace with tests
that verify a balanced nested snippet stays inside the parent body and
does not register as a separate row.
2026-07-06 22:05:05 +08:00
qer
a5fbcb75b4
fix(kimi-web): keep composer toolbar usable on narrow windows (#1436)
The desktop composer toolbar renders every control on one row and relies on
overflow:hidden to fit, so between the mobile breakpoint and a wide window it
clipped its own content. Shed secondary ink below 980px (the context readout
moves into the ring tooltip, the model name truncates earlier, permission is
capped) and keep the context ring visible on phones too, instead of sending it
to the settings sheet.
2026-07-06 21:54:38 +08:00
7Sageer
10922fc70f
feat(telemetry): add system metrics collection (#1435)
* feat(telemetry): add system metrics collection

Add periodic CPU and memory telemetry sampling with warmup capture, lifecycle cleanup, and tests.

* fix(telemetry): attach prompt session to system metrics
2026-07-06 21:54:12 +08:00
qer
578f7d334c
fix(web): reconcile session from snapshot on reopen (#1409)
* fix(web): reconcile session from snapshot on reopen

* fix(web): discard stale snapshot when a newer prompt races reopen

* fix(web): harden reopen snapshot against first-open and optimistic-send races

* fix(web): keep evicted reopens subscribed when a snapshot races

* fix(web): let resync snapshots bypass the reopen staleness guard

* fix(web): force-apply the snapshot after an undo

* fix(web): gate session reopen on durable seq instead of updatedAt

* refactor(web): always rebuild reopened sessions from a snapshot

* fix(web): sharpen reopen snapshot discard and skip rebuilds mid-stream

* refactor(web): unconditionally apply session snapshots, drop the staleness guard

* fix(web): preserve loaded older messages when reopen snapshots apply
2026-07-06 21:30:25 +08:00
qer
4aacddc432
fix(kimi-web): clarify desktop notification title and icon (#1434) 2026-07-06 21:18:45 +08:00
7Sageer
dd9077595d
chore(agent-core): classify turn_interrupted telemetry cause (#1431)
Add an `interrupt_reason` field to the `turn_interrupted` telemetry event so the data can tell a deliberate user cancel (`user_cancelled`) apart from a programmatic abort (`aborted`), max-steps exhaustion (`max_steps`), an error (`error`), or a hook-filtered turn (`filtered`).

The user-cancel signal comes from the existing UserCancellationError carried as the abort signal's reason, reused here without changing any loop control or external protocol semantics.
2026-07-06 20:52:40 +08:00
Haozhe
6c0ce09414
feat(server): support restoring and listing archived sessions (#1073)
* feat(server): support restoring and listing archived sessions

- add a `:restore` session action that clears the archived flag in state.json and returns the restored session
- add an `archived_only` list query param, mutually exclusive with `include_archive`, that post-filters to archived sessions
- keep the implementation in the server layer as a temporary measure until agent-core exposes restore natively

* fix(server): paginate archived-only sessions before response

* feat(web): add archived sessions page in Settings

Browse, search, filter by workspace, sort, and restore archived sessions
from a new Archived tab in Settings, backed by the server archived_only
list and :restore action.

* fix(web): keep archived Load more visible when a page filters to empty

When a search or workspace filter empties the loaded archived page, the
Load more button was hidden inside the non-empty branch, so users could
not fetch older pages to find a match. Move the button out so it stays
available whenever more archived pages exist.

* fix(server): preserve after_id bound while draining archived pages

Draining an archived_only request that starts from after_id would switch
to before_id and cross the pivot, reintroducing the pivot and older
sessions. Take a single filtered page for after_id instead of draining
past the lower bound.

* fix(server): drain archived_only within the after_id bound

An archived_only request starting from after_id now keeps paging toward
older sessions until it reaches the pivot, instead of treating the first
page as exhaustive. The loop stops as soon as it encounters the pivot
session itself, so it never reintroduces the pivot or anything older.

* feat(web): drain all archived pages for global search and sort

When the user searches, sorts, or changes the workspace filter in the
Archived settings page, fetch every remaining archived page first so the
client-side filter and sort run over the full set rather than only the
pages loaded so far.

* refactor(web): load all archived sessions upfront instead of paginating

Fetch every archived session once when the Archived settings tab opens and
drop frontend pagination entirely. Search, sort and workspace filter now run
over the full set, removing the empty-page and cursor bookkeeping that
previously caused bugs.

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-06 20:09:45 +08:00
liruifengv
fa6d198b01
fix(kimi-web): fix input caret and todo strikethrough (#1423)
* fix(kimi-web): keep composer caret visible when input is empty

The composer textarea coloured its empty state with `--faint`, and with no caret-color set the caret inherited that faint colour and nearly vanished until the first character was typed. Pin the caret to --color-text so it stays readable regardless of the placeholder state.

* fix(kimi-web): use currentColor for done todo strikethrough

TodoCard pinned the done-state strikethrough to --color-line-strong, which is lighter than the faint text in light theme and darker in dark theme, so the line looked washed-out and broken against the text. Drop the override so the line inherits the text colour, matching TasksPane and the design-system examples.

* fix(kimi-web): do not reserve width for hidden workspace row actions

The workspace header's more/add buttons were hidden with opacity: 0, which kept them in the flex layout and permanently reserved ~60px on the right, so the workspace name (flex:1) truncated long before the row was full. Hide them with display: none and restore display: inline-flex on hover/focus/open, so the name fills the row and only truncates once the buttons actually appear.

The same actions remain reachable via the right-click menu and the section kebab, so removing them from the tab order when hidden is acceptable.

* fix(kimi-web): keep workspace header row height stable on hover

Lock .gh-top to the sm IconButton height (26px) so revealing the hover actions no longer grows the row and nudges the path line and the groups below it. Follow-up to the display: none change: that freed the horizontal space but let the row height collapse when the buttons were hidden.

* chore: add changeset for kimi-web UI fixes

* fix(kimi-web): restore keyboard access to workspace actions

Revert the display: none change (and the min-height that accompanied it) for the workspace row's hover actions, going back to opacity-hidden buttons.

The display: none approach removed the buttons from the tab order, and the 'create in workspace' add button has no keyboard-accessible alternative in the right-click or section menus, so keyboard users lost that action. Restoring opacity keeps the buttons focusable again, at the cost of the workspace name truncating a little earlier to reserve their space.

Addresses the P2 review on PR #1423.
2026-07-06 18:34:06 +08:00
迷渡
e95fc83cc2
fix: honor web font size setting (#1394)
* fix: honor web font size setting

* fix: scale web font-size dependents

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-07-06 17:29:23 +08:00
liruifengv
1de028612c
fix(tui): keep Edit preview height stable as results land (#1421) 2026-07-06 16:57:52 +08:00
liruifengv
5ea3ec489e
fix(tui): stabilize Bash card height and distinguish command from output (#1419)
Keep the command preview in the body after the result lands so a multi-line command with short output no longer collapses the card. Render the command in textDim with a shellMode $ and the result one shade dimmer in textMuted, and simplify the header to "Running a command" / "Ran a command".
2026-07-06 16:50:07 +08:00
Kai
c12f30951f
feat(agent-core): feed AskUserQuestion answers back as question text and option labels (#1414)
* feat(agent-core): feed AskUserQuestion answers back as question text and option labels

The flattened answers record the model receives was keyed by synthesized
ids (q_0 / opt_0_1), forcing a cross-message positional lookup against the
original tool call to understand what the user picked — both unreadable in
transcripts and a real model-misreads-the-choice badcase.

- toAgentCoreResponse now takes the original broker request and translates
  wire ids back to question text (keys) and option labels (values);
  unknown ids are kept verbatim, missing request falls back to raw ids
- wire protocol unchanged: clients still answer with option ids; the
  resolve route reads the pending request before settling it
- question texts must be unique per call and option labels unique per
  question, enforced in the tool execution path (AJV cannot express the
  zod refine) and mirrored on the exported schemas
- web transcript card resolves both the new label form and legacy id
  transcripts; TUI and ACP paths already produced the text form

* fix(agent-core): align multi-select answer join across clients and harden question schema

- Join multi-select labels with ', ' in the server translator, matching
  what the TUI reverse-RPC path already emits, so the model sees one
  format regardless of which client answered
- Trim segments in the web transcript resolver before label matching:
  TUI-answered multi-select transcripts (', '-joined) previously lost
  their highlight to a spurious leading-space Other row
- Move the question-text/legacy-q_<i> answer lookup out of the SFC into
  askUserToolParse as answerFor(), per that module's testability intent
- Require non-empty question text and option labels (.min(1)) so empty
  strings are rejected by AJV at the tool boundary instead of failing
  deeper in the protocol layer

* fix(agent-core): resolve option ids only within the answered question

The translator's option-id lookup was a single flat map across all
questions, so a stale or malformed response pairing one question with
another question's option id (q_1 + opt_0_0) was silently translated
into a label that was never offered for that question. Scope the lookup
to the answered question's own options; cross-question and unknown ids
now both pass through verbatim, staying diagnosable.
2026-07-06 16:37:54 +08:00
Kai
f7b557732b
chore: symlink CLAUDE.md to AGENTS.md for compatibility (#1420)
Some agent tooling looks for a CLAUDE.md at the repo root. Add a
symlink to the canonical AGENTS.md so both filename conventions
resolve to the same instructions, avoiding any duplicated content.
2026-07-06 16:21:52 +08:00
STAR-QUAKE
f0896a53b0
feat(agent-core): progressive tool disclosure via select_tools (#1369)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core): progressive tool disclosure via select_tools

Keep MCP tool schemas out of the immutable top-level tools[] and let the
model load them on demand, preserving the provider prompt cache:

- kosong: Message.tools (append-only load primitive, serialized as Kimi
  messages[].tools with type:function wrapping and no content),
  Tool.deferred (stripped once in generate() so loaded tools stay
  executable without re-entering the top level), select_tools capability
  bit (UNKNOWN/catalog default false).
- select_tools builtin: load-by-exact-name, three-branch semantics
  settled per name (Loaded / Already available / Unknown), schemas read
  from the live registry, injection-origin schema messages survive undo.
- ToolsDiffInjector: <tools_added>/<tools_removed> announcements at turn
  boundaries and post-compaction, folded from history (undo/compaction/
  resume self-heal), appended only when the loadable set changes.
- Loaded-tools ledger = history scan + defer-window pending set (cleared
  on /clear); loop re-reads the executable table per step so a selected
  tool dispatches on the next step of the same turn; preflight
  distinguishes not-loaded from loaded-but-disconnected.
- Cross-cuts: projection strips protocol context for non-select_tools
  models (lossless mid-session model switch both ways), compaction
  filters it from the summarizer input and rebuilds loaded schemas
  keep-all after folding, token estimation counts message.tools, request
  logging reflects the post-strip wire tools.
- Three-condition gate: capability.select_tools x capability.tool_use x
  tool-select experimental flag (KIMI_CODE_EXPERIMENTAL_TOOL_SELECT).
  Any gate closed reproduces the inline request byte-for-byte; all
  current models keep the capability off, so behavior is unchanged
  until a supporting model is catalogued. The SDK catalog-to-alias
  mapping forwards the capability so catalog-driven setups can enable it.

* feat(kosong): skip tool-declaration-only messages in non-Kimi providers

Message-level tool declarations (messages[].tools) are a Kimi wire
feature. The other providers' explicit field construction already keeps
the tools field off the wire, but the content-free leftover message
would be rejected (OpenAI: system message without content) or serialize
as a garbage <system></system> turn (Anthropic/Google system-to-user
wrapping). Skip such messages entirely via a shared predicate; a message
that also carries content only loses the tools field, as before.

Unreachable in kimi-code (the projection gate strips dynamic-tool
context for models without the select_tools capability before any
provider sees it) — defense-in-depth for direct kosong consumers.

* fix(agent-core): survive runtime flag flips and align tool table with post-compaction state

Two fixes from PR review:

- Register select_tools unconditionally and gate only its exposure in
  loopTools. The tool-select flag can flip at runtime (config reload
  calls setConfigOverrides on the live resolver) without
  initializeBuiltinTools re-running; previously the disclosure shape
  activated while the tool itself was unregistered, cutting the session
  off from MCP entirely until a model/cwd change rebuilt the builtins.
  A profile listing the name explicitly still never surfaces it in
  inline mode, and execution guards the flip race defensively.

- Resolve the per-step tool table AFTER beforeStep, next to
  buildMessages. beforeStep can run full compaction, which trims loaded
  schemas and rewrites the ledger; a table captured before it could
  still dispatch a tool whose schema the model no longer has. The
  executable table and the request messages now always reflect the same
  state, so a trimmed tool is rejected with select guidance instead of
  executed.

* fix(agent-core): drop unused Tool import in dynamic-tools

* fix(agent-core): baseline compaction guard after post-compaction reinjection

The reinjected reminders (loadable-tools manifest, goal) are re-appended
after every compaction, but the nothing-new-since-compaction baseline was
captured before injectAfterCompaction. With a large manifest the guard
could re-trigger auto-compaction against a floor that cannot shrink.
Raise the baseline to the true post-compaction floor once reinjection
completes; the earlier capture stays as a fallback when reinjection
throws.

---------

Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
2026-07-06 15:51:08 +08:00
Kai
79b360c96a
feat(thinking): enable Preserved Thinking by default for kimi models (#1417)
Default `thinking.keep` to "all" when Thinking is on so prior `reasoning_content` is kept across turns. Add `[thinking] keep` to config.toml and keep `KIMI_MODEL_THINKING_KEEP` as an override (env > config > default); off-values disable it.
2026-07-06 15:27:08 +08:00
liruifengv
8c7e6da582
chore(changelog): enable thanks credit in changelog (#1418) 2026-07-06 15:26:03 +08:00
liruifengv
913d042208
fix(tui): keep input anchored after slash command menu closes (#1413)
* fix(tui): keep input anchored after slash command menu closes

Force a full re-render when the slash command menu closes, but only when the session content already overflows one screen; skipped under tmux. Detect the close edge from a render frame so asynchronous closes (Backspace deleting the leading slash) are covered too. Apply the same overflow/tmux gating when restoring the editor from selector panels.

* fix(tui): measure overflow against restored editor tree

Address Codex review: the overflow probe ran before the editor container was swapped back, so it counted the tall replacement panel and forced a full clear/home even when the restored content fit on one screen, yanking the editor to the top. Measure after the editor is mounted instead.

* fix(tui): redraw when content exactly fills one screen

Address Codex review: the guard skipped the forced redraw when the post-close layout is exactly one screen, leaving the editor shifted up because the differential renderer keeps the old viewport offset after a shrink. An exact fill is safe to clear (no blank tail), so redraw when content fills or overflows the viewport, and cover it with a test.
2026-07-06 15:18:27 +08:00
liruifengv
353c0885e5
chore: remove unused kimi-migration-legacy package (#1415)
This package only ever contained a package.json with no sources, dependencies, or scripts, and nothing in the repo imports it (the CLI uses @moonshot-ai/migration-legacy instead). Remove the directory and drop its entries from flake.nix and the changeset config, then refresh the lockfile.
2026-07-06 15:09:27 +08:00
liruifengv
fc259abdb4
fix(tui): complete @ file mentions across additional workspace roots with fd (#1408)
* fix(tui): complete @ file mentions across additional workspace roots with fd

When additional workspace directories are added via /add-dir, @ file completion fell back to a readdir-based scanner capped at 2000 entries, so deeply nested files in large projects never appeared. Route @ completion through fd across every root instead, keeping the query pushed down to fd and deduplicating by absolute path. The readdir fallback remains for when fd is unavailable.

* fix(tui): preserve per-root full-path fallback for @ mentions

Address review feedback: decide the scoped-versus-full-path fallback per root instead of globally. When one root has the scoped directory but another does not, the latter still runs a whole-tree --full-path search with the original query, so a match that only exists under that root is not hidden just because a sibling root happens to contain the prefix directory.

* fix(tui): fall back to filesystem when fd binary is not executable

Address review: when fdPath is non-null but the binary cannot be spawned (managed fd removed or lost execute permission), @ completion returned null because pi-tui swallows the spawn error into an empty result, so the catch never ran. Probe fd with accessSync(X_OK) before delegating and use the filesystem fallback when it is not executable, while still returning null for genuine no-match results.

* fix(tui): trust bare fd command names when probing executability

Address review: when fd is discovered on the system PATH, detectSystemFdPath returns the bare name (fd/fdfind). accessSync checked that literal string relative to cwd and never searched PATH, so a valid system fd was treated as unavailable and @ completion fell back to the capped scanner. Trust bare names (spawn resolves them via PATH) and only probe absolute/relative paths, which is how the managed fd is referenced and which can go stale.

* chore: remove accidentally committed plan files

* test(pi-tui): stabilize paste-burst test by freezing the clock

The paste-burst heuristic uses an 8ms inter-character interval that a slow or busy CI runner can exceed between synchronous handleInput calls, which resets the burst and lets Enter submit. Freeze Date so the synchronous keystrokes always register as one burst, making the assertion deterministic.

* chore: ignore top-level plan directory
2026-07-06 15:02:02 +08:00
qer
e6e6dd53ce
fix(web): stop replaying the bubble entrance animation on session open (#1411) 2026-07-06 13:58:44 +08:00
liruifengv
1c817df1e5
fix(tui): include surrounding context in edit approval preview (#1410) 2026-07-06 13:50:23 +08:00
liruifengv
b9258ee07d
feat(tui): show compaction summary with Ctrl-O (#1346)
* feat(tui): show compaction summary with Ctrl-O

* docs: document compaction summary toggle

* fix(tui): preserve compaction summary expansion state

* fix(tui): preserve compaction summary expansion across replay and theme changes
2026-07-06 13:32:37 +08:00
qer
ce41f4b58d
fix(web): keep Sidebar single-root so collapse hides it (#1406)
A top-level Teleport in the sidebar template made the component multi-root, so v-show could not apply display:none and the collapsed sidebar stayed mounted at the rail width, squeezing the conversation. Move the teleport inside the aside so the sidebar is single-root again.
2026-07-06 12:39:58 +08:00
迷渡
4c43935e31
fix: show Windows session search shortcut (#1393) 2026-07-06 12:08:08 +08:00
qer
c5c6282f44
feat(web): render AskUserQuestion result as an option list (#1391)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(web): render AskUserQuestion result as an option list

Parse the AskUserQuestion tool result and echo the full option list, highlighting the chosen option(s) and dimming the rest, instead of dumping raw JSON. Handles single/multi select, free-text Other answers, and the dismissed state. Answers are zipped back to the input questions by index since the input carries no ids.

* fix(web): drop the stray indent in the tool-call card body

The expanded body of every tool-call card carried a hard-coded 36px left indent (a magic number meant to align with the header name). Drop it so expanded content aligns with the header's 11px padding instead. The design-system preview page mirrored the same rule and is updated to match.

* fix(web): align markdown diff block with the design system

Restyle the local ```diff renderer in chat Markdown to match the ~/diff panel: code text keeps the normal ink colour, the +/- sign carries the add/del colour, and rows use a soft background with an inset accent bar instead of dyeing the text green/red. The header and copy button now match the standard code-block chrome (height, hover, focus ring).

* fix(web): match the markdown diff block chrome to code blocks

Make the local markdown diff renderer's shell identical to a regular code block: swap the text copy button for the same icon button, inherit the header's text-xs sizing, and keep the container / header / code-area padding, background, radius and shadow in lockstep with .code-block-container and .code-block-header. The diff-specific row tinting (soft background, inset accent bar, coloured sign) is kept since that is what makes it a diff.

* fix(web): fall back to raw output for non-answer AskUserQuestion results

Background launches and error cases return plain-text tool output (task_id/status lines, or a failure reason), not the { answers } JSON the card expects. The card used to render an empty, fully-unselected option list in those cases, hiding the task id or error. Now the card only renders the option list when the output parses as the answer payload; otherwise it shows the raw output. Addresses review feedback (P2).

* fix(web): localize AskUserQuestion result labels

The card hard-coded user-visible strings (Dismissed, answer/answers, Answered, the (+N more) summary), so Chinese-locale transcripts mixed in English. Move them into the en/zh tools locale files and read them via t(). Addresses review feedback (P2).
2026-07-06 02:36:31 +08:00
Haozhe
4963c9016f
feat(skills): list workspace skills without a session (#1392)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
- add GET /api/v1/workspaces/{workspace_id}/skills backed by a listWorkspaceSkills core RPC and ISkillService.listForWorkDir, reusing the session skill-loading path so results match a new session
- web: populate the composer slash menu from workspace skills before a session exists, then fall back to session skills once one is active
2026-07-05 18:05:15 +08:00
Haozhe
083d0caf05
fix(session): rebuild index on boot to find missing sessions (#1390)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(session): rebuild session index on boot and self-describe workDir

- persist workDir into state.json so session dirs are self-describing and
  summaries do not depend on the index's one-way-hashed workDir
- relax readSessionIndex so a stale or non-absolute index workDir no longer
  drops an otherwise valid entry
- serialize in-process index appends to avoid torn jsonl lines
- add SessionStore.reindex() and run it once at server boot so the
  scan-free request path can find sessions whose index line is missing
  or stale

* chore: add changeset for session index rebuild

* fix(session): repair index entries with a stale workDir during reindex
2026-07-05 14:06:33 +08:00
Haozhe
ebdffc7df7
fix(gemini): fix Gemini tool calling and thought-signature round-trip (#1389)
- send tool declarations, system prompt, and sampling/thinking settings in the camelCase shape the Google SDK forwards so tool calls reach the model
- thread tool-call extras (thought signatures) through the loop tool-call event into context so Gemini 3 can resume a tool turn
- update and add tests for the corrected request shape and signature round-trip
2026-07-05 14:00:09 +08:00
qer
be7c9916b0
fix(web): refill attachments when editing a queued or undone message (#1357)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(web): refill attachments when editing a queued or undone message

Queued prompts that carry images/video are no longer remove-only: clicking one loads its text and attachments back into the composer. Undo ("edit & resend") now restores the message's attachments too, not just its text. useAttachmentUpload gains loadAttachments, which reuses the existing fileIds (no re-upload) and fetches authenticated blob URLs for protected getFileUrl previews so the refilled thumbnails don't 401.

* fix(web): forward attachment refills through ChatDock

When the normal chat dock is mounted (non-empty conversation), bindChatDock receives the ChatDock instance, but ChatDock only exposed loadForEdit/focus — so the new loadAttachmentsForEdit fell back to a no-op and editing a queued media prompt or undoing a media message still dropped the attachments. ChatDock now forwards loadAttachmentsForEdit to the underlying Composer.

* fix(web): replace composer attachments when refilling edits

loadForEdit(text) overwrites the composer text, but loadAttachments appended to any unsent draft attachments, so a later submit sent the stale draft files together with the edited message's files. Make loadAttachments replace the current session's attachments (revoking their object URLs) so an edit/undo replaces the whole composer, mirroring loadForEdit.

* fix(web): clear stale attachments on text-only edits

When the composer already has attachments loaded (for example after editing a queued media prompt) and the user then edits a text-only queued prompt or undoes a text-only message, loadComposerForEdit skipped loadAttachmentsForEdit (the only path that clears the strip) because the new attachment list was empty/undefined, so the next submit would send the stale media with the new text. Always call loadAttachmentsForEdit(attachments ?? []) so text-only edits replace the strip with an empty set.

* fix(web): make fileId-less refilled media resendable

When editing a user turn whose media was base64-inlined by the server (no fileId), loadAttachments used to add a non-uploading chip with no fileId, which handleSubmit silently drops on resend (and an image-only edit would not submit at all). Re-upload the data URL to obtain a fileId so the attachment is actually resendable; when re-upload is unavailable, skip the chip instead of showing a misleading ready attachment. Also factor a patchAttachment helper.

* chore: prefix web changeset entry

The gen-changesets rules require web-app changelog entries to start with 'web: ' so the synced release notes classify web UI changes correctly.

* fix(web): don't dequeue a prompt when the composer is hidden

When a queued media item is clicked while the dock is showing a pending question or approval, ChatDock has no nested Composer (only rendered in its v-else), so loadComposerForEdit no-ops — but handleEditQueued still dequeued the item, losing it. Make ChatDock.loadForEdit report whether the nested composer is present, have loadComposerForEdit return success, and only dequeue when the load actually succeeds.

* fix(web): preserve URL-backed media when refilling composer

When an undone turn contains media with source.kind === 'url' (a URL but no fileId), loadAttachments used to fall through and drop it. Re-upload the URL (data: or http(s):) to obtain a fileId so URL-backed media is preserved on edit/resend; if the URL can't be fetched (CORS / non-2xx) the chip is dropped instead of shown as a misleading ready attachment.
2026-07-04 23:32:32 +08:00
liruifengv
fbb9dc3772
docs(changelog): fix tui revert wording in 0.22.3 (#1376)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-04 19:57:03 +08:00
liruifengv
d0f2994536
docs(changelog): sync 0.22.3 from apps/kimi-code/CHANGELOG.md (#1375) 2026-07-04 19:47:55 +08:00
github-actions[bot]
dfcd6c8ed5
ci: release packages (#1355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-04 19:17:47 +08:00
liruifengv
7d40d55ac3
chore: downgrade unreleased changesets to patch (#1374) 2026-07-04 19:15:14 +08:00
Haozhe
d111c02ea0
fix(agent-core): cap background shell output to match foreground (#1372)
Background (detached) shell commands were exempt from the 16 MiB output
ceiling, so a runaway background command could fill the disk or crash
the process. Apply the same cap to background shell commands and stop
feeding the disk write chain once it trips. Scope the ceiling to
process tasks so subagent and user-question results, which are appended
once and must be persisted, are left untouched.
2026-07-04 18:48:04 +08:00
Haozhe
5394feaabb
feat: hold print-mode turn until background subagents drain (#1371)
* feat: hold print-mode turn until background subagents drain

In `kimi -p` (print mode), when the main agent ends a turn while background
subagents (`kind === 'agent'`) are still running, hold the turn open and
idle-wait until they finish, flushing their completions into the turn so the
model can react before the run exits.

Previously, the main agent could end its turn after launching background
subagents; the print flow then drained them with their completion
notifications suppressed, so the main agent never saw the results and the run
exited with the work abandoned (e.g. no nomination). This was the root cause
of the swarm-alpha-mining eval failures.

The hold is gated on a new `drainAgentTasksOnStop` session option (set by the
print flow), only affects `kind === 'agent'` background tasks, and is bounded
by `background.printWaitCeilingS`. Backfill / fan-out is handled by
re-enumerating active tasks. Other background task kinds and non-print modes
are unaffected.
2026-07-04 18:44:44 +08:00
Haozhe
e715b1648c
docs(server): document --dangerous-bypass-auth and --keep-alive flags (#1373)
Add the two kimi server run flags introduced in #1368 to the CLI reference (en + zh), including a danger callout for the auth-bypass flag, and add the changeset so the next release notes the feature.
2026-07-04 18:33:22 +08:00
Haozhe
7db88b6f0c
feat(server): add --dangerous-bypass-auth and --keep-alive flags (#1368)
* feat(server): add --dangerous-bypass-auth and --keep-alive flags

- --dangerous-bypass-auth disables bearer-token auth on every REST and
  WebSocket route and advertises it via /api/v1/meta so the web UI skips
  the token prompt; the startup banner drops the token and shows a red
  danger notice
- --keep-alive keeps the daemon running instead of idle-killing after 60s;
  implied by --host / --allowed-host and always on in --foreground mode

* fix(server): address review feedback on bypass-auth

- keep the token and skip the bypass notice when a daemon is reused, since
  the requested --dangerous-bypass-auth flag is not applied to the
  already-running server
- clear the cached dangerous_bypass_auth web state on HTTP 401 so a stale
  bypass value cannot hide the token prompt after the server restarts
  without the flag
2026-07-04 18:03:50 +08:00
liruifengv
23daf0f3c1
revert(pi-tui): restore upstream differential rendering behavior (#1367)
* fix(pi-tui): make the viewport anchor follow above-viewport content shifts

The anchor pins a buffer row index, but an above-viewport length change
shifts the content living at every index below it. The pinned window
then suddenly showed content further along (visible upward creep), and
the rows that slid above the window top were lost: never committed to
scrollback, which holds older bytes at those indices. During streaming,
every above-viewport net shrink (a finished agent row collapsing, a
merged step, a spinner line disappearing) permanently swallowed that
many rows, and the blank area under the input box kept growing.

doRender now scores two hypotheses for such frames — window stayed put
vs window content shifted by the length delta — and when the shift
explains the frame strictly better, moves the anchor with the content:
a pure shift re-anchors with no painting at all (the screen already
shows exactly that content), and a shift with local in-window changes
(spinner/timer rows) repaints the window at the shifted anchor.
Commit order stays continuous, so the exactly-once scrollback invariant
is preserved: no loss, no duplication.

Covered by e2e case07 (above-viewport shift), which fails on the
previous revision; the stale-content unit test now asserts the
follow-the-content window instead of the old swallowed-row behavior.

* revert(pi-tui): restore upstream differential rendering behavior

The fork's viewport/scrollback rendering patches (clamping the diff to
the visible viewport, viewport re-anchoring on collapse, the pinned
anchor with commit-on-advance, cursor visibility guarding, and the
content-shift anchor follow) accumulated interacting edge cases faster
than they could be stabilized: blank screens, duplicated scrollback
spans, vanished rows, and a growing blank area under the input box.

Revert src/tui.ts to the upstream 0.80.2 differential rendering
behavior: a change above the viewport triggers a destructive full
redraw again. Verified line-by-line against the upstream source — the
only remaining divergences are the TypeScript strict-mode syntax
adaptations and the narrow-terminal fixes (Container width clamping
and overwide-line truncation replacing the upstream crash-and-throw),
which are kept.

Also remove the rendering-bug e2e ledger and the shrink test suite
that specified the reverted behavior, restore the pre-fork rendering
tests (the transient-content test asserts the upstream full-redraw
behavior again), and drop the e2e glob from the test script. Editor
input-history and paste-burst changes are untouched.

Known trade-off, accepted for now: the original scroll-position yank
during streaming (destructive redraws emitting ESC[3J) returns; the
rendering rework will restart from this clean baseline.

* chore: downgrade the web thinking-effort changeset to patch
2026-07-04 17:57:43 +08:00
qer
26b90225d2
feat(web): support multi-level thinking effort selection (#1344)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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
Release / Native release artifact (push) Blocked by required conditions
Release / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(web): support multi-level thinking effort selection

Surface each model's declared reasoning efforts (support_efforts /
default_effort) in the web model picker as a segmented control,
replacing the on/off toggle. ThinkingLevel is now an open string, the
model catalog carries the effort metadata to the web app, and the
mobile settings sheet and /thinking command cycle through the
available levels.

* fix(web): preserve persisted thinking level before models load

coerceThinkingForModel returned 'on' for any non-'off' level when the
active model was still undefined (catalog not loaded yet), which
rewrote a persisted/default effort like 'high' to 'on' and silently
dropped the model's declared effort on later prompts. Keep the
requested level as-is until loadModels() re-runs coercion with the
real model.

Addresses Codex P1 review on modelThinking.ts.

* fix(web): coerce stale thinking level against the active model

When selecting another session, the persisted thinking level can be a
boolean 'on'/'off' carried over from a previous model. Deriving the
pill suffix and active segment from that raw value could show
"thinking: off" on an always-on model or hide the concrete effort
behind a bare "thinking" tag. Coerce the level against the current
model before deriving display state.

Addresses Codex P2 review on Composer.vue.

* fix(web): submit coerced thinking level for the prompt's target model

The send paths (submitPromptInternal / steerPrompt) previously submitted
rawState.thinking verbatim, so a value carried over from another session
(e.g. 'max' from an effort model) was sent to a model that doesn't
declare it, even though the composer already showed the coerced default.
Coerce the level against the target model before submitting so the first
turn runs with the level the UI displays.

Addresses Codex P2 review on Composer.vue.

* fix(web): coerce stale thinking in /thinking and mobile settings

Two more surfaces derived their active state from the raw persisted
level, which is stale when the saved level came from a different model:

- /thinking slash command: indexing the raw value (e.g. 'on' from a
  boolean model) into an effort model's segments returned -1 and jumped
  to 'off' instead of advancing from the model's default effort.
- Mobile settings sheet: clamping the raw prop fell back to the first
  segment, showing/selecting 'off' or the first effort while the
  composer and prompt submission coerce to the model default.

Coerce the level against the active model in both places, matching the
composer and send-path behavior.

Addresses Codex P2 reviews on App.vue and MobileSettingsSheet.vue.
2026-07-04 00:57:35 +08:00
qer
ec758c747a
fix(web): make uploaded videos play in the chat (#1343)
* feat(media): materialize video uploads to cache and reference by path

- copy TUI video placeholders into the shared cache instead of
  inlining the original source path
- emit <video path="..."> tags so ReadMediaFile / the provider's
  VideoUploader owns upload behavior
- apply the same cache materialization to server prompt video
  submissions, matching the TUI flow
- update TUI unit tests and server e2e test to assert cache-path
  behavior

* fix(web): make uploaded videos play in the chat

Render the server's <video path> tag as a real video and reconcile the echoed user message so the bubble no longer shows raw markup or a duplicate. Serve file downloads with byte-range support and fetch video bytes with the bearer credential into a blob URL, since browsers cannot authorize a <video> src on their own. Also let users click an uploaded image to open it in the preview panel.

* fix(web): use authenticated source for uploaded image previews

openMediaPreview stored the raw getFileUrl as sourceUrl, and FilePreview renders it with a native <img> that sends no Authorization header, so the enlarge action 401'd for uploaded images. When the media carries a fileId, fetch the bytes through the authenticated API client and preview a blob URL instead, revoking it when the preview is replaced or closed.

* fix(web): ignore stale authenticated media fetches

AuthMedia fetches the file bytes asynchronously; when the component is reused with a new fileId before a prior fetch resolves (e.g. queued thumbnails keyed by index), the older response could still create a blob URL and show the previous file. Add a per-request sequence guard (and an unmount guard) so a stale response is discarded and its blob URL revoked instead of being applied.

* fix(web): gate media path tags on file-store id shape

Treating any standalone <video path="..."> text as an uploaded daemon file and stripping the basename into getFileUrl is only valid for server cache files named after the file-store id (f_…). TUI/ReadMediaFile tags use arbitrary cache names like <uuid>-<label>, and older transcripts may point at paths like /tmp/foo.mp4; those produced a broken /files/<basename> request. Only extract a fileId when the basename matches the file-store id shape, otherwise leave the raw tag as text.

* fix(web): invalidate pending media preview on close

Closing an uploaded-image preview before getFileBlob() resolved left previewRequestSeq untouched, so the fetch callback still passed its seq check, created a blob URL, then skipped attaching it because previewFile was already null — leaking up to the file size until another preview opened. Bump previewRequestSeq on close so the in-flight callback bails before creating the blob URL.

* fix(web): defer authenticated media fetch until near viewport

AuthMedia fetched the full image/video into a Blob on mount whenever a fileId was present, bypassing native loading="lazy" and preload="metadata". Opening a session with several historical large video uploads started many full downloads and held all blobs in memory even if the user never scrolled to or played them. Use an IntersectionObserver to defer the fetch until the element nears the viewport.

* fix(web): revoke preview blob when leaving the file panel

Switching to another detail panel only flips detailTarget and never calls closeFilePreview, so an in-flight getFileBlob could still create a blob URL after the file panel hid, and an already-shown blob URL was held until the next file preview. Check detailTarget before creating the blob URL, and reset/revoke the preview when detailTarget leaves 'file'.

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-07-04 00:36:00 +08:00
liruifengv
36bb506a8f
docs(changelog): sync 0.22.2 from apps/kimi-code/CHANGELOG.md (#1354)
* docs(changelog): sync 0.22.2 from apps/kimi-code/CHANGELOG.md

* docs(agents): require web: prefix for web UI changelog entries
2026-07-04 00:18:02 +08:00
github-actions[bot]
9f4079106c
ci: release packages (#1334)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-03 23:51:39 +08:00
liruifengv
68ad686211
fix(pi-tui): stop scrollback duplication from viewport rewinds (#1353)
* fix(pi-tui): stop scrollback duplication from viewport rewinds

Rewinding the viewport anchor repaints rows that the terminal
scrollback already holds, and the next scroll commits them again —
every rewind duplicated its span. Two paths triggered it during
streaming oscillation (content shrinking then growing back): the
shrink re-anchor rewound immediately, and the clamped differential
path then painted the shifted content through the screen.

Rework the shrink/shift handling around a shared in-place viewport
repaint that never scrolls, with the anchor treated as the scrollback
high-water mark that must never move backward:

- Partial shrinks keep the anchor pinned: the content bottom hovers
  above a bounded blank gap that the next growth naturally fills. No
  rewind means duplication is impossible by construction.
- Only a collapse past the viewport top (compaction, clears) rewinds,
  as nothing sensible could be shown otherwise; the content has
  changed so drastically there that the repainted span is not
  recognizable as a duplicate.
- Above-viewport length changes repaint the visible window in place
  instead of painting through: nothing scrolls, scrollback keeps the
  stale old version, and the anchor only advances when the content
  outgrows the pinned window.
- Deleted-tail changes within a pinned viewport repaint at the pinned
  anchor instead of falling back to a destructive full render.

Pure appends keep flowing through the screen into scrollback, and
equal-length above-viewport changes keep the bounded clamped diff.

* fix(pi-tui): commit skipped rows on anchor advance and guard cursor visibility

Address two review findings on the pinned-anchor rendering:

- An anchor advance (growth past the pinned viewport combined with an
  above-viewport change) repainted the screen in place without
  scrolling, so the rows between the old and new anchor were never
  committed to scrollback and vanished. repaintViewport now paints from
  the old anchor and lets the paint loop scroll the skipped rows out,
  committing each exactly once with fresh content.

- positionHardwareCursor recorded hardwareCursorRow on a logical row
  outside the visible window when the cursor marker sat above a pinned
  viewport (tall editor after a deep shrink), desyncing every later
  differential move. It now hides the cursor and keeps the bookkeeping
  on the real cursor row when the marker is not visible.

Also add an e2e rendering-bug ledger (packages/pi-tui/e2e): one
xterm-emulated repro per production rendering bug, asserting the
renderer invariants (monotonic anchor, exactly-once commit, cursor
bookkeeping sync). The two findings above are case05/case06.

* ci: run the pi-tui node:test suite in a dedicated job

pi-tui's tests (unit + e2e) run on node:test, which the root vitest
run silently skips, so CI never executed them. Add a test-pi-tui job
that runs the package's test script.
2026-07-03 23:49:04 +08:00
liruifengv
e9db9cafcf
feat: record model response id in wire logs (#1349)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: record model response id in wire logs

* chore: include sdk in response id changeset

* chore: include agent-core in response id changeset
2026-07-03 17:15:04 +08:00
Kai
02da587795
feat(cli): wait for background subagents before exiting kimi -p (#1347)
* feat(agent-core): guide the model away from repeating denied or failed tool calls

- system.md: add a diagnose-before-retrying paragraph next to the existing
  permission-denial guidance, covering failed tool calls
- permission: when the user rejects an approval on the main agent, tell the
  model not to re-attempt the exact same call (sub agents already had an
  equivalent hint)

* fix(agent-core): close abandoned tool exchanges and dedupe duplicate tool_use ids

A turn that dies between a recorded tool.call and its paired tool.result
(e.g. a transcript write failure mid-batch) used to leave
pendingToolResultIds open forever: every later message was stranded in
deferredMessages and user input was silently swallowed.

- runOneTurn now defensively closes any dangling tool calls when a turn
  ends (completed, cancelled, or failed), synthesizing an error result
  that names the cause, with a warn log and a tool_exchange_abandoned
  telemetry event
- the projector drops assistant tool calls whose id already appeared
  earlier (first occurrence wins): a duplicate id is wire-invalid on
  strict providers and not repairable by the strict resend; reported via
  the existing projection-repair log and telemetry
- resume-side closePendingToolResults now logs what it closes (warn for
  a mid-history gap, info for the routine trailing interruption)

* chore: add changesets for tool exchange fixes

* fix(agent-core): scope duplicate tool_use id dedup to the strict resend

Unconditional dedup regressed providers that emit per-response counter
ids (e.g. call_0 in every step) and accept their own duplicates: later
tool exchanges silently vanished from the projected history, and a
duplicate call's own recorded result was left dangling.

- the dedupe pass is now opt-in via dedupeDuplicateToolCalls and enabled
  only in strictMessages, so the normal projection keeps the history the
  provider produced
- the pass also drops every tool result after the first for an id, so no
  dangling tool message survives; when the kept call has no result of
  its own, the surviving one is reattached by the adjacency repair
- kosong now classifies the Anthropic "tool_use ids must be unique" 400
  as a recoverable request-structure error so it triggers the strict
  resend

* feat(cli): wait for background subagents before exiting kimi -p

When `background.keep_alive_on_exit` is enabled, `kimi -p` now waits for
all background subagents to reach a terminal state before exiting, bounded
by `background.print_wait_ceiling_s` (default 3600s). This lets concurrent
background subagents run to completion in single-turn runs instead of being
torn down when the main agent's turn ends.
2026-07-03 16:56:16 +08:00
liruifengv
3ed22e35a4
feat: redesign the subagent card (#1345)
Stable height across running, done, failed and backgrounded states: all share the same header + one-line tool summary + two-row content window, so the card no longer shrinks when a run finishes. Add a braille spinner in the header while active, collapse sub-tool calls into a one-line summary, and have the two-row window follow the live stream (tool output, text, or thinking) instead of showing thinking and text side by side. Mute the window tones so a brief text or tool-output segment no longer flashes white against dim thinking.
2026-07-03 16:54:21 +08:00
Kai
175b95f3af
fix(agent-core): route image-compression captions through hidden system reminders (#1348)
* fix(agent-core): route image-compression captions through hidden system reminders

Prompt ingestion (server upload/base64 route, TUI paste, ACP) annotates a
compressed image with an inline <system> caption inside the user's own
message. That raw markup rendered verbatim in every user-visible history
projection (TUI session replay, web UI) and leaked into session titles.

Split the caption out at the appendUserMessage chokepoint and deliver it
through the built-in system-reminder injection (origin
{kind: 'injection', variant: 'image_compression'}), which every UI already
hides. The model still receives the full note; ingestion sites and the wire
protocol are unchanged. Session titles/lastPrompt strip the caption the same
way. Tool-result captions (MCP) keep the established <system> convention.

Covered by unit tests plus an end-to-end smoke suite that drives
rpc.prompt/steer through the real turn pipeline and asserts the provider
wire request, stored history, replay records, and resume parity.

* chore: tighten changeset wording per gen-changesets conventions
2026-07-03 16:21:10 +08:00
Kai
021786f5a2
fix(kaos): enrich PATH from the user's login shell at startup (#1339)
* feat(agent-core): strengthen the language-matching rule in the default system prompt

* chore: refine changeset wording

* fix(kaos): enrich PATH from the user's login shell at startup

When kimi-code is launched from a context that skipped the user's shell
profile (GUI launchers, non-login parent shells), process.env.PATH misses
entries like /opt/homebrew/bin, so commands spawned by the Bash tool
cannot find user-installed tools such as gh.

LocalKaos.create() now probes the user's login shell once
($SHELL -l -c env, 5s timeout, memoised) and appends the missing PATH
entries to process.env.PATH. Existing entries keep their order and
priority; probe failures silently leave PATH untouched. Windows is
skipped: the problem is specific to POSIX login-shell profiles.

* fix(kaos): fall back to the account login shell when $SHELL is unset

launchd/daemon launches can leave $SHELL unset or blank — the very
contexts whose PATH is impoverished — so the login-shell PATH probe
would give up exactly where it matters most. Resolve the shell from the
OS user database (os.userInfo().shell) before giving up; lookups that
throw (uid without a database entry) or yield nologin shells degrade
silently as before.

* fix(kaos): preserve empty PATH components when merging login-shell PATH

POSIX command lookup treats an empty PATH component (leading colon,
trailing colon, or double colon) as the current directory. The merge
previously filtered those out of the current PATH and rewrote the value
even when nothing was appended, silently dropping cwd lookup for users
who rely on it.

Keep the current PATH string verbatim as the prefix, append only the
missing login-shell entries, and skip the env write entirely when the
login shell contributes nothing — an unset PATH stays unset, a set PATH
is never rewritten. Empty login-shell components are still never
imported.

* fix(kaos): only import absolute login-shell PATH entries

A `.` or relative component in the login-shell PATH is cwd-dependent
lookup with another spelling, and LocalKaos runs commands from arbitrary
workspace directories — importing one would let a command name resolve
from an untrusted project cwd. Tighten the merge's skip condition from
"empty" to "not absolute", which subsumes the empty-component check.

* fix(kaos): invoke the login-shell probe's env by absolute path

A bare `env` inside `$SHELL -l -c` resolves through the inherited PATH
from the workspace cwd. If that PATH carries a cwd-dependent component
(which the merge deliberately preserves), a repo-planted `env` binary
would run automatically at session startup and could feed the probe an
arbitrary PATH. /usr/bin/env is guaranteed on mainstream POSIX systems
and also bypasses profile function shadowing.
2026-07-03 15:20:38 +08:00
Kai
e2fe62a5ef
fix(agent-core): harden tool_use/tool_result exchange integrity (#1340)
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / build (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core): guide the model away from repeating denied or failed tool calls

- system.md: add a diagnose-before-retrying paragraph next to the existing
  permission-denial guidance, covering failed tool calls
- permission: when the user rejects an approval on the main agent, tell the
  model not to re-attempt the exact same call (sub agents already had an
  equivalent hint)

* fix(agent-core): close abandoned tool exchanges and dedupe duplicate tool_use ids

A turn that dies between a recorded tool.call and its paired tool.result
(e.g. a transcript write failure mid-batch) used to leave
pendingToolResultIds open forever: every later message was stranded in
deferredMessages and user input was silently swallowed.

- runOneTurn now defensively closes any dangling tool calls when a turn
  ends (completed, cancelled, or failed), synthesizing an error result
  that names the cause, with a warn log and a tool_exchange_abandoned
  telemetry event
- the projector drops assistant tool calls whose id already appeared
  earlier (first occurrence wins): a duplicate id is wire-invalid on
  strict providers and not repairable by the strict resend; reported via
  the existing projection-repair log and telemetry
- resume-side closePendingToolResults now logs what it closes (warn for
  a mid-history gap, info for the routine trailing interruption)

* chore: add changesets for tool exchange fixes

* fix(agent-core): scope duplicate tool_use id dedup to the strict resend

Unconditional dedup regressed providers that emit per-response counter
ids (e.g. call_0 in every step) and accept their own duplicates: later
tool exchanges silently vanished from the projected history, and a
duplicate call's own recorded result was left dangling.

- the dedupe pass is now opt-in via dedupeDuplicateToolCalls and enabled
  only in strictMessages, so the normal projection keeps the history the
  provider produced
- the pass also drops every tool result after the first for an id, so no
  dangling tool message survives; when the kept call has no result of
  its own, the surviving one is reattached by the adjacency repair
- kosong now classifies the Anthropic "tool_use ids must be unique" 400
  as a recoverable request-structure error so it triggers the strict
  resend
2026-07-03 14:26:57 +08:00
liruifengv
9091627257
fix(tui): avoid submitting rapid multi-line paste bursts (#1305)
* fix(tui): avoid submitting rapid multi-line paste bursts

* chore(tui): remove paste burst settings picker

* fix(tui): reset paste burst on DEL backspace
2026-07-03 14:21:02 +08:00
qer
01b65bdddc
feat(web): open design-system easter egg at /design-system route (#1328)
* feat(web): open design-system easter egg at /design-system route

Replace the long-press-logo iframe overlay, which loaded a separately maintained static design-system.html, with a real /design-system route. The new view aliases to the product design tokens, so the design system is maintained in one place. Adds vue-router for the route, removes the duplicate static HTML copies, and exempts the showcase view from the style scanner.

* fix(web): lazy-load the design-system view

Address Codex review: load the 2.4k-line showcase via defineAsyncComponent so it is code-split and fetched only when /design-system is visited, instead of bloating the initial bundle for every load.

* chore(nix): update pnpmDeps hash for vue-router

Adding vue-router changed pnpm-lock.yaml, which invalidated the fetchPnpmDeps hash. Set it to the value reported by the nix build.

* fix(web): return to app root when closing the design system

In-page nav anchors push hash history entries, so router.back() only stepped through them and required multiple clicks to leave. Navigate to / directly; the client lives above the route so session state is preserved.

* feat(web): let /design-system bypass the auth gate

Render the design-system route ahead of the auth/server gates and skip the /login rewrite for it, so a direct deep link shows the showcase even before the app is OAuth-ready. The view is read-only and holds no user data, matching the old static page behavior.

* fix(web): make the design-system root scrollable

The route renders inside .app-shell (height:100dvh; overflow:hidden), so a position:fixed root could be clipped. Make .ds-page a flex item that fills the shell and scrolls internally, so later sections and hash navigation remain reachable.

* fix(web): preserve /design-system during initial session load

On load the app auto-selects the first session and rewrites the URL to /sessions/<id>, which clobbered a deep-linked /design-system. Skip the session URL write while on the design-system route so refreshing keeps the route.

* fix(web): restore the prior session URL when leaving the design system

Record the URL on entry to /design-system and navigate back to it on close, so a /sessions/<id> URL is preserved instead of falling back to /. This also keeps the earlier fix that sidesteps in-page hash anchors.

* fix(web): capture the real browser URL before opening the design system

Session URLs are rewritten via the native history API, so vue-router's from.fullPath can be stale ('/' after a session is selected). Read window.location on entry instead, falling back to / for a direct deep link.

* fix(web): sync the active session URL when leaving the design system

After a direct deep link to /design-system the app auto-selects a session but the address stays '/'. On close, fall back to the active session's canonical URL so the address bar matches the displayed session.

* fix(web): capture the design-system return path at logo entry

Capture window.location in the logo long-press handler instead of a navigation guard. The guard fired on browser Back/Forward too and overwrote the return path with the design-system URL itself; capturing only at the explicit entry action avoids that.

* fix(web): replace the design-system route when closing

Use router.replace instead of push for the captured return URL, so closing does not append a second app URL after /design-system and the browser Back button returns to the page before the easter egg.

* revert(web): drop the design-system auth-gate bypass

Keep /design-system behind the auth gate so it has a single in-app entry (logo long-press). This removes the deep-link machinery (auth exemption, active-session fallback) that drove most of the URL/session edge cases, while keeping the lazy-loaded route, the flex scroll fix, the logo-entry return-path capture, and replace-on-close.

* refactor(web): rebuild the design-system easter egg as an in-app overlay

The easter egg is a hidden, read-only spec viewer opened by long-pressing the logo; it does not need to be a URL route. Replace the vue-router approach with an overlay: Sidebar opens a lazy-loaded DesignSystemView in a body-teleported full-screen overlay, dismissed by the Back button or Escape. This removes vue-router, the route, the auth-gate exemption, the session-URL guards, and the return-path machinery — none of which the feature needs.

* chore(nix): restore pnpmDeps hash after removing vue-router
2026-07-03 13:42:27 +08:00
Kai
84d8d5b063
feat(agent-core): make compaction notes capture a forward plan, not just the next step (#1342)
Compaction runs at the point of maximum context for the task, and the next
turn resumes with less. So the handoff note now records the plan for the
remaining work — upcoming steps, settled decisions, and foreseeable obstacles,
plus any work that can be pre-committed — instead of only the immediate next
command. Update the affected compaction snapshots and one hardcoded
input-token assertion (the instruction is ~163 tokens longer).
2026-07-03 13:38:09 +08:00
Kai
276407d2a4
feat(agent-core): strengthen the language-matching rule in the default system prompt (#1338)
* feat(agent-core): strengthen the language-matching rule in the default system prompt

* chore: refine changeset wording
2026-07-03 11:58:02 +08:00
wenhua020201-arch
4498ea9ee3
docs: use kimi-for-coding in model overrides example; drop dangling experimental refs (#1337)
* docs: use kimi-for-coding in model overrides example

kimi-for-coding is the stable public model ID users actually configure;
kimi-k2 is the underlying model name and shouldn't appear in the config
example. Demonstrate overrides with max_context_size and display_name.

* docs: drop dangling references to commented-out experimental section

The `## experimental` section was commented out when micro_compaction
was removed, but the top-level fields table and the intro sentence still
linked to the now-dead #experimental anchor. Remove those references.
2026-07-03 11:41:03 +08:00
liruifengv
4c1d0a1633
fix: hide the background updater console window on Windows (#1336)
A detached Windows child gets its own console window. With the shell: true introduced for the CVE-2024-27980 fix, a passive background auto-update could flash a command window even though stdio is ignored. Set windowsHide on the detached background child so the silent updater stays silent. The foreground `kimi upgrade` path is interactive (stdio: inherit, non-detached) and reuses the parent console, so it is left unchanged.
2026-07-03 11:32:31 +08:00
liruifengv
93f16c32d7
fix: run package-manager upgrades through a shell on Windows (#1332)
On Windows, npm/pnpm/yarn are .cmd shims. Since Node's CVE-2024-27980 fix, spawning a .cmd/.bat without a shell throws EINVAL, which broke `kimi upgrade` and background auto-install on Windows. Pass shell: true on win32 so the install runs through the shell.
2026-07-03 11:03:46 +08:00
liruifengv
ceeebc1f85
docs(changelog): sync 0.22.1 from apps/kimi-code/CHANGELOG.md (#1325)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-07-02 22:20:57 +08:00
github-actions[bot]
508384502d
ci: release packages (#1291)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 21:56:42 +08:00
qer
5441ad1838
feat(web): collapse loaded sessions back to the first page (#1322)
* feat(web): collapse loaded sessions back to the first page

The workspace session list's load-more control was one-way: once expanded, the only way to hide the extra sessions was to collapse the whole group. Add a Show less / Show all toggle so an expanded list can be collapsed back to its first page and re-expanded without losing the loaded data.

Restyle the control as a session-row-shaped pill whose label aligns with the session titles, per design-system section 07, and mirror the behavior in the mobile switcher.

* fix(web): preserve first-page capacity for sparse workspaces

The collapse target was seeded with the exact number of sessions loaded on first paint, which is 0 for an empty workspace and below a full page for a sparse one. Newly created sessions are prepended without bumping that count, so a workspace that was empty on load would hide its first new session behind a Show all control, and a sparse one would hide an older row on each new session even when it had never paged.

Floor the collapse target at one full page so the first-page capacity is preserved.

* fix(web): keep the active session visible in a collapsed group

A collapsed workspace only rendered its first page, so an older session selected from outside the pagination flow — Cmd/Ctrl-K search (loadAllSessions) or a URL deep link (fetchSessionIntoList) — was marked active but had no visible row in the sidebar until the user manually clicked Show all.

Include the active session in the collapsed view (appended in newest-first order) on both the desktop sidebar and the mobile switcher, so selection and search never navigate to a hidden row.
2026-07-02 21:51:28 +08:00
qer
444e6b15f0
fix(web): cap live session subscriptions to reduce lag with many sessions (#1320)
* fix(web): cap live session subscriptions to reduce lag with many sessions

Every opened session stayed subscribed to its WebSocket event stream across reconnects, so opening hundreds of sessions turned background events into a constant reducer and sidebar recompute storm. Keep only the four most-recently-opened sessions subscribed; evicted sessions resume from their tracked cursor on re-open.

* fix(web): reset cursor for sessions evicted from the subscription cap

Some session events (status_changed, meta_updated, ...) are broadcast to every connection and still advance lastSeqBySession for an unsubscribed session. If an evicted session emits per-session durable events and then a global event, the cursor jumps past the missed events, so resuming from it later would skip them and leave the reopened session stale. Track evicted sessions and reset their cursor on the next re-subscribe so the daemon replays or snapshots what was missed.

* fix(web): rebuild evicted sessions from a snapshot on re-open

Two fixes for the subscription cap:

- Re-opening a session that was evicted now rebuilds it from a snapshot instead of resuming from seq 0. Replaying from zero made the projector regenerate assistant/tool message ids, which duplicated the already-loaded transcript; resuming from the kept cursor could skip per-session events that arrived while unsubscribed.

- Eviction now skips the active session wherever it sits in the list, instead of breaking when it lands at the tail. First-time opens retain only after an awaited snapshot, so rapid clicks can complete out of order and leave the active session at the tail, which previously let the list grow past the cap.

* fix(web): keep stale cursor marker until snapshot succeeds

Re-opening an evicted session deleted the stale-cursor marker before the snapshot ran. If the snapshot failed transiently, the marker was gone and a later re-open would fall back to subscribeToSessionEvents, resuming from a cursor that may have skipped per-session events while evicted. Read the marker instead of deleting it, and let syncSessionFromSnapshot clear it once the snapshot succeeds.
2026-07-02 21:48:47 +08:00
liruifengv
3ff401261f
chore: refine changeset wording (#1323)
Simplify the image compression and compaction handoff changesets, and set the image compression bump to patch.
2026-07-02 21:18:32 +08:00
liruifengv
b40bb71399
fix(pi-tui): repaint viewport in place when content collapses above it (#1315)
* fix(pi-tui): repaint viewport in place when content collapses above it

When content shrinks past the viewport top while a line above the
viewport also changes, the clamped differential path left the render
loop empty, cleared deleted lines past the screen bottom, and desynced
the cursor anchor — leaving the viewport blank with the input box gone
until a full redraw.

Repaint the visible viewport in place for that case (no ESC[3J, so the
scrollback and the user's scroll position are preserved), and clamp
deleted-line clearing to the screen bottom so it can never scroll
untracked.

* fix(kimi-code): clear the screen fully on session reset

The collapse repaint in pi-tui intentionally preserves scrollback, so
/new, /clear, and session switches no longer got a clean screen as a
side effect of the destructive full redraw — the previous session's
text stayed above the welcome banner.

Session resets want a pristine screen, so force a destructive full
render explicitly instead of relying on the renderer's shrink
behavior.

* fix(pi-tui): delete kitty images straddling the viewport top on collapse repaint

A multi-row kitty image can start above prevViewportTop while its
reserved rows are still visible. The collapse repaint's image-delete
range started at prevViewportTop and missed the image line carrying the
id, leaving a stale overlay that also dropped out of
previousKittyImageIds tracking. Widen the range to include such a
straddling block.

* chore: shorten session reset changeset wording

* fix(pi-tui): re-anchor the viewport whenever content shrinks below the screen bottom

previousViewportTop only ever grows during normal rendering, so after a
shrink the content bottom could hover above the screen bottom, leaving
dead rows that nothing repaints. Upstream masked this by frequently
doing destructive full redraws, which re-anchored as a side effect; the
fork removed those redraws without replacing the re-anchoring.

Generalize the collapse repaint into a re-anchor check at the top of
the differential path: whenever prevViewportTop exceeds
max(0, newLines - height), repaint the visible viewport in place with
the tail of the new content. The input area snaps back to the screen
bottom, scrollback and the user's scroll position stay intact (no
ESC[3J), and the previous collapse branch becomes a defensive
destructive fallback.

Update the three renderer tests that encoded the old behavior
(destructive redraw on shrink, viewport hover after clamped shrink) to
assert the re-anchored behavior instead.

* fix(kimi-code): full repaint on ctrl+o expansion toggle

Expanding tool output shifts content above the viewport; the clamped
differential render paints the shifted content through the screen,
stacking a duplicate copy below the stale one in scrollback on every
toggle. The toggle is a deliberate user action (like /clear), so do a
destructive full render instead: scrollback holds exactly one copy and
the expanded output stays readable by scrolling up.

* chore: simplify user-facing changeset wording
2026-07-02 20:52:37 +08:00
qer
e8ab7ca786
fix(web): stop session row from shifting on hover (#1319)
The session row swapped the relative time for the kebab on hover using display:none, which dropped the time from layout and appended the kebab at the end. Because the time is variable-width (2h / 5m / just now) and the kebab is a fixed 26px, the right region reflowed: status badges shifted and the title's truncation changed, causing visible jitter.

Place the time and kebab in one inline-grid cell (grid-area:1/1) and swap them via visibility instead, so the slot width stays max(time width, 26px) across hover. The badges and title no longer reflow. Documented in the design system (section 07 Session row) as a trailing action slot rule.
2026-07-02 20:40:47 +08:00
qer
5322c63889
fix(web): trim redundant and incorrect tooltips (#1316)
* fix(web): trim redundant and incorrect tooltips

Drop hover tooltips that only restated a button's accessible label, and remove the permission-pill tooltip that described cycling modes while the control actually opens a dropdown. Keep tooltips that reveal truncated text or explain status.

* fix(web): remove remaining ChatHeader tooltips

Drop the session-title, git-branch, and open-PR tooltips in ChatHeader so the header has no hover tooltips. Broaden the changeset wording to cover the wider trim.

* fix(web): keep tooltips from getting stuck on unmount

Tooltip attached its mouseleave listener to the slotted trigger element once, so if that element was removed (e.g. a v-if toggled while hovering a tool-call path) the open bubble never received mouseleave and stranded on screen. Re-sync the listener to the live slotted element via a MutationObserver and close the tooltip whenever the trigger changes.

* chore: add changeset for stuck-tooltip fix

* fix(web): restore session title tooltip in ChatHeader

The session title is truncated with an ellipsis when it exceeds the header width, so the tooltip was the only way to read the full name from the header. Restoring it keeps the truncation-revealing hint while the redundant git/branch/open-PR tooltips stay removed.
2026-07-02 19:53:01 +08:00
Kai
78a058acd2
chore(agent-core): remove experimental micro compaction (#1317)
* chore(agent-core): remove experimental micro compaction

* fix(docs): drop micro compaction row from env-vars table
2026-07-02 19:50:51 +08:00
liruifengv
b40649b2ae
chore(tui): remove shortcut newline telemetry (#1311)
The prompt editor no longer needs custom newline interception: the underlying editor handles Shift+Enter and Ctrl+J natively. Drop the interception along with the shortcut_newline telemetry hook.
2026-07-02 19:38:04 +08:00
Kai
4dd926b0ac
fix(agent-core): recover sessions bricked by orphan tool results (#1308)
* fix(agent-core): recover sessions bricked by orphan tool results

A stray `tool` message with no preceding assistant `tool_calls` permanently
bricked a session on OpenAI-compatible providers: every turn re-sent the same
malformed history and got a 400, and switching model/provider did not help.

Two independent gaps caused this:

- kosong did not recognize the OpenAI / DeepSeek / vLLM / Qwen phrasings of the
  tool-exchange structural 400 (`role 'tool' must be a response to a preceding
  message with 'tool_calls'` and the mirror `assistant message with 'tool_calls'
  must be followed by tool messages`), so the post-400 strict-resend fallback
  that drops the orphan never fired.

- The legacy-restore compaction path kept a verbatim tail
  `history.slice(compactedCount)`; when the cut landed inside a tool exchange the
  tail began with an orphan tool result whose assistant was summarized away. The
  normal projection does not repair a leading orphan, so the malformed history
  was baked in and re-sent every turn.

Recognize the additional phrasings so the strict resend un-bricks any session,
and trim leading tool results from the legacy-restore tail so the orphan is
never persisted in the first place.

* fix(agent-core): drop orphan tool results at the projection boundary

Rework the legacy-restore half of the fix based on review feedback: mutating
`_history` at restore time desyncs every consumer that models the history from
the wire records — the transcript reducer's fold length would overcount and
make MessageService skip unflushed live-tail messages.

Keep the restored history faithful to the wire records instead, and drop a
`tool` result whose call is nowhere in the history at the projection boundary,
on every request-building projection: the normal wire (`messages`), the
post-400 strict resend (`strictMessages`), and the compaction summarizer. An
orphan is wire-invalid on strict providers and useless to the model either
way, so it never reaches a provider — no longer relying on recognizing the
provider's 400 phrasing to recover. Fragment projections (e.g. token-estimating
a history slice) leave results untouched, since a matching call may
legitimately sit outside the slice.
2026-07-02 19:29:05 +08:00
Kai
329846c569
feat(agent-core): keep head and tail of user messages during compaction (#1313)
* feat(agent-core): keep head and tail of user messages during compaction

Compaction used to keep only the most recent 20k tokens of real user
input, so the original task statement was the first thing to vanish in
long sessions. Now, when the user-message pool fits the 20k budget it is
still kept whole; when it overflows, the oldest 2k tokens and the most
recent 18k are kept instead, with an elision marker between the two
segments telling the model what was omitted and that the summary covers
it. The summary prefix and the default system prompt describe the new
shape as well.

The new `keptHeadUserMessageCount` record field keeps restore and the
wire-transcript folded length consistent: records without it (written by
older versions) restore with the original tail-only selection that
produced them, and the vis model-mode projection mirrors the same
head/marker/tail rebuild.

* style(agent-core): drop redundant spread over slice in head selection
2026-07-02 19:24:37 +08:00
qer
d1275557f9
docs(web): translate design system to English and add anti-slop guidance (#1302)
* docs(web): add anti-slop design guidance inspired by taste-skill

Codify one icon family (Remix) with no hand-rolled SVG in §02. Expand the banned AI-tell list in §01 (AI-purple/blue glow, infinite-loop micro-animations). Add button and form contrast requirements to §08. Add a 'declare design intent (Design Read) first' callout.

* docs(web): resolve merge conflict with main

Reset design-system.html to latest main (which includes the merged #1300 and #1301) and re-apply the taste-skill design guidance on top, so the branch merges cleanly.

* docs(web): translate design system to English

Translate the entire design-system.html from Chinese to English, preserving all HTML structure, CSS, code blocks, SVGs, the scroll-spy script, and token values. The design system is now a single English document.
2026-07-02 19:16:04 +08:00
Kai
0fc0ae380b
feat(agent-core): announce image compression and keep originals readable (#1304)
* feat(agent-core): announce image compression and keep originals readable

Every image ingestion point (ReadMediaFile, MCP tool results, clipboard
paste, REST upload/inline base64, ACP) now places a <system> caption next
to a compressed image stating the original vs. delivered dimensions, byte
size, and format, so downsampling is never silent to the model.

Originals stay readable: file uploads point at the stored blob, and
in-memory images are persisted into the session's media-originals dir
(content-addressed, size-capped, removed with the session; temp-dir
fallback when no session is known).

ReadMediaFile gains region (crop in original-image pixel coordinates,
delivered at full fidelity) and full_resolution (skip downscaling, with
an explicit error over the per-image byte limit), so the model can zoom
into fine detail instead of silently degrading on large images.

* fix(agent-core): exempt compression captions from the MCP text budget

The caption announcing an image's compression was inserted before the
shared 100K text budget was applied, so a chatty MCP result (page text +
screenshot) consumed the budget first and the caption was evicted — or
sliced mid-string into an unclosed <system> fragment — while the
downsampled image survived, silently reintroducing the exact degradation
the caption exists to report, and orphaning the persisted original.

Split the size-limit pass in two and reorder the pipeline: the text
budget now runs on the tool's own text BEFORE compression inserts
captions (exempt by construction), and the per-part binary cap still
runs after compression so compressible screenshots are kept.

* fix(agent-core): harden crop error reporting and document readback semantics

- cropImageForModel rejects non-finite region coordinates with a clean
  message instead of surfacing the codec's internal validation dump
- the full_resolution and cropped-region over-budget errors now include
  exact byte counts alongside the rounded sizes, so a file a hair over
  budget no longer reads "is 3.8 MB, over the 3.8 MB limit"
- read-media.md notes that re-reading a file without region or
  full_resolution reproduces the same downsampled image
2026-07-02 19:07:56 +08:00
liruifengv
77eb3a9fe4
feat(tui): include shell commands in input history (#1295)
* feat(tui): include shell commands in input history

Shell commands entered through the `!` prompt are now saved to input history. Recalling one restores bash mode, and in bash mode Up only cycles through previous shell commands while a normal prompt browses all history.

* docs(interaction): document shell command recall in input history

Note that shell commands are now saved to input history and can be recalled in Shell mode, in both the English and Chinese interaction guides.

* feat(pi-tui): add setHistoryFilter and onRecall to editor history

Add two first-class hooks to the editor's history navigation: setHistoryFilter to limit which entries Up/Down visit, and onRecall to decorate a recalled entry before it is shown. Draft restore, direction-aware cursor placement, and undo behavior are unchanged.

* refactor(tui): use pi-tui history filter for shell command recall

Replace the CustomEditor navigateHistory shadow with pi-tui's setHistoryFilter + onRecall hooks, wired in the editor-keyboard controller. This keeps pi-tui's draft-restore and direction-aware cursor behavior intact (the shadow dropped both) and moves the shell/prompt filtering and mode-restore logic into the business layer.

* feat(pi-tui): save and restore host state with the history draft

Add onHistoryDraftSave/onHistoryDraftRestore hooks so hosts can stash their own state when entering history browsing and restore it when the user navigates back to the draft. The saved host state is discarded when browsing ends any other way (typing, submit), mirroring the editor draft lifecycle.

* fix(tui): restore input mode when returning to the history draft

Wire pi-tui's history draft save/restore hooks to the editor input mode. Without this, recalling a shell entry and then pressing Down back to an empty draft left the editor in bash mode, so the next typed message was submitted as a shell command.

* fix(pi-tui): capture host draft state before running the history filter

Fire onHistoryDraftSave before the history filter runs when entering browse, so the host's filter can read the browse-entry mode rather than a mode that changes as entries are recalled. The captured state is still only committed once a matching entry is found.

* fix(tui): lock history filter to the browse-entry mode

Lock the history filter to the input mode captured when entering browse. Previously the filter read inputMode live, so after recalling a shell entry (which flips to bash mode) a second Up would only show shell commands.
2026-07-02 17:59:26 +08:00
liruifengv
2639786ce5
fix(pi-tui): prevent crashes on very narrow terminals (#1303)
* docs: add pi-tui narrow-width fix plan

* fix(pi-tui): stop wordWrapLine infinite recursion on wide graphemes at width 1

* docs: extend pi-tui narrow-width plan with emoji grapheme regression coverage

* fix(pi-tui): clamp container render width to a minimum of 1

* fix(pi-tui): truncate overwide rendered lines instead of throwing

* perf(pi-tui): fast-path overwide line detection and enlarge width cache

* test(pi-tui): assert exact truncated viewport in overwide line test

* docs: record review amendments in pi-tui narrow-width plan

* test(pi-tui): add editor narrow-width regression tests

* docs: record task 4 review amendments in pi-tui narrow-width plan

* fix(pi-tui): guard blank-line padding against negative widths

* docs: record task 5 review amendments in pi-tui narrow-width plan

* docs(pi-tui): document local divergences from upstream

* chore: add changeset for pi-tui narrow width fixes

* docs(pi-tui): point Text guard test to its actual test file

* test(pi-tui): translate narrow-width test comments to English

* docs: remove internal pi-tui narrow-width plan

* docs(pi-tui): translate AGENTS.md to English
2026-07-02 17:14:11 +08:00
qer
c3653a1c50
refactor(web): show an up arrow on the composer send button (#1301)
* refactor(web): show an up arrow on the composer send button

* docs(web): regenerate design-system catalog with the send up-arrow
2026-07-02 15:56:48 +08:00
qer
7fa18c9e45
docs(web): sync design system with the Remix icon switch (#1300)
* docs(web): sync design system with the Remix icon switch

Update the §02 icon guidance to describe Remix Icon (fill, 24x24, registry-sourced) and drop the stale 'line-icon' wording. Convert the §03 component-gallery demo icons from hand-drawn stroke SVGs to Remix fill icons. Clarify that the workspace-group add button reveals on hover or keyboard focus for accessibility. Sync public/design-system.html with design/.

* docs(web): show composer send button as an up arrow
2026-07-02 15:34:33 +08:00
Kai
021de5433b
feat(agent-core): align model-facing prompts with actual tool behavior (#1296)
* feat(agent-core): align model-facing prompts with actual tool behavior

A hunk-by-hunk accuracy pass over every model-visible prompt surface
(system.md, tool .md descriptions, zod describes, profile role prompts,
and injected reminder strings), with each claim verified against the
implementation and, where possible, empirically (ripgrep semantics).

Fix descriptions that drifted from the code:
- Grep `glob` matches against each file's absolute path, so
  `src/**/*.ts` silently matches nothing — document the working forms
- Glob `path` accepts relative paths; results are files-only
- FetchURL no longer promises a content-type-to-mode mapping the
  default provider does not honor
- cron: a pinned-date 5-field expression repeats yearly unless
  `recurring: false`; drop a bench-only env knob from cron-list
- skill `args` expansion covers $NAME/$1/$ARGUMENTS and the trailing
  ARGUMENTS: line; goal reminder no longer cites a nonexistent
  developer-message channel

Disclose enforced-but-silent behavior:
- cron fires deliver only while the session is idle; expressions with
  no fire within 5 years are rejected at create time
- VCS metadata directories are always excluded from Glob/Grep, even
  with include_ignored; sensitive-file guard exemptions
  (.env.example/.env.sample/.env.template, public SSH keys)
- large images may be downsampled while the <system> block reports
  original dimensions; subagent summaries under the length floor are
  sent back for expansion; background-disabled Agent calls are
  rejected before launch; AGENTS.md beyond ~32 KB triggers a
  performance warning (surfaced in the /init prompt)

Resolve cross-surface contradictions:
- AskUserQuestion background describe/envelope no longer teach polling
- AgentSwarm subagent_type documents that resume keeps original types
- bash.md scopes &&-chaining to dependent commands and steers
  independent read-only commands to parallel calls
- the shared system prompt no longer names tools that read-only
  subagent profiles lack

Add missing guidance:
- denied/rejected tool calls mean the user declined that action —
  adjust, don't retry or route around (root agent)
- plan subagent now knows it is read-only; coder subagent knows its
  final message is the entire handoff; explore subagent knows web
  tools are in scope
- gh CLI routing for GitHub-hosted work; FetchURL login-wall note;
  a dual-use content-safety boundary; scope discipline,
  surrounding-idiom, and dependency-verification norms; file:line
  citation convention; progress notes on long multi-phase tasks

* fix(agent-core): let the model fetch a background answer after the completion notice

In sessions with background persistence (any agent with a homedir), a
background question's answer is flushed to output.log and the completion
notification carries an <output-file> pointer, not the answer text. The
previous envelope wording ("use TaskOutput only to re-read the answer if
you missed the notification") gated the normal post-completion fetch
behind a missed-notification condition, so a model could acknowledge the
notice and continue without ever reading the user's answer.

Reword the envelope to state that the completion notice may carry a
pointer and to direct the model to read that file (or call TaskOutput
once) for the answer, while still forbidding polling before the user
responds. Align the background param describe the same way ("notified
automatically" rather than "the answer arrives", polling scoped to the
pending window).

* fix
2026-07-02 14:51:30 +08:00
qer
0b4c05e394
fix(kimi-desktop): make the sidebar header buttons clickable (#1289)
On macOS the sidebar header has a hidden title bar, so the whole header doubles as the window-drag region (matching the chat header). The collapse / settings buttons sit inside it and were being captured by the drag, so they would not click. Mark the buttons and the logo as no-drag inside the drag-region header — the same no-drag-inside-drag pattern ChatHeader.vue already uses — so they receive clicks normally.
2026-07-02 14:39:52 +08:00
qer
6a469b3e07
refactor(web): replace hand-written icons with Remix Icon (#1293)
* refactor(web): replace hand-written icons with Remix Icon

Generate a tree-shaken Remix Icon subset at build time via @iconify/utils + @iconify-json/ri, keeping the <Icon>/iconSvg() API.

Add a chat-new icon for the new-chat buttons and reveal the workspace 'new chat in group' button on hover. Unify the message copy and undo buttons (matching hover style and tooltip, drop the undo hover label, align sizes). Switch the mobile switcher kebab to the horizontal dots icon and tweak sidebar search colors. Regenerate the design-system icon catalog.

* fix(web): address PR review feedback

Restore accessible names (aria-label) on the message copy and undo buttons. Keep the workspace add button reachable for keyboard users by revealing it on header focus-within. Update the nix pnpmDeps hash for the newly added icon dependencies.

* fix(web): address follow-up review feedback

Keep the workspace add/more buttons focusable without hover by revealing them via opacity instead of display:none, so keyboard and non-hover users can reach the control.

Drop explicit .ts extensions in icon imports to satisfy oxlint, and read the design-system icon catalog directly from the generated icon data.
2026-07-02 14:20:17 +08:00
Kai
93ec6cb652
fix(kosong): recognize OpenAI-compatible tool_call_id 400 as a recoverable tool-exchange error (#1292)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Moonshot / Kimi (OpenAI-compatible) rejects a history whose tool message
references a tool_call_id with no matching tool_calls entry in the preceding
assistant message as `400 tool_call_id  is not found`. The
TOOL_EXCHANGE_ADJACENCY_MESSAGE_PATTERNS only covered Anthropic's
tool_use/tool_result phrasing, so isRecoverableRequestStructureError returned
false, the strict-resend fallback in executeLoopStep never fired, and the
session stayed permanently stuck re-sending the same rejected history every
turn (observed in the field after a manual compaction busted the prompt cache
and forced full revalidation of a latently misordered prefix).

Add the tool_call_id-anchored pattern so the whole recovery chain — strict
projection (adjacency repair, orphan-result drop, synthetic results) plus the
one-shot resend — now also covers the default provider. Covered by classifier
unit tests and an e2e resend-and-recover case.
2026-07-02 13:52:44 +08:00
Kai
ea55911062
feat(agent-core): sharpen the compaction handoff prompt (#1283)
* feat(agent-core): sharpen the compaction handoff prompt

Refine the first-person handoff note the model writes at compaction so it
preserves what actually gets dropped instead of what already survives:

- Lead with the intent of the latest request, not a verbatim re-transcription
  (the recent user messages are already kept verbatim beside the summary);
  name which request governs when several are in play.
- Carry forward tool results — the concrete values, key lines, schemas — not
  just the commands that produced them.
- Keep settled decisions separate from still-open questions, and name the
  context the next turn must go and re-check.
- Write in the conversation's language, keep the note proportional to the task,
  and don't re-transcribe the auto-attached TODO list.

Also correct the system prompt's description of the post-compaction shape: the
recent user messages come first, followed by a first-person summary (not a
rigidly sectioned report), and a newer kept message supersedes the summary.

Update the affected inline snapshots and the compaction request token count.

* fix(agent-core): preserve an oversized latest request in the handoff note

selectRecentUserMessages truncates a kept user message to the size cap,
keeping only its prefix, so when the latest request itself exceeds the cap
only its head survives verbatim beside the summary. Telling the summary
"don't re-transcribe, it survives verbatim" then permanently dropped the
tail — often the actual ask. Keep the intent-not-transcription guidance,
but require preserving the at-risk parts of a long current request.
2026-07-02 13:44:29 +08:00
Kai
c434b4c3e6
fix(agent-core): cap foreground shell output to prevent OOM crash (#1285)
* fix(agent-core): cap foreground shell output to prevent OOM crash

A foreground command that streams a very large or unbounded amount of output (e.g. `b3sum --length 18446744073709551615`) grew the live-output buffer until Node aborted with a JavaScript heap out-of-memory error (exit 134). The per-command output is now capped at 16 MiB: on breach the command is gracefully terminated (SIGTERM -> grace -> SIGKILL) and the result carries a message pointing at redirecting large output to a file. The per-task output ring buffer is also made O(1) per chunk (was O(n^2)), which previously starved the event loop and the foreground timeout. Background/detached tasks are exempt.

* fix(agent-core): stop buffering output after the foreground cap trips

After the 16 MiB foreground ceiling tripped, appendOutput still enqueued every subsequent chunk into the per-task disk write chain during the SIGTERM grace window. A producer that ignores SIGTERM could keep that chain — and the chunk strings each pending write retains — growing until SIGKILL, re-introducing the same out-of-memory risk the cap prevents. Once the cap has tripped, keep only the bounded in-memory ring buffer and stop feeding the disk write chain. Interrupt/timeout capture behaviour is unchanged (they do not set outputLimitTripped).
2026-07-02 13:42:39 +08:00
qer
3ea84a56e4
fix(web): prevent horizontal scrollbar in session search dialog (#1290)
The search sessions dialog used a column flex layout where the title and snippet had white-space: nowrap but no min-width: 0, so the flex items refused to shrink below their text width and overflowed, surfacing a horizontal scrollbar on the dialog body. Add min-width: 0 so they shrink and ellipsize instead.
2026-07-02 12:24:46 +08:00
liruifengv
a4d7a4ff2a
docs(changelog): sync 0.22.0 from apps/kimi-code/CHANGELOG.md (#1288) 2026-07-02 11:34:17 +08:00
github-actions[bot]
ba7f18b3fb
ci: release packages (#1268)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 11:12:20 +08:00
qer
bbda90af84
fix(web): hide conversation outline when it cannot expand (#1278)
Some checks are pending
Release / Publish native release assets (push) Blocked by required conditions
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
* fix(web): hide conversation outline when it cannot expand

* fix(web): re-measure conversation outline when it becomes visible

When ConversationToc mounts while hidden (sessionLoading, mobile, or before a second user turn), navRef is null and the ResizeObserver is never set up, so fits stays true and the outline shows even in narrow layouts. Re-initialize the measurement whenever the nav is rendered.
2026-07-02 00:44:11 +08:00
Kai
074bb9ba13
fix(kosong): retry a dropped provider stream (terminated) on the Anthropic path (#1274)
A raw undici `terminated` error — an SSE/HTTP response body cut mid-flight,
common on long streaming responses — fell through convertAnthropicError to a
generic base ChatProviderError, which isRetryableGenerateError treats as fatal,
so it was never retried. Route raw non-SDK errors through the shared
classifyBaseApiError heuristic (already used by the OpenAI path) so a dropped
stream is classified as a retryable APIConnectionError and retried instead of
failing the turn.
2026-07-01 22:48:57 +08:00
liruifengv
a5db546d77
feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort (#1275)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort

Send thinking effort only when the model declares it in support_efforts, and add the KIMI_MODEL_THINKING_EFFORT environment variable as an escape hatch to force a specific effort regardless of declared support.

* test: align thinking effort expectations with support_efforts gating

Update the kimi adapter e2e and compaction tests that asserted the previous pass-through behavior on models without support_efforts.
2026-07-01 21:20:27 +08:00
liruifengv
54703d9457
fix(tui): restore terminal state on crash and release leaked resources (#1272)
* fix(tui): restore terminal state on crash and release leaked resources

Restore raw mode, cursor, and flow control on uncaughtException, unhandledRejection, and SIGTERM cleanup failure; reclaim pasted image bytes when transcript entries are trimmed; and stop feedback, activity, transcript, footer, and editor timers during shutdown.

* fix(tui): keep crash handlers installed and attach stty to the tty

Keep uncaughtException / unhandledRejection handlers installed for the whole interactive session; removing them right after start() resolved left runtime crashes uncaught. Run stty with stdin inherited from the TTY, since stty fails when stdin is /dev/null.
2026-07-01 21:14:19 +08:00
qer
b905dd4910
feat(web): redesign web UI and add design system (#1258)
* feat: redesign web ui & add design system

* feat(web): add motion to redesigned UI and add changesets

Animate toast enter/leave, dialog open, and workspace-list and tool-row expand/collapse instead of snapping, and add the changesets covering the web redesign.

* fix(web): remove undo message exit animation

* fix(web): route agent tools to AgentTool and focus dialog on open

- toolRegistry matched the raw 'agent' name, but normalizeToolName folds
  agent/subagent into 'task', so agent calls fell through to GenericTool
  and lost the inline Open button for the subagent detail panel.
- Dialog's focus watcher only fired on change; callers that mount with
  open already true (Login, Settings, ...) never moved focus into the
  modal. Run it immediately so initial focus and restore-on-close work.

* feat(web): add logo long-press design-system easter egg

Hold the sidebar logo for 3 seconds to open a dialog showing the design system page. Also trim and rebalance the redesign changesets.

* fix(web): silence Sidebar v-show warning by making it single-root

Nest the design-system Teleport inside the sidebar <aside> so the component has a single element root. App applies v-show to Sidebar, which needs an element root to attach to; the fragment root logged a "non-element root node" warning on every reactive update and the collapse did not take effect.

* fix(web): thinking toggle, tool-group i18n, agent detail button

- Show the default-thinking switch as on whenever thinking is effectively enabled (enabled !== false), matching the core resolver.

- Route the grouped tool-call header and status through vue-i18n.

- Hide the subagent "Open detail" button when no matching task exists (e.g. a completed foreground subagent after a refresh).

* fix(web): use strict equality in agent detail button guard

oxlint eqeqeq flagged the loose != null check; resolveAgentTaskId returns string | undefined, so compare with !== undefined.

* chore(web): remove stray design mockups and screenshots

Remove the design exploration mockups, screenshots, prompts, and notes that were accidentally committed under apps/kimi-web/design. Keep design-system.html, which the sidebar logo easter egg still references.

* fix(web): keep sessions on continuation failure, treat absent thinking as on

- Keep sessions already loaded from earlier pages when a continuation page fetch fails, instead of replacing the workspace with an empty page.

- Treat an absent thinking config as enabled in the settings toggle, matching the core resolver (thinking is on unless explicitly disabled).

* feat(web): open design-system easter egg on 10 logo clicks

Replace the 3-second long-press trigger with 10 consecutive clicks on the Kimi mark; the count resets after a short idle. The long-press was unreliable because pointerleave cancelled the timer on any drift.

* fix(web): treat cancelled swarm members as finished

phaseForTask now lets a terminal status (completed/failed/cancelled) override a stale subagentPhase, so a cancelled swarm member no longer stays live and suppresses the finished AgentSwarm card.

* feat(web): use 1s long-press for design-system easter egg

Switch the logo easter egg back to a long-press, shortened to 1 second, and make it reliable this time: use pointer capture plus touch-action:none so a slight drift no longer cancels the hold.

* fix(web): use plain Spinner for activity notices

ActivityNotice renders for non-chat loading states (e.g. compaction), so it must use Spinner per the design-system rule that reserves MoonSpinner for the chat first-response state.

* docs(web): add a11y guidance to design system

* docs(web): drop stale design README link

* fix(web): restore model search focus and define panel header weight

- Bind the model picker's search Input to searchRef so useDialogFocus moves focus into it on open instead of the dialog's close button.

- Use the defined --weight-semibold token for panel header titles (--weight-bold is not declared, so the shorthand was invalid).

* fix(web): let any open dialog own Escape over the side panel

Track open design-system Dialog instances in a shared count and include it in App.vue's anyOverlayOpen, so a dialog whose open state lives outside App.vue (such as the sidebar session search) captures Escape before the background side panel closes.

* fix(web): base dock-work flag on filtered dock task lists

Foreground subagents are excluded from the dock task lists, so a session whose only task is a foreground subagent no longer renders an empty workbar above the composer.

* fix(web): create subagent task before forwarding text deltas

A client that subscribes from a snapshot after subagent.spawned already fired never received the lifecycle taskCreated; the reducer only applies taskProgress to existing tasks, so assistant text deltas were dropped and the live subagent detail stayed blank. Emit taskCreated (via patchSubagent) before the text progress, mirroring the tool-progress path.

* fix(web): keep plan, swarm, and goal mode toggles per session

Plan, swarm, and goal modes were stored as global scalars on the web client and a single localStorage key each, so they leaked across sessions. Bind them to the active session via per-session maps, persist per session, and apply server status/events to the originating session so background sessions keep independent state.

* fix(web): keep subagent detail reachable for synthesized tasks

When the web client subscribes after a subagent already spawned, the synthesized subagent task has no parentToolCallId, so the Agent tool's Open-detail button was hidden and the panel would not open. Fall back to the single unmapped subagent task when resolving the detail target in both the button visibility and the panel open paths.

* fix(web): keep session kebab menu from being clipped

Teleport the SessionRow kebab menu to body and anchor it with fixed positioning so the collapsing group-sessions list's overflow:hidden no longer clips the dropdown.

* fix(web): apply staged modes to the created session by id

When starting the first prompt, apply the staged plan/swarm/goal modes to the just-created session's per-session maps by id instead of via the activeSessionId-based setters, so a session switch during the selectSession await can't drop the modes for this session or pollute another.

* refactor(web): unify confirmation dialogs into a single modal

Replace the inconsistent confirmation patterns (native confirm(),
two-step menu arming, hand-rolled inline strips, bare buttons) with one
modal ConfirmDialog driven by a global useConfirmDialog() composable,
and consolidate the duplicated confirm/cancel i18n keys.

* feat(web): inline message queue with separate stop button

- Send button always sends/enqueues; interrupt moves to a separate red Stop
  button shown only while running, so the two can no longer be confused.
- Queued prompts now render inline at the tail of the transcript (after the
  running turn) instead of behind the dock panel: click to edit, remove, drag
  the grip to reorder, with image thumbnails and a "next up" marker.
- Remove the dock queue panel and the QueuePane component; Steer stays on
  Ctrl/Cmd+S.

* chore: add changeset for web queue UX

* feat(web): prompt reliability, sidebar menu, and composer/markdown polish

- Fix spurious errors when question/approval/task actions were already complete
- Add loading feedback to question and approval prompts; block double-clicks
- Make the question "Other" option selectable by row click and let Enter advance/submit
- Consolidate workspace section actions into an overflow menu
- Tighten markdown prose line-height and block spacing
- Recall input history only when the caret is at text start
2026-07-01 20:47:12 +08:00
liruifengv
7859b0afe8
feat(kimi-code): vendor @moonshot-ai/pi-tui (#1254)
* chore(kimi-code): upgrade pi-tui to 0.78.1 and adapt native helpers

Bump @earendil-works/pi-tui from ^0.74.0 to ^0.78.1. pi-tui 0.75.5 replaced its koffi-based Windows VT input with a bundled native helper, and 0.76.0 added a darwin native helper for Terminal.app Shift+Enter.

- SEA build: teach native-deps to collect pi-tui's per-target .node files, drop the koffi registry, and add a native-file-only collect mode so only package.json + the target .node ship (28 -> 2 files).

- Redirect pi-tui's absolute-path native require() into the native-asset cache through the Module._load hook, and extend the native smoke test to actually load the helper.

- npm package: ship pi-tui's native/ directory so macOS Terminal.app Shift+Enter and Windows Shift+Tab keep working for npm installs.

* chore: add changeset for pi-tui upgrade

* chore: vendor @earendil-works/pi-tui 0.80.2

Fork the upstream pi-tui source into packages/pi-tui for local modification. Pristine snapshot of @earendil-works/pi-tui@0.80.2; the apps/kimi-code dependency on ^0.78.1 from npm is left unchanged.

* feat(kimi-code): integrate vendored @moonshot-ai/pi-tui

Replace the npm @earendil-works/pi-tui dependency with the vendored @moonshot-ai/pi-tui workspace package so the fork can be modified locally.

- Point apps/kimi-code imports and native-deps at @moonshot-ai/pi-tui.

- Make pi-tui source-first (exports -> src, publishConfig.exports -> dist, mirroring node-sdk) and strict-clean: bracket access for process.env / named capture groups, an override modifier, and non-null assertions for noUncheckedIndexedAccess.

- Bump the root tsconfig target to ES2024 and enable allowImportingTsExtensions (needed for pi-tui's /v regex and .ts imports, which node --test requires).

- Add packages/pi-tui to flake.nix workspaces and exclude the vendored source from oxlint.

* fix(pi-tui): export package.json for native asset resolution

The SEA native-asset collector resolves the package root via
require.resolve('@moonshot-ai/pi-tui/package.json'). The vendored
package's exports field only exposed ".", which blocked the
"./package.json" subpath and broke build:native:sea with
ERR_PACKAGE_PATH_NOT_EXPORTED.

* chore(kimi-code): sync pi-tui native prebuilds at build time

Copy packages/pi-tui/native prebuilds into apps/kimi-code/native
during build instead of tracking a manual copy in git. Only the
.node prebuilds are copied (not the C sources); the directory is
now a build artifact covered by .gitignore.

* fix(pi-tui): avoid destructive full redraw during streaming

When the first changed line is above the viewport, the differential
renderer fell back to fullRender(true), which clears scrollback and
yanks the user's viewport. On Windows Terminal this jumps to the
absolute top (microsoft/Terminal#20370).

Clamp the diff to the visible viewport when content length is
unchanged (spinner tick / markdown reflow above the viewport), so
streaming no longer triggers a full redraw in those cases. Length
changes still fall back to fullRender to reset the viewport.

* fix(kimi-code): update pi-tui imports in files merged from main

Two files added on main (effort-selector, plugin-command) still
imported the old @earendil-works/pi-tui package name; point them at
the vendored @moonshot-ai/pi-tui.

* chore(nix): update pnpmDeps hash after lockfile regen

* fix(pi-tui): clamp above-viewport diff even when content shrinks

Previously, when the first changed line was above the viewport and
content length changed (e.g. spinner removed at end of streaming), the
renderer fell back to fullRender(true), which clears scrollback and
yanks the viewport to the absolute top on Windows Terminal.

Always clamp the diff to the visible viewport instead, preserving the
user's scroll position. Stale bytes remain in scrollback but are not
visible.

* fix(kimi-code): keep activity placeholder to avoid streaming shrink

When streaming ends, removing the activity spinner shrank the content
by two rows, which (combined with transient→final code highlighting
above the viewport) triggered a destructive full redraw. Keep a one-row
placeholder in the activity pane when idle so the content does not
fully shrink.

* chore: refine streaming scroll changeset wording

* chore: remove obsolete pi-tui native helpers changeset

* chore: add pi-tui changesets and document the pi-tui changelog rule

Add changesets for the fork integration, package manifest export, and
viewport clamp fix so the vendored pi-tui keeps its own changelog.

Update the gen-changesets skill to treat @moonshot-ai/pi-tui as a
special internal package that lists itself instead of the CLI, with a
separate CLI changeset only when the change is user-visible there.

* chore(nix): update pnpmDeps hash after merging main

* fix(changeset): drop private kimi-code-docs from google-genai changeset
2026-07-01 20:23:35 +08:00
liruifengv
3cb7936cf5
chore(telemetry): track conversation undo, shell mode, and effort changes (#1271)
* chore(telemetry): track conversation undo, shell mode, and effort changes

- conversation_undo: fires when an undo reverts history (double-Esc selector, /undo, /undo N)
- shell_command: fires when a ! shell command runs in the TUI
- thinking_toggle: now fires on any effort change (not just on/off flips) and carries { enabled, effort, from }; align the no-session TUI path to the same payload

* chore(telemetry): track conversation_undo in core undoHistory

Tracking it in the TUI only covered the TUI; web/acp/REST undos reach
core.rpc.undoHistory directly and were missed. Emit the event from the
agent undoHistory RPC after the undo succeeds so every host is covered,
and drop the now-redundant TUI track.

Addresses review feedback on #1271.
2026-07-01 20:04:31 +08:00
liruifengv
c070fbedde
feat: add model alias overrides (#1262)
* feat: add model alias overrides

Preserve user model overrides across provider catalog refreshes and resolve effective model metadata for runtime, TUI, protocol, and ACP consumers.

* fix: apply model display name overrides

Show overridden model display names in the footer, welcome panel, status output, and model switch confirmations.

* fix: pass through kimi effort when undeclared

Keep support_efforts authoritative when declared, but pass requested Kimi thinking effort through when the model does not declare support_efforts.

* fix: honor model overrides in effort commands

Use effective model metadata for /effort choices and for always_thinking clamping when resolving thinking effort.
2026-07-01 19:57:13 +08:00
qer
0e279bfd7e
chore(kimi-desktop): rename installers to kcd-beta-alpha-crazy-internal (#1267)
* chore(kimi-desktop): rename installers to kcd-beta-alpha-crazy-internal

New artifact name: kcd-beta-alpha-crazy-internal-v50-<arch>-<MMDD>.<ext>,
where MMDD is the build date in UTC+8. This makes leaked or forwarded
installers harder to mistake for an official release, and the date makes
each build easy to identify.

* chore(kimi-desktop): uppercase KCD in installer name
2026-07-01 19:50:11 +08:00
Kai
ace7901066
feat(agent-core): compress oversized images before sending to the model (#1243)
* feat(agent-core): compress oversized images before sending to the model

Downsample images to a 2000px longest-edge and per-image byte budget at the
single prompt-ingestion chokepoint (the prompt/steer RPC) and on tool results
(ReadMediaFile, MCP), so every client transport — CLI, web, desktop, ACP, SDK —
is covered uniformly inside the core. PNG screenshots stay lossless and only
degrade to JPEG when the byte budget cannot otherwise be met. Best-effort: the
original image is sent unchanged if compression fails.

* fix(agent-core): serialize prompt/steer RPCs to avoid a turn-claim race

The prompt/steer RPC handlers await image compression before turn.launch()
synchronously claims the active turn, so two overlapping calls could both
compress first — letting the faster-to-compress one win the turn and strand the
other on agent_busy. Run these two RPCs through a per-agent serialization chain
so they claim in submit order; cancel and the other RPCs stay immediate.

* fix: update flake.nix pnpmDeps hash for the jimp dependency

Adding jimp to the workspace changed pnpm-lock.yaml, so the pnpmDeps
fixed-output hash was stale and the nix build failed. Update it to the value
the CI nix build reported.

* fix(agent-core): guard image compression against decompression bombs

A tiny-byte, huge-dimension image (e.g. a solid 30000x30000 PNG) would be fully
decoded into a multi-gigabyte bitmap by Jimp before any resize — an OOM vector
the byte budget never catches. Skip compression when the sniffed pixel count
exceeds MAX_DECODE_PIXELS (~100 MP), before the decode; oversized images pass
through uncompressed as they did before compression existed.

* fix(agent-core): cap decode byte size before compressing images

Compression runs before downstream size caps (e.g. the 10MB MCP per-part
limit), so a huge or invalid base64 image from an MCP tool was Buffer.from-
decoded — and handed to Jimp — just to be dropped afterward. Add a
MAX_DECODE_BYTES ceiling (64MB, overridable) checked before the base64 decode
and before Jimp, the byte-side complement to the pixel-count guard; oversized
payloads pass through uncompressed.

* refactor(agent-core): compress images at ingestion, not on the turn RPC

Move image compression off the prompt/steer RPC path and back to each ingestion
site (CLI paste, server upload resolution, ACP conversion; ReadMediaFile and MCP
already compressed at their producers). Compressing on the RPC control path put
an async step before the synchronous turn-claim, which spawned a series of
races: prompt/steer interleaving, and — with a cancel arriving mid-compression —
an ineffective abort that let a cancelled prompt launch anyway.

Treating compression as a pure input-stage transform (done while the content
part is built, before it ever enters the agent loop) removes those races
structurally: rpc.prompt/steer are plain synchronous handlers again, and the
serialization/cancel-window machinery is gone. Records stay compressed, resume
stays consistent, and coverage degrades gracefully (a new client that skips
compression just sends a larger image, as before this feature).

* fix: compress inline base64 prompts and honor ACP cancels mid-compression

Two contained ingestion-site follow-ups:

- server: resolvePromptMediaFiles now also compresses images submitted as an
  inline `{ kind: 'base64' }` source, not just uploaded files, so the REST
  inline-base64 path gets the same downsampling.
- acp-adapter: AcpSession tracks a pending-abort flag while prompt() awaits
  image compression (before any turn exists). A session/cancel in that window
  flips it, so the prompt returns `cancelled` instead of launching a turn the
  client already stopped.

* fix(acp-adapter): cover all concurrent pre-turn prompts on cancel

The pending-abort marker was a single session field, so with two
`session/prompt` requests compressing large inline images at once the later
one overwrote it and a `session/cancel` could mark only one — the other
launched after the client had cancelled. Track a token per in-flight prompt in
a set and flip them all on cancel so every pre-turn prompt is covered.

* chore(node-sdk): declare jimp as a devDependency

The SDK re-exports the image compressor, whose lazy `import('jimp')` (inside
the bundled agent-core code) is inlined into the published dist. jimp was
resolved only transitively via agent-core, so declare it as an explicit build
input here — matching the CLI — to make the bundling reliable rather than
phantom. It stays a devDependency: jimp is bundled, not a runtime dependency.
2026-07-01 19:36:48 +08:00
Kai
bf35f63c5d
fix(provider): honor base_url for google-genai and vertexai providers (#1269)
* fix(provider): honor base_url for google-genai and vertexai providers

The google-genai and vertexai provider types silently ignored a configured
base_url and always hit generativelanguage.googleapis.com (e.g. a Gemini-
compatible proxy URL + key could not be used). Plumb the endpoint through to
the @google/genai SDK via httpOptions.baseUrl:

- kosong: add baseUrl to GoogleGenAIOptions and inject it into the client's
  httpOptions alongside the existing headers (the SDK merges headers and
  overrides the base host).
- agent-core: forward provider.baseUrl in the google-genai and vertexai
  branches, with GOOGLE_GEMINI_BASE_URL / GOOGLE_VERTEX_BASE_URL env
  fallback. vertexai keeps deriving location from an aiplatform host.
- docs: document base_url for both providers, noting the host root only
  must be given because the SDK appends the API version itself.

Covered by unit tests asserting the URL reaches the kosong config and the
SDK client's httpOptions.

* fix(provider): use the effective base_url for vertex location detection

The vertexai branch forwarded the endpoint from config `base_url` OR the
GOOGLE_VERTEX_BASE_URL env fallback, but service-account detection
(`hasVertexAIServiceEnv` / `vertexAILocation`) still derived the region from
`provider.baseUrl` only. Supplying the regional endpoint via the env fallback
(with a project but no explicit GOOGLE_CLOUD_LOCATION) therefore left location
undefined and silently downgraded Vertex ADC to API-key Gemini routing.

Resolve the effective base URL once and use it for both forwarding and location
derivation, so the env fallback behaves exactly like `base_url`. Add a
changeset for the kosong + agent-core patch release.
2026-07-01 19:33:35 +08:00
Kai
ffe41ac752
fix(changeset): drop private kimi-code-docs from web-search changeset to unblock release (#1270)
The web-search-query-only changeset mixed @moonshot-ai/agent-core (published)
with kimi-code-docs, a private package without a version field that changesets
treats as ignored. Changesets rejects any changeset containing both ignored and
non-ignored packages, which broke the Release workflow's version step. Drop the
private docs entry; it never publishes and needs no changelog.
2026-07-01 19:28:20 +08:00
Kai
e47ca10267
feat(agent-core): slim WebSearch to query-only; fetch page content via FetchURL (#1260)
* feat(agent-core): slim WebSearch to query-only; fetch page content via FetchURL

The coding search backend no longer honors `limit`/`enable_page_crawling` and
always returns full page content, which was token-heavy and frequently
truncated. Realign the web tools around a search+fetch split:

- WebSearch request sends only `text_query`; the tool exposes only `query`.
- Search results drop inline page content and add the source site; full page
  content is now fetched on demand via FetchURL.
- Add a citation reminder to both WebSearch and FetchURL results (in the
  FetchURL front note so it survives body truncation).
- Update tool descriptions and reference docs accordingly.

* fix(agent-core): satisfy no-base-to-string lint and drop redundant | undefined

- Assert the exact serialized WebSearch request body instead of String()-coercing
  a BodyInit, fixing the type-aware no-base-to-string lint error.
- Drop redundant `| undefined` from WebSearchResult optional fields per AGENTS.md.
- Add changeset.

* chore(changeset): bump agent-core to minor for WebSearch input-contract change

Removing the `limit`/`include_content` tool inputs tightens a closed
(`additionalProperties: false`) schema, so previously-valid args are now
rejected — an incompatible change for a released package. Bump minor rather
than patch.
2026-07-01 18:46:15 +08:00
liruifengv
8cfb1657ad
fix(tui): reduce default transcript window to keep long sessions responsive (#1265)
Lower KIMI_CODE_TUI_MAX_TURNS default from 50 to 15 and KIMI_CODE_TUI_HYSTERESIS from 10 to 5 so the TUI keeps fewer transcript turns in long sessions.
2026-07-01 18:41:27 +08:00
liruifengv
003733c751
fix(tui): hide redundant Off option for always-on effort models (#1264)
Always-on models that expose multiple effort levels already render only the effort segments in the /model switcher, so the trailing "Off (Unsupported)" label was non-selectable clutter. Drop it for those models while keeping it for legacy boolean always-on models.
2026-07-01 18:33:56 +08:00
liruifengv
62999caca3
docs(changelog): sync 0.21.1 from apps/kimi-code/CHANGELOG.md (#1259) 2026-07-01 15:15:12 +08:00
github-actions[bot]
70f01ab01b
ci: release packages (#1257)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-01 14:43:39 +08:00
liruifengv
0cc02ac67d
fix(tui): keep waiting spinner during encrypted reasoning streams (#1256)
Empty (encrypted/redacted) thinking deltas no longer switch out of waiting mode, which previously stopped the moon spinner while no thinking component was ever created, leaving a blank spinner-less gap until the first real text/tool token.
2026-07-01 14:39:35 +08:00
wenhua020201-arch
ef61f4369b
docs: document plugin slash commands (#1253) 2026-07-01 13:49:43 +08:00
qer
170b29aa90
feat(vis): support importing debug zips via drag and drop (#1251)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(vis): support importing debug zips via drag and drop

Add a window-level drop target so a /export-debug-zip bundle can be
imported by dragging it anywhere into the vis UI, alongside the existing
file-picker button. A full-screen overlay gives feedback during the drag
and while the upload is in flight, and non-zip files are rejected with a
hint.

* fix(vis): gate drop handler to file drags

Match the other drag handlers by checking dataTransfer.types before
calling preventDefault, so non-file drops (selected text or a URL into
the search input) keep their native behavior instead of being swallowed
by the window-level listener.
2026-07-01 12:02:08 +08:00
liruifengv
c2fd9f0494
docs(changelog): sync 0.21.0 from apps/kimi-code/CHANGELOG.md (#1250) 2026-07-01 11:26:44 +08:00
github-actions[bot]
f2c7ec75d3
ci: release packages (#1224)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-01 10:51:54 +08:00
qer
4d936d98b8
fix(kimi-desktop): revert sidebar header padding to 80px (#1242)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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 / Desktop release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
100px left too much empty space next to the traffic lights; 80px clears
them without the extra gap.
2026-07-01 02:25:41 +08:00
Simon He
7f05f589e7
feat(web): add Mermaid diagram rendering and off-thread KaTeX/Mermaid workers (#1226)
* feat(web): add Mermaid diagram rendering and off-thread KaTeX/Mermaid workers

Enable Mermaid diagram support in the web chat via markstream-vue's enableMermaid(). Set up Web Workers for both KaTeX rendering and Mermaid parsing using markstream-vue's pre-built workers (katexRenderer.worker, mermaidParser.worker), keeping heavy computation off the main thread during live streaming.

* fix(web): skip Mermaid SVG subtrees in file link and markdown link rewriters

processFileLinks() uses a TreeWalker that scans all text nodes in mdRef.
Mermaid diagrams render as inline SVG, and diagram labels containing
file-path-like strings (e.g. src/App.vue) would be replaced with HTML
<button> elements inside SVG <text> nodes, corrupting the rendered diagram.

processMarkdownLinks() similarly queries a[href] inside SVGs; while
isLocalLink() mostly filters these out, explicitly skipping SVG subtrees
is safer.

Add svg to the closest() exclusion in processFileLinks(), and skip
links inside svg in processMarkdownLinks().

* fix(web): satisfy worker import lint

* chore: align mermaid dependency versions

* chore: change mermaid workers changeset to patch

---------

Co-authored-by: qer <wbxl2000@outlook.com>
2026-07-01 02:17:34 +08:00
Kai
8ac337a2b2
fix(agent-core): harden strict-provider wire compliance so malformed history can't brick a session (#1241)
* feat(agent-core): rework compaction to keep only user prompts and summary

* refactor(agent-core): rewrite compaction summary as first-person handoff

Rework the full-compaction summary to read as the agent's own continuing
notes instead of a third-party report:

- compaction-instruction.md: free-form first-person continuation that
  preserves exact commands, paths and outcomes, states the precise next
  action, and flags claimed-but-unverified work rather than trusting it.
- compaction-summary-prefix.md: skeptical "your own working notes"
  framing; drop the collaborative third-party prefix.
- system.md: add compaction-awareness guidance so the model continues
  naturally from a summary and re-checks any reported "done".
- Rename the compaction helpers module to handoff.ts.

Update tests and regenerate snapshots for the new prompt text, and fill
in contextSummary in the restored-compaction replay expectations.

* fix(agent-core): count image/audio/video parts in token estimation

estimateTokensForContentPart returned 0 for image_url/audio_url/video_url,
so auto-compaction triggers, the overflow-shrink budget, the kept-user
budget, and the reported context size all went blind to media — a
media-heavy session could overflow the model window while the estimate
reported a near-empty context. Media parts now carry a fixed estimate
(MEDIA_TOKEN_ESTIMATE), and the content-part switch is exhaustive so a new
ContentPart kind must declare its estimate rather than silently count as
zero.

* feat(agent-core): re-surface active background tasks after compaction

Folding the live context to [recent user prompts, summary] drops the
messages that started background tasks and their status updates, so the
model could forget a task is still running and spawn a duplicate.
injectAfterCompaction now appends a system-reminder listing active
background tasks (with guidance to use TaskOutput/TaskList/TaskStop
instead of re-spawning). It runs only post-compaction and carries an
injection origin, so the next compaction drops and rebuilds it rather
than stacking copies; the all-user-role post-compaction shape is
preserved (no tool-pairing reintroduced).

* test(agent-core): add compaction scenario guards and risk probes

Adds compaction-scenarios.test.ts driving the real Agent/ContextMemory/
FullCompaction machinery:

- A guard test locking in that repeated compaction folds the prior summary
  into the new one instead of stacking two summaries.
- Seven `it.fails` probes that executably reproduce known, currently-accepted
  edge-case defects so the suite stays green while documenting each one
  precisely; any of them will flip red (forcing removal of `.fails`) the day
  the behavior is fixed. They cover: assistant/tool appended during an
  in-flight summarizer call being dropped; unbounded shrink on empty
  summaries; the fixed 20k kept-user budget overflowing a small model window;
  a tool result orphaned when compaction starts mid-exchange; legacy
  compaction records dropping their verbatim tail on replay; micro-compaction
  clearing recent tool results in an overflow-shrunk suffix; and media being
  discarded when the oldest kept user message is truncated.

* fix(agent-core): repair tool_use/tool_result adjacency in projected context

A tool call and its result can end up non-adjacent in history — a
background-task notification or flushed steer lands between them, or an
interrupted/nested step delays the result — which strict providers reject
with HTTP 400. The projector now moves each tool_use's result up to
immediately follow it (projection-time only; the stored history is
untouched), and full compaction projects its summarizer input with a
synthetic result for any still-open call so the summary request stays
well-formed. Micro-compaction only surfaced this latent ordering by busting
the prompt cache, so it now defaults off.

Includes projector adjacency regression tests, a context-level integration
test, and a compaction synthesize-missing guard; the prior "keeps an
unresolved tool exchange out of the compaction prompt" test is updated to
the now-well-formed (synthetic-result) behavior.

* fix(agent-core): preserve the verbatim tail when restoring legacy compactions

A pre-rework `context.apply_compaction` record used
`[summary, ...history.slice(compactedCount)]` semantics and kept a verbatim
recent tail, but it has no `keptUserMessageCount`. The reworked applyCompaction
re-folded such records into the all-user shape, dropping the recent
assistant/tool tail — so resuming a session compacted by an older version
silently lost its most recent context.

On restore of such a record (gated on records.restoring, no keptUserMessageCount,
and compactedCount < history length) reproduce the old shape instead. The
forward/live path is unchanged; the projector's tool-adjacency repair keeps the
restored tail well-formed, and compaction only runs at clean step boundaries so
the tail has no open exchange. The legacy-tail probe now passes as a regression
guard via the real restore path.

* fix(agent-core): align legacy compaction foldedLength with live restore

The transcript reducer re-derived foldedLength for pre-rework
context.apply_compaction records (no keptUserMessageCount) using the new
kept-user+summary rule, but ContextMemory's restore now reproduces the legacy
[summary, ...history.slice(compactedCount)] shape for those records. The two
diverged for legacy sessions, so MessageService's foldedLength-vs-live-history
comparison could mis-handle GET /messages (miss or misorder recent output).

The reducer now mirrors the live legacy fold: when compactedCount is below the
pre-compaction length it computes 1 + (length - compactedCount); otherwise it
falls back to the kept-user derivation. The MessageService transcript test's
fixture is corrected to a new-format record, matching its all-user live mock.

* fix(kosong): merge a follow-up user turn into the preceding tool_results

The Anthropic message merge keyed on isToolResultOnly(last) ===
isToolResultOnly(converted), which left a tool_result-only user turn
followed by a plain-text user turn unmerged. After tool-exchange repair
this shape (assistant tool_use -> tool_result -> injected notification)
produces two adjacent user messages, which strict Anthropic-compatible
backends reject with HTTP 400.

Switch to the asymmetric predicate isToolResultOnly(last) ||
!isToolResultOnly(converted): a tool-result-only running message absorbs
whatever user turn follows (parallel tool_results or a trailing text),
yielding a valid [tool_result, ..., text] message; a plain-text running
message still only absorbs plain text. [tool_result, text] is valid for
both native Anthropic (which concatenates anyway) and strict backends.

* test(agent-core): pin micro-compaction flag in the shrunk-suffix probe

The 'does not clear recent tool results when projecting a shrunk suffix'
probe is an it.fails that only documents a real defect while
micro-compaction is active. It inherited the ambient
KIMI_CODE_EXPERIMENTAL master switch, so its pass/fail flipped with the
runner: green locally (master switch on) but a hard failure in CI, where
the flag defaults off and MicroCompaction.compact() is a no-op that
leaves the tool result intact.

Enable KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION explicitly for this probe
so it deterministically exercises the micro-compaction path regardless of
the environment.

* fix(agent-core): harden full compaction against in-flight races, unbounded shrink, and media loss

Three compaction-path fixes surfaced by review, each flipping its
documenting it.fails probe to a passing it:

- Append race (CMP-02): after the summarizer returns, the post-summary
  history check only compared the compacted prefix. A live step appending
  to the tail while a manual/SDK compaction was in flight slipped through —
  an appended assistant/tool turn is neither summarized (the summary covers
  only the snapshot) nor kept (the rebuild keeps user input), so it
  vanished. Now cancel when the appended tail contains a non-user message;
  an appended user message is still kept (rebuild picks it up), preserving
  the existing 'keeps messages appended while compacting an unchanged
  prefix' behavior.

- Unbounded empty/truncated shrink: an empty or truncated summary dropped
  the oldest message and reset retryCount, so a model that kept returning
  empty could issue ~one request per history entry. Bound the shrink
  attempts by MAX_COMPACTION_RETRY_ATTEMPTS, mirroring the overflow-shrink
  counter.

- Media dropped on truncation (CMP-07): truncating the oldest kept user
  message replaced its whole content with one text block, discarding any
  image/audio/video. Keep the non-text parts and spend the remaining budget
  (maxTokens minus their cost) on truncated text.

* fix(vis): mirror legacy compaction tail in the model-mode projector

For a pre-rework context.apply_compaction record (no keptUserMessageCount),
agent-core's ContextMemory restore and the transcript reducer keep the old
[summary, ...history.slice(compactedCount)] tail — a verbatim recent tail
including assistant/tool. The vis model-mode projector always applied the
new kept-user selection, so opening an older compacted session in model
mode hid the assistant/tool tail the resumed agent still holds (and
surfaced a pre-compaction user message the agent dropped).

Branch on a missing keptUserMessageCount with compactedCount < history
length and reproduce the legacy shape, matching the agent-core restore.

* fix(agent-core): cancel compaction on any droppable user-role tail

The in-flight append guard cancelled only when the tail grew with a
non-user role. A user-role message that compaction would still drop — a
background-task notification, hook/cron reminder, or shell-command output —
slipped through: appended after the summary snapshot (so absent from the
summary) and dropped by the all-user rebuild (which keeps only real user
input), vanishing silently.

Key the guard on the same predicate applyCompaction uses (!isRealUserInput)
so it cancels whenever the appended tail holds anything compaction would
drop. A real user message is still kept, so a live user turn racing a
manual/SDK compaction continues to complete.

* fix(agent-core): exclude pre-clear prompts from legacy folded length

The transcript reducer's legacy fallback (records predating
keptUserMessageCount, compacted with no verbatim tail) re-derived the
kept-user count from the whole transcript, including messages before the
last context.clear. Live ContextMemory rebuilds _history from post-clear
messages only, so counting pre-clear prompts overstated foldedLength;
MessageService then saw context.history.length <= foldedLength and skipped
appending unflushed live tail messages, dropping recent output from the
messages endpoint for old sessions compacted after a clear.

Derive only from entries at or after clearFloor to match the live context.

* fix(agent-core): drop media when truncating the oldest kept prompt

Revert the media-preserving truncation: keeping non-text parts on the
truncated boundary message overshot the kept-user budget when the media
alone exceeded it, and reordered interleaved text/media parts. Both codex
(no media-aware truncation) and Claude Code (strips media at compaction)
decline to preserve media on a truncated message, since media cannot be
partially truncated and keeping it whole breaks the budget.

truncateUserMessage now keeps only the truncated text. Recent messages
that fit the budget are still kept verbatim with their media; only the
oldest, partially-overflowing boundary message loses its attachments.

* fix(agent-core): make manual compaction and turns mutually exclusive

A manual/SDK compaction could start while a turn was streaming, or a new
turn could launch while a compaction was in flight. Either way the turn
mutates the shared context (streaming content into an existing assistant
message, or appending new messages) during the summarizer await, and that
output is neither summarized nor preserved by the all-user rebuild —
silent loss that object-identity checks can't detect (the streamed message
is mutated in place).

Guard both directions so the agent does one of {turn, compaction} at a
time: begin() refuses a manual compaction while a turn is active, and
launch() refuses a new turn while a compaction is in progress. Auto
compaction is exempt — it runs from within the turn at a step boundary,
which blocks the turn for its duration.

* chore(changeset): consolidate compaction changesets into one

* chore(agent-core): drop external-product references from compaction comments

* test(agent-core): add Anthropic wire-compliance smoke tests for compaction

Drive real compaction output and the compaction summarizer projection
through the real Anthropic provider conversion and assert the wire request
is well-formed: strict user/assistant alternation and every tool_use
answered by an adjacent tool_result. Locks in the cross-layer guarantee
(projector merge + Anthropic consecutive-user merge + adjacency repair +
synthesizeMissing) that compacted sessions stay valid for strict
Anthropic-compatible backends.

* fix(agent-core): defer and replay inputs during manual compaction instead of rejecting

Manual/SDK compaction runs outside a turn, so the earlier guard rejected
prompts/steers that arrived while it held the context. That broke three
things: a REST/web prompt got stuck 'running' (no terminal turn event), a
background-task/cron steer was silently lost (null was read as 'buffered'
but nothing was), and a follow-up prompt could land in the window after
isCompacting cleared but before reminders were reinjected.

Reuse the existing defer-and-replay model instead of rejecting:

- steer() and launch() buffer into steerBuffer while a compaction is in
  progress (returning null = buffered), mirroring how an active turn defers
  input.
- FullCompaction.compactionWorker keeps isCompacting true through
  refreshSystemPrompt + injectAfterCompaction (moving markCompleted and the
  completed event after reinjection), then replays the buffer via
  TurnFlow.onCompactionFinished — on success, on an A1 prefix/tail cancel,
  and on failure/abort.
- onCompactionFinished flushes into an active turn if one exists, else
  launches a fresh turn from the deferred input.

No PromptService change: a deferred prompt's eventual turn.started lets it
associate the pending prompt and clear it on turn.ended.

* feat(kosong): detect tool_use/tool_result adjacency errors

Add isToolExchangeAdjacencyError to classify the strict-provider 400 raised
when an assistant tool_use is not correctly paired with its tool_result
(missing, stray, or non-adjacent), excluding context-overflow 400s. Lets the
agent loop recognize the error and resend a wire-compliant request instead of
leaving the session stuck.

* fix(agent-core): close mid-history orphan tool calls and resend wire-compliant after a strict 400

Strict providers (Anthropic) reject a request whose assistant tool_use is not
answered by an adjacent tool_result, and the same malformed history is re-sent
every turn, permanently bricking the session.

- Projector now closes a mid-history tool call whose result is missing entirely
  (a later turn proves it is not in-flight) with a synthetic result; the
  trailing in-flight call is still left untouched.
- Add a strict projection (synthesize every open call, drop stray results) and,
  on a tool_use/tool_result adjacency 400, resend the request once with it.
- Report every projection repair (reorder / synthesize / drop) via log and
  telemetry, deduped by signature, so a silently-mangled history leaves a trace.
  Trailing-tail synthesis (expected under compaction) is not flagged.

* fix(kosong): merge consecutive user turns for strict providers

Gemini/Vertex require strictly alternating user/model turns and reject
consecutive user turns with HTTP 400. They arise after compaction (kept
prompts + user-role summary + injected reminders) and when a turn is
steered in right after a tool result. Anthropic already merged them
inline; the Google converter did not, so post-compaction requests failed.

Extract the asymmetric merge into a shared mergeConsecutiveUserMessages
helper applied at each strict provider's conversion boundary: refactor
Anthropic to use it (behavior unchanged) and apply it at the Google
converter's exit. A conformance suite drives every strict provider with
the post-compaction shape and a steer-after-tool-result shape, asserting
no consecutive same-role turns reach the wire, so a new strict provider
cannot silently omit the merge.

The provider-agnostic projector stays structure-preserving: lenient
providers (OpenAI/Kimi) keep distinct turns for clearer message
boundaries; only strict providers normalize, where the requirement lives.

* feat(kosong): recognize the broader structural request-rejection family

Add isRecoverableRequestStructureError, covering the strict-provider 400s that
stem from a malformed message array re-sent every turn: tool_use/tool_result
pairing, empty/whitespace-only text blocks, a non-user first message, and
non-alternating roles. Context-overflow 400s are excluded (handled by
compaction). Lets the loop trigger one strict, wire-compliant resend for the
whole family rather than only tool-pairing errors.

* fix(agent-core): sanitize whitespace and strict-resend structural 400s, with diagnostics

- Drop empty AND whitespace-only text blocks in projection (Anthropic rejects
  whitespace-only with "text content blocks must contain non-whitespace text",
  which otherwise sticks a session); treat whitespace-only tool output as empty.
- Broaden the post-400 strict resend to the whole structural family and add two
  strict-only passes to the strict projection: drop leading non-user messages
  (first message must be user) and merge consecutive assistant turns.
- Log + telemetry for every wire repair the projector applies (reorder,
  synthesize, drop orphan, drop leading, merge assistants, drop whitespace),
  deduped by signature; log the strict resend outcome (recovered or still
  rejected) so a stuck session always leaves a trace.

* fix(agent-core): normalize empty-equivalent tool result arrays to the empty placeholder

A tool result whose ContentPart[] output has no sendable content (an empty array,
or only empty/whitespace-only text blocks) was returned verbatim, so projection
stripped the blank blocks, left the tool message empty, and threw on every send —
bricking the session locally. String outputs were already normalized; do the same
for arrays. A non-text part or any non-whitespace text still keeps the real
output.

* chore(changeset): simplify the wire-compliance changeset
2026-07-01 02:16:19 +08:00
qer
882cf355a9
fix(web): persist workspace rename and hide provider manager (#1234)
* fix(web): persist workspace rename via the daemon update API

* fix(web): hide the unshipped provider manager

* fix(web): fall back to a local override for derived workspace renames

* fix(web): preserve workspace name override across registration
2026-07-01 02:03:19 +08:00
Kai
86e0c9201e
feat(agent-core): rework compaction to keep only user prompts and summary (#1214)
* feat(agent-core): rework compaction to keep only user prompts and summary

* refactor(agent-core): rewrite compaction summary as first-person handoff

Rework the full-compaction summary to read as the agent's own continuing
notes instead of a third-party report:

- compaction-instruction.md: free-form first-person continuation that
  preserves exact commands, paths and outcomes, states the precise next
  action, and flags claimed-but-unverified work rather than trusting it.
- compaction-summary-prefix.md: skeptical "your own working notes"
  framing; drop the collaborative third-party prefix.
- system.md: add compaction-awareness guidance so the model continues
  naturally from a summary and re-checks any reported "done".
- Rename the compaction helpers module to handoff.ts.

Update tests and regenerate snapshots for the new prompt text, and fill
in contextSummary in the restored-compaction replay expectations.

* fix(agent-core): count image/audio/video parts in token estimation

estimateTokensForContentPart returned 0 for image_url/audio_url/video_url,
so auto-compaction triggers, the overflow-shrink budget, the kept-user
budget, and the reported context size all went blind to media — a
media-heavy session could overflow the model window while the estimate
reported a near-empty context. Media parts now carry a fixed estimate
(MEDIA_TOKEN_ESTIMATE), and the content-part switch is exhaustive so a new
ContentPart kind must declare its estimate rather than silently count as
zero.

* feat(agent-core): re-surface active background tasks after compaction

Folding the live context to [recent user prompts, summary] drops the
messages that started background tasks and their status updates, so the
model could forget a task is still running and spawn a duplicate.
injectAfterCompaction now appends a system-reminder listing active
background tasks (with guidance to use TaskOutput/TaskList/TaskStop
instead of re-spawning). It runs only post-compaction and carries an
injection origin, so the next compaction drops and rebuilds it rather
than stacking copies; the all-user-role post-compaction shape is
preserved (no tool-pairing reintroduced).

* test(agent-core): add compaction scenario guards and risk probes

Adds compaction-scenarios.test.ts driving the real Agent/ContextMemory/
FullCompaction machinery:

- A guard test locking in that repeated compaction folds the prior summary
  into the new one instead of stacking two summaries.
- Seven `it.fails` probes that executably reproduce known, currently-accepted
  edge-case defects so the suite stays green while documenting each one
  precisely; any of them will flip red (forcing removal of `.fails`) the day
  the behavior is fixed. They cover: assistant/tool appended during an
  in-flight summarizer call being dropped; unbounded shrink on empty
  summaries; the fixed 20k kept-user budget overflowing a small model window;
  a tool result orphaned when compaction starts mid-exchange; legacy
  compaction records dropping their verbatim tail on replay; micro-compaction
  clearing recent tool results in an overflow-shrunk suffix; and media being
  discarded when the oldest kept user message is truncated.

* fix(agent-core): repair tool_use/tool_result adjacency in projected context

A tool call and its result can end up non-adjacent in history — a
background-task notification or flushed steer lands between them, or an
interrupted/nested step delays the result — which strict providers reject
with HTTP 400. The projector now moves each tool_use's result up to
immediately follow it (projection-time only; the stored history is
untouched), and full compaction projects its summarizer input with a
synthetic result for any still-open call so the summary request stays
well-formed. Micro-compaction only surfaced this latent ordering by busting
the prompt cache, so it now defaults off.

Includes projector adjacency regression tests, a context-level integration
test, and a compaction synthesize-missing guard; the prior "keeps an
unresolved tool exchange out of the compaction prompt" test is updated to
the now-well-formed (synthetic-result) behavior.

* fix(agent-core): preserve the verbatim tail when restoring legacy compactions

A pre-rework `context.apply_compaction` record used
`[summary, ...history.slice(compactedCount)]` semantics and kept a verbatim
recent tail, but it has no `keptUserMessageCount`. The reworked applyCompaction
re-folded such records into the all-user shape, dropping the recent
assistant/tool tail — so resuming a session compacted by an older version
silently lost its most recent context.

On restore of such a record (gated on records.restoring, no keptUserMessageCount,
and compactedCount < history length) reproduce the old shape instead. The
forward/live path is unchanged; the projector's tool-adjacency repair keeps the
restored tail well-formed, and compaction only runs at clean step boundaries so
the tail has no open exchange. The legacy-tail probe now passes as a regression
guard via the real restore path.

* fix(agent-core): align legacy compaction foldedLength with live restore

The transcript reducer re-derived foldedLength for pre-rework
context.apply_compaction records (no keptUserMessageCount) using the new
kept-user+summary rule, but ContextMemory's restore now reproduces the legacy
[summary, ...history.slice(compactedCount)] shape for those records. The two
diverged for legacy sessions, so MessageService's foldedLength-vs-live-history
comparison could mis-handle GET /messages (miss or misorder recent output).

The reducer now mirrors the live legacy fold: when compactedCount is below the
pre-compaction length it computes 1 + (length - compactedCount); otherwise it
falls back to the kept-user derivation. The MessageService transcript test's
fixture is corrected to a new-format record, matching its all-user live mock.

* fix(kosong): merge a follow-up user turn into the preceding tool_results

The Anthropic message merge keyed on isToolResultOnly(last) ===
isToolResultOnly(converted), which left a tool_result-only user turn
followed by a plain-text user turn unmerged. After tool-exchange repair
this shape (assistant tool_use -> tool_result -> injected notification)
produces two adjacent user messages, which strict Anthropic-compatible
backends reject with HTTP 400.

Switch to the asymmetric predicate isToolResultOnly(last) ||
!isToolResultOnly(converted): a tool-result-only running message absorbs
whatever user turn follows (parallel tool_results or a trailing text),
yielding a valid [tool_result, ..., text] message; a plain-text running
message still only absorbs plain text. [tool_result, text] is valid for
both native Anthropic (which concatenates anyway) and strict backends.

* test(agent-core): pin micro-compaction flag in the shrunk-suffix probe

The 'does not clear recent tool results when projecting a shrunk suffix'
probe is an it.fails that only documents a real defect while
micro-compaction is active. It inherited the ambient
KIMI_CODE_EXPERIMENTAL master switch, so its pass/fail flipped with the
runner: green locally (master switch on) but a hard failure in CI, where
the flag defaults off and MicroCompaction.compact() is a no-op that
leaves the tool result intact.

Enable KIMI_CODE_EXPERIMENTAL_MICRO_COMPACTION explicitly for this probe
so it deterministically exercises the micro-compaction path regardless of
the environment.

* fix(agent-core): harden full compaction against in-flight races, unbounded shrink, and media loss

Three compaction-path fixes surfaced by review, each flipping its
documenting it.fails probe to a passing it:

- Append race (CMP-02): after the summarizer returns, the post-summary
  history check only compared the compacted prefix. A live step appending
  to the tail while a manual/SDK compaction was in flight slipped through —
  an appended assistant/tool turn is neither summarized (the summary covers
  only the snapshot) nor kept (the rebuild keeps user input), so it
  vanished. Now cancel when the appended tail contains a non-user message;
  an appended user message is still kept (rebuild picks it up), preserving
  the existing 'keeps messages appended while compacting an unchanged
  prefix' behavior.

- Unbounded empty/truncated shrink: an empty or truncated summary dropped
  the oldest message and reset retryCount, so a model that kept returning
  empty could issue ~one request per history entry. Bound the shrink
  attempts by MAX_COMPACTION_RETRY_ATTEMPTS, mirroring the overflow-shrink
  counter.

- Media dropped on truncation (CMP-07): truncating the oldest kept user
  message replaced its whole content with one text block, discarding any
  image/audio/video. Keep the non-text parts and spend the remaining budget
  (maxTokens minus their cost) on truncated text.

* fix(vis): mirror legacy compaction tail in the model-mode projector

For a pre-rework context.apply_compaction record (no keptUserMessageCount),
agent-core's ContextMemory restore and the transcript reducer keep the old
[summary, ...history.slice(compactedCount)] tail — a verbatim recent tail
including assistant/tool. The vis model-mode projector always applied the
new kept-user selection, so opening an older compacted session in model
mode hid the assistant/tool tail the resumed agent still holds (and
surfaced a pre-compaction user message the agent dropped).

Branch on a missing keptUserMessageCount with compactedCount < history
length and reproduce the legacy shape, matching the agent-core restore.

* fix(agent-core): cancel compaction on any droppable user-role tail

The in-flight append guard cancelled only when the tail grew with a
non-user role. A user-role message that compaction would still drop — a
background-task notification, hook/cron reminder, or shell-command output —
slipped through: appended after the summary snapshot (so absent from the
summary) and dropped by the all-user rebuild (which keeps only real user
input), vanishing silently.

Key the guard on the same predicate applyCompaction uses (!isRealUserInput)
so it cancels whenever the appended tail holds anything compaction would
drop. A real user message is still kept, so a live user turn racing a
manual/SDK compaction continues to complete.

* fix(agent-core): exclude pre-clear prompts from legacy folded length

The transcript reducer's legacy fallback (records predating
keptUserMessageCount, compacted with no verbatim tail) re-derived the
kept-user count from the whole transcript, including messages before the
last context.clear. Live ContextMemory rebuilds _history from post-clear
messages only, so counting pre-clear prompts overstated foldedLength;
MessageService then saw context.history.length <= foldedLength and skipped
appending unflushed live tail messages, dropping recent output from the
messages endpoint for old sessions compacted after a clear.

Derive only from entries at or after clearFloor to match the live context.

* fix(agent-core): drop media when truncating the oldest kept prompt

Revert the media-preserving truncation: keeping non-text parts on the
truncated boundary message overshot the kept-user budget when the media
alone exceeded it, and reordered interleaved text/media parts. Both codex
(no media-aware truncation) and Claude Code (strips media at compaction)
decline to preserve media on a truncated message, since media cannot be
partially truncated and keeping it whole breaks the budget.

truncateUserMessage now keeps only the truncated text. Recent messages
that fit the budget are still kept verbatim with their media; only the
oldest, partially-overflowing boundary message loses its attachments.

* fix(agent-core): make manual compaction and turns mutually exclusive

A manual/SDK compaction could start while a turn was streaming, or a new
turn could launch while a compaction was in flight. Either way the turn
mutates the shared context (streaming content into an existing assistant
message, or appending new messages) during the summarizer await, and that
output is neither summarized nor preserved by the all-user rebuild —
silent loss that object-identity checks can't detect (the streamed message
is mutated in place).

Guard both directions so the agent does one of {turn, compaction} at a
time: begin() refuses a manual compaction while a turn is active, and
launch() refuses a new turn while a compaction is in progress. Auto
compaction is exempt — it runs from within the turn at a step boundary,
which blocks the turn for its duration.

* chore(changeset): consolidate compaction changesets into one

* chore(agent-core): drop external-product references from compaction comments

* test(agent-core): add Anthropic wire-compliance smoke tests for compaction

Drive real compaction output and the compaction summarizer projection
through the real Anthropic provider conversion and assert the wire request
is well-formed: strict user/assistant alternation and every tool_use
answered by an adjacent tool_result. Locks in the cross-layer guarantee
(projector merge + Anthropic consecutive-user merge + adjacency repair +
synthesizeMissing) that compacted sessions stay valid for strict
Anthropic-compatible backends.

* fix(agent-core): defer and replay inputs during manual compaction instead of rejecting

Manual/SDK compaction runs outside a turn, so the earlier guard rejected
prompts/steers that arrived while it held the context. That broke three
things: a REST/web prompt got stuck 'running' (no terminal turn event), a
background-task/cron steer was silently lost (null was read as 'buffered'
but nothing was), and a follow-up prompt could land in the window after
isCompacting cleared but before reminders were reinjected.

Reuse the existing defer-and-replay model instead of rejecting:

- steer() and launch() buffer into steerBuffer while a compaction is in
  progress (returning null = buffered), mirroring how an active turn defers
  input.
- FullCompaction.compactionWorker keeps isCompacting true through
  refreshSystemPrompt + injectAfterCompaction (moving markCompleted and the
  completed event after reinjection), then replays the buffer via
  TurnFlow.onCompactionFinished — on success, on an A1 prefix/tail cancel,
  and on failure/abort.
- onCompactionFinished flushes into an active turn if one exists, else
  launches a fresh turn from the deferred input.

No PromptService change: a deferred prompt's eventual turn.started lets it
associate the pending prompt and clear it on turn.ended.

* fix(kosong): merge consecutive user turns for strict providers

Gemini/Vertex require strictly alternating user/model turns and reject
consecutive user turns with HTTP 400. They arise after compaction (kept
prompts + user-role summary + injected reminders) and when a turn is
steered in right after a tool result. Anthropic already merged them
inline; the Google converter did not, so post-compaction requests failed.

Extract the asymmetric merge into a shared mergeConsecutiveUserMessages
helper applied at each strict provider's conversion boundary: refactor
Anthropic to use it (behavior unchanged) and apply it at the Google
converter's exit. A conformance suite drives every strict provider with
the post-compaction shape and a steer-after-tool-result shape, asserting
no consecutive same-role turns reach the wire, so a new strict provider
cannot silently omit the merge.

The provider-agnostic projector stays structure-preserving: lenient
providers (OpenAI/Kimi) keep distinct turns for clearer message
boundaries; only strict providers normalize, where the requirement lives.
2026-07-01 01:17:30 +08:00
qer
f12c83cfb9
fix(kimi-desktop): clear traffic lights and make the top draggable (#1237)
- Increase the sidebar header's left padding (100px) on macOS so the floating
  traffic lights no longer overlap the Kimi Code brand.
- Make the conversation header a window-drag region on macOS (interactive
  controls opt out with no-drag), so the whole top of the window can be dragged
  without adding a separate titlebar strip.
2026-06-30 23:28:34 +08:00
qer
bfe8e6ace3
fix(web): surface error when adding a workspace by path fails (#1236)
* fix(web): surface error when adding a workspace by path fails

Previously, addWorkspaceByPath swallowed any error from the daemon and created a local-only workspace that could not host sessions and vanished on reload, so an invalid absolute path appeared to succeed but never took effect. Now the error is reported to the user and no fake workspace is added.

* fix(web): preserve pending prompt when adding workspace fails

addWorkspaceByPath now returns a boolean success signal. handleAddWorkspace only clears pendingWorkspaceSubmit and sends the prompt on success, so a daemon-rejected path no longer drops the user's pending submission; they can retry with a valid path.

* fix(web): keep workspace picker open when add fails

Move closing the add-workspace dialog until after a successful add. On a daemon-rejected path the picker now stays open with the user's input intact, so they can correct the path and retry; the pending submission is consumed by a successful retry or dropped only when the user explicitly closes the picker.

* fix(web): show add-workspace errors inline in the picker

When the daemon rejects a path, the global warning toast rendered behind the picker's backdrop (z-index 60 vs 200) and could auto-dismiss unseen. Surface the failure as an inline error inside the dialog instead, so it is visible and persists until the user retries or closes. addWorkspaceByPath now returns a boolean and leaves surfacing to the caller; the picker shows a localized inline message.
2026-06-30 22:40:53 +08:00
liruifengv
108299be3c
refactor!: overhaul thinking config and effort resolution (#1132)
* feat: support multi-level thinking effort switching

- kimi provider: emit thinking.effort in the new wire format; keep reasoning_effort mirrored during the transition
- model catalog: thread support_efforts / default_effort from oauth through to /models
- config schema: add supportEfforts / defaultEffort on model aliases
- TUI: multi-segment thinking control in /model, new /effort command, footer effort display
- switch status uses displayName and distinguishes model vs effort-only changes

* docs: add thinking effort design plans

- thinking-effort-switching.md: implemented multi-level effort switching
- thinking-model-overhaul.md: follow-up refactor plan for the thinking state model

* docs: collapse thinking overhaul plan into a single PR

* refactor!: overhaul thinking config and effort resolution

Replace default_thinking and thinking.mode with a single [thinking] enabled/effort table. ThinkingEffort is now an open string ('off' | 'on' | model-declared effort); effort levels come from each model's support_efforts instead of a fixed enum.

Centralize default and always_thinking clamp logic in resolveThinkingEffort/defaultThinkingEffortFor, and honor an explicitly configured effort when an always_thinking model is forced back on.

TUI keeps a single thinkingEffort field instead of the boolean + level pair; 'on' is normalized to the model default at the UI boundary.

BREAKING CHANGE: default_thinking and thinking.mode are removed from config; migrate to [thinking] enabled/effort.

* refactor: rename residual thinking level wording to effort

Rename comments, error messages, parameter names, the SetThinkingPayload wire field (level -> effort), and TUI local variables so the thinking effort naming is consistent throughout. No behavior change.

* refactor: rename remaining camelCase thinking level identifiers to effort

Rename liveLevel/prevLevel/levelChanged/commitLevel/effectiveLevel to liveEffort/prevEffort/effortChanged/commitEffort/effectiveEffort in the TUI model picker and config commands.

* refactor: eliminate remaining thinking level wording in comments and tests

Rename levelLabel -> effortLabel, EffortSelectorOptions.levels -> efforts, and 'effort level(s)' / 'default level' / 'requested level' wording in comments, error messages, slash-command description, and test titles to effort. Also restore the withThinking(effort) parameter rename in the Kimi provider that was accidentally reverted.

* fix: address codex review feedback on thinking effort handling

- OpenAI thinkingEffortToReasoningEffort and Anthropic clampEffort now normalize 'on' / unrecognized efforts instead of throwing, so boolean non-Kimi models no longer crash on session start.

- ACP resolveCurrentThinkingEnabled treats a non-empty thinking.effort as enabled, matching agent-core's resolveThinkingEffort.

- REST promptThinkingSchema accepts any non-empty effort string so model-declared efforts are not rejected at the API boundary.

* test: align kimi e2e expectations with supportEfforts-gated reasoning_effort

The kimi provider now sends reasoning_effort only when the model declares support_efforts; boolean models (no support_efforts) send only thinking.type. Update the kimi e2e tests to drop the stale reasoning_effort expectation for the boolean test model.

* test: cover [thinking] effort parsing in config.test

Add effort = "high" to the documented [thinking] table in the config parse test and assert config.thinking.effort is resolved, so the new [thinking] effort field has direct parse coverage.

* docs: add thinking test coverage gap analysis

Capture the explore agent's test coverage review for the thinking overhaul PR, including P1/P2 gaps and the two open design questions, for follow-up test additions.

* feat(oauth): parse nested think_efforts from /models response

The /models endpoint now returns effort levels under a nested think_efforts object ({ support, valid_efforts, default_effort }). Parse it preferentially in both managed-kimi-code and open-platform model parsing, falling back to the legacy flat support_efforts / default_effort fields for older servers.

* refactor(oauth): only read nested think_efforts; gate on support=true

Drop the legacy flat support_efforts / default_effort fallback. The think_efforts object is now the single source, and its support flag gates the whole object — when support is not true, valid_efforts and default_effort are ignored entirely.

* chore: remove unused parseStringArray import in open-platform

* docs: finalize thinking effort release notes

Downgrade the changeset to minor with an English summary, drop the version-specific 'added in 1.0.0' info block, and present the deprecated config fields as a table (field / deprecated in 0.21.0 / description).

* refactor: drop temporary refresh toggles and kimi reasoning_effort mirror

Remove the always-true REFRESH_MODELS_ON_PICKER_OPEN / REFRESH_PROVIDER_MODELS_ON_STARTUP toggles and their stale re-enable TODOs, and stop sending reasoning_effort from the kimi provider (thinking.effort is the only wire field now).

* fix(tui): avoid persisting "on" as thinking effort

* fix: preserve persisted thinking effort across login and provider setup

* fix(tui): show actual thinking effort in /status and footer

* test(tui): align message-flow expectations with effort persistence and /status display

* fix(vis): rename thinkingLevel to thinkingEffort in config.update analysis
2026-06-30 22:34:13 +08:00
qer
e207f52f55
ci(kimi-desktop): strip Developer ID prefix from CSC_NAME (#1235)
* ci(kimi-desktop): strip Developer ID prefix from CSC_NAME

electron-builder rejects the 'Developer ID Application: ' prefix in CSC_NAME;
the keychain setup exports the full certificate name, so strip the prefix
before passing it to electron-builder.

* ci(kimi-desktop): add homepage and maintainer for linux .deb

electron-builder's .deb target requires a project homepage and a package
maintainer; set both so the Linux build succeeds.
2026-06-30 22:19:35 +08:00
Kai
020992c286
fix(cli): force-exit headless runs so a lingering handle can't wedge kimi -p (#1233)
* fix(cli): force-exit headless runs so a lingering handle can't wedge kimi -p

Print mode (`kimi -p`) never calls process.exit(); it relies on the Node
event loop draining once the run completes. A stray ref'd handle left over
from the run — a lingering socket, an un-cleared timer, or a child whose
pipes stay open — keeps the loop alive, so a finished run hangs until an
external timeout kills it instead of exiting.

- Arm an unref'd force-exit fallback after a headless run completes. A
  healthy run drains and exits before it fires (behaviour unchanged); it
  only force-exits a run whose loop is already wedged.
- Bound prompt cleanup with a timeout so a wedged shutdown step (a
  SessionEnd hook, MCP shutdown, or a connection blackholed by a
  restrictive firewall) can't keep a completed run alive forever.

Add unit tests for the force-exit guard and for cleanup staying bounded
when harness.close() hangs.

* refactor(cli): move headless force-exit to the entrypoint, keep handleMainCommand pure

handleMainCommand is a reusable, exported, unit-tested handler — scheduling a
deferred process.exit inside it could terminate a test runner or an embedding
host once the spied exit was restored. Make it pure: it now returns a
MainCommandOutcome, and the process entrypoint (the program action, which
already owns the error-path process.exit) arms the unref'd force-exit fallback
only for a completed headless run.

* fix(cli): drain stdio before the headless force-exit to avoid truncating output

The unref'd force-exit fallback fired after a fixed grace even when the event
loop was alive only because buffered stdout/stderr had not finished flushing to
a slow or piped consumer (the prompt writers ignore write() backpressure). That
could truncate the assistant output or resume hint of an otherwise-successful
`kimi -p` run — a regression versus the previous natural-drain exit.

finalizeHeadlessRun now flushes stdio first (bounded by HEADLESS_STDIO_DRAIN_TIMEOUT_MS)
and only then arms the force-exit. Draining first also means a leaked handle is
the only thing that can still hold the loop, so the backstop stays precise and
bounded.

* fix(cli): only swallow cleanup rejections abandoned by the timeout

The cleanup timeout guard caught every rejection, so a cleanup step that failed
fast (e.g. restoring a resumed session's permission, or harness.close hitting a
persistence error) was silently swallowed: runPrompt resolved as success and the
session could be left in `auto` with no error surfaced.

A timeout should bound how long we wait, not change the outcome. raceWithTimeout
now propagates a rejection that settles before the timeout, and only swallows the
abandoned promise's late rejection once the timeout has won the race.
2026-06-30 22:13:17 +08:00
qer
210cedb3bf
chore: add KCD client for test (#1230)
* feat(kimi-desktop): add Electron desktop client wrapping kimi-web

New apps/kimi-desktop — a thin Electron shell + process manager around
the existing web UI. It reuses kimi-code's shared daemon: it runs the
bundled SEA's `server run` (the same ensureDaemon reuse-or-spawn flow as
`kimi web`), reads ~/.kimi-code/server/lock for the real origin, and
loads the SEA-served kimi-web same-origin. The daemon is left running on
quit so the CLI / browser / TUI keep sharing it.

- main process: ensure-server (run SEA, read lock, confirm healthz),
  sea-path (dev vs packaged), window + native menu + window-state +
  loading/error screens
- packaging: electron-builder config; before-pack stages the
  matching-platform SEA into <resources>/bin/<target>
- CI: desktop-build workflow builds unsigned mac/win/linux installers,
  each runner building its own SEA
- workspace wiring: register in flake.nix, allow electron postinstall
  (onlyBuiltDependencies), root dev:desktop + typecheck entries

v1 is unsigned, default icon, no auto-update.

* feat(kimi-desktop): sign + notarize macOS builds

Unsigned macOS builds are blocked by Gatekeeper ("app is damaged") once
transferred to another Mac. Add Developer ID signing + Apple notarization,
mirroring the TUI native build:

- build/entitlements.mac.plist: hardened-runtime entitlements (allow-jit,
  disable-library-validation for koffi/clipboard, etc.) applied to the app
  and — via entitlementsInherit — the nested SEA backend
- electron-builder.config.cjs (replaces .yml): hardenedRuntime + entitlements;
  signing and notarization are env-driven (CSC_* + KIMI_DESKTOP_NOTARIZE +
  APPLE_API_* ), so the same config builds unsigned locally or signed+notarized
- desktop-build CI: sign-macos input reuses the existing macos-keychain-setup
  action + APPLE_* secrets, notarizes via the notary API key
- README: document signing, the Developer-ID requirement, and the
  "don't rename the .app" gotcha

Verified locally that electron-builder signs both the app and the nested SEA
with hardened runtime + the entitlements, and the signed app still launches and
serves the web UI. Notarization itself needs a Developer ID cert (CI / a machine
that has one).

* feat(kimi-desktop): rename product to Kimi Code Desktop

productName / window title / menu label / error-screen text all use
"Kimi Code Desktop" so the bundle name matches its executable (a
mismatch from manual renaming is itself reported as "damaged").

* ci(kimi-desktop): build and attach desktop installers in the release pipeline

Make desktop-build.yml reusable (workflow_call) and invoke it from the
release workflow, mirroring the native-build pipeline, so each release
also attaches signed+notarized macOS, Windows and Linux desktop
installers to the GitHub Release.

* feat(kimi-desktop): brand the desktop as an internal testing build

- Add an inline 'internal testing build' tag next to the Kimi Code brand
  in the sidebar header, shown only inside the desktop app.
- Use a hidden native title bar on macOS with the traffic lights folded
  into the sidebar header, and pin the window title to the product name.
- Ship the Kimi app icon for macOS and Linux builds.

Desktop detection is runtime (a query hint from the Electron shell,
persisted in sessionStorage) so the branding appears even when the
window is served by an already-running shared daemon.

* docs(kimi-desktop): update v1 scope now that the app icon ships

* feat(kimi-desktop): add the Kimi app icon for Windows builds

* chore(nix): update pnpmDeps hash after lockfile refresh

* ci(kimi-desktop): build desktop on release but do not attach to GitHub Release

The desktop build is an internal-testing artifact (branded as such), so
keep it as a CI artifact for internal download instead of publishing it
to the public GitHub Release.

* chore(kimi-desktop): mark installers as internal pre-release builds

Rename the packaged artifacts to KCD-Internal-<version>-<arch>.<ext> and
bump the version to the 0.1.1-internal.0 pre-release, so a leaked or
forwarded installer file is not mistaken for an official public release.

* feat(kimi-desktop): strengthen the internal-build tag wording

Change the sidebar tag to 'Internal testing · do not distribute' /
'内部测试 · 禁止外传' so the no-distribution intent is explicit.

* feat(kimi-desktop): tweak internal-build tag to '仅供内部测试'

* fix(kimi-desktop): pass the server token to the web UI on launch

Read the daemon's persistent bearer token from <KIMI_CODE_HOME>/server.token
and carry it in the URL fragment (#token=), matching how 'kimi web' opens
the Web UI. Without this, a fresh launch (no saved credential) boots the
web UI without a token, hits 401, and falls into the manual token dialog
even though the desktop started the daemon itself.

Addresses review feedback on the desktop URL.
2026-06-30 21:46:51 +08:00
Haozhe
ceb27f5e44
feat(server): add GUI store API mirroring localStorage (#1231)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / 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
Release / Release (push) Waiting to run
* feat(server): add GUI store API mirroring localStorage

- add /api/v1/gui/store/* endpoints (getItem/setItem/removeItem/clear/length) mirroring the browser localStorage interface
- add IGuiStoreService persisting opaque string values to ~/.kimi-code/gui.toml via smol-toml with atomic writes and an in-process write lock
- wire protocol schema, service, routes, and DI registration; add e2e tests and update the API surface snapshot

* chore: add changeset for gui store api

* fix(server): harden GUI store key handling and file permissions

- use a null-prototype record and an own-property check so keys that exist on Object.prototype (toString, constructor, __proto__) behave like ordinary keys
- write gui.toml with 0600 permissions so unsent drafts and input history stay private to the owning user
2026-06-30 21:04:59 +08:00
qer
aa6b0d065e
feat(web): always show the usage-data opt-out toggle in settings (#1232)
The telemetry toggle was hidden unless the config explicitly set a value, and its on/off mapping treated the default (unset) state as off even though telemetry is enabled by default. Show it always, treat unset/true as on, and rename it with a description and a restart note.
2026-06-30 20:49:35 +08:00
qer
5cb80ce879
feat: support plugin slash commands (#1204)
* feat(agent-core): support plugin slash commands

* feat(node-sdk): expose listPluginCommands

* feat(kimi-code): register and dispatch plugin slash commands

* chore: add changeset for plugin commands

* feat(agent-core): activate plugin commands server-side

* feat(node-sdk): add activatePluginCommand

* feat(kimi-code): render plugin command activations compactly

* feat(agent-core): recurse plugin command directories and preserve namespace

* fix(kimi-code): parse nested plugin command names

* fix(agent-core): update prompt metadata for plugin command turns

* fix(kimi-code): replay plugin command turns as command cards

* fix: treat plugin-command origins as real user prompts in undo

* fix(kimi-code): guard model-empty and clear plugin command render ids

* fix: propagate plugin_command to web and vis turn projectors

* fix(kimi-code): refresh plugin commands through auth flow

* fix(kimi-web): render plugin command cards in chat pane

* fix(kimi-web): render plugin command card in desktop chat view

* fix(kimi-code): treat slash-activation cards as transcript turn boundaries

* fix(kimi-code): count slash-activation entries when trimming transcript turns

* fix(kimi-code): preserve plugin command args in undo selector
2026-06-30 19:38:01 +08:00
Kai
42e37eb898
feat(timing): split TTFT into api-server and client portions (#1228)
* feat(timing): split TTFT into api-server and client portions

Time-to-first-token previously lumped in-process request building
(message serialization, param assembly) together with network + server
latency, making it impossible to tell whether a slow turn was the client
or the API server.

Add an `onRequestSent` hook to kosong's GenerateOptions, fired by every
provider immediately before it dispatches the network call. The window
from request start to dispatch is attributed to the client; the window
from dispatch to the first streamed token is attributed to the API
server. The split flows through the step.end / turn.step.completed
events (and therefore wire.jsonl) and is surfaced in three places:

- KIMI_CODE_DEBUG=1: `TTFT: 2.5s (api 2.4s + client 100ms)`
- session log: new `llm response` line with the timing breakdown
- vis: firstToken/api + firstToken/client rows and timeline label

The split is omitted (total only) when a provider does not report the
boundary, preserving backward compatibility.

* feat(timing): split the decode window into server vs client time

Time-to-first-token now reports a client/server split, but the slow part
of a long turn is the decode window (inter-token streaming), which was
still a single opaque number. Profiling long sessions showed decode
throughput halving over a session's lifetime independent of context
size, which the synchronous per-chunk stream pipeline can cause: kosong
awaits the host callback for every streamed part, so a loaded main
thread throttles how fast tokens are pulled off the wire.

Account for this directly in the stream loop: the time awaiting the next
part (server + network) versus the time spent processing each part
in-process (deep copy, host callback, part merge). The split is reported
through onStreamEnd and flows through the step.end / turn.step.completed
events (and wire.jsonl) into the same three surfaces as the TTFT split:

- KIMI_CODE_DEBUG=1: `TPS: 40.0 tok/s (200 tokens in 5.0s; server 4.6s + client 400ms)`
- session log: serverDecodeMs / clientConsumeMs on the `llm response` line
- vis: streamDuration/server + streamDuration/client rows and timeline label

A large, growing client share confirms host-side throttling; a dominant
server share points at the server/connection. The per-chunk accounting is
wrapped in try/finally so it stays correct across `continue` and aborts,
and is omitted when the stream reports nothing.
2026-06-30 19:15:02 +08:00
liruifengv
659062d11c
fix(tui): enable file path completion for / in shell mode (#1225)
Typing `/\' in shell mode (`!\') now triggers file path completion instead of the slash command menu, for both a bare leading `/\' and inline paths like `ls /\'. Hidden entries are skipped to match `/add-dir\', and accepting a completion no longer produces a double leading slash.
2026-06-30 17:54:24 +08:00
qer
a3f9cec8a9
fix(web): deduplicate workspaces shown in the sidebar (#1221)
Collapse registered workspaces that share a root in the daemon registry (preferring the canonical id) and in the web sidebar merge, so the same folder no longer renders as two identical, synchronously-selected entries.
2026-06-30 17:25:24 +08:00
liruifengv
ec51324230
feat(tui): open undo selector on double-Esc (#1220)
* feat(tui): open undo selector on double-Esc

Pressing Esc twice while idle now opens the undo selector, equivalent to running /undo with no arguments. Esc during streaming, compaction, or with a popup open keeps its cancel/close behavior and does not arm the double-press.

* fix(tui): disarm double-Esc undo on any intervening key

A pending double-Esc was only cleared by text changes, so a sequence like Esc, Ctrl-C, Esc within the window still opened the undo selector. Fire an onNonEscapeInput hook for every non-Escape key and clear the pending state there, so the shortcut only triggers for two consecutive Escape presses.
2026-06-30 15:42:27 +08:00
liruifengv
80e6888e34
fix(tui): open @ file mentions inside slash command arguments (#1223)
Typing @ in the middle of a slash command argument (for example `/goal Fix the @checkout docs`) was swallowed by the slash-argument completion guard before the @ mention branch ran, so the file list never opened. Run the @ mention branch ahead of the slash guards so file mentions take priority; plain slash-argument editing is still suppressed as before.
2026-06-30 15:35:19 +08:00
liruifengv
7f61488a88
docs(changelog): sync 0.20.3 from apps/kimi-code/CHANGELOG.md (#1216) 2026-06-30 14:50:31 +08:00
Kai
525fb146d6
feat(vis): full session debugging — tasks/cron, execution timeline, retries & tool progress (#1210)
* feat(vis): surface background tasks and cron jobs

The visualizer read every wire/state/blob artifact a session persists but
ignored the two on-demand families agent-core also writes under the session
directory: background tasks (tasks/<id>.json + output.log) and cron jobs
(cron/<id>.json). Neither is reconstructable from the wire, so there was no
way to inspect what a session spawned in the background or scheduled.

Server:
- task-store / cron-store read-only readers mirroring agent-core's on-disk
  layout, id-validation guard, and legacy snake_case task normalization
- GET /:id/tasks, /:id/tasks/:taskId/output (byte-window paged via an exact
  nextOffset cursor), and /:id/cron routes
- re-export the public background-task types from agent-core; mirror the
  non-exported CronTask shape with a fixture-backed drift test

Web:
- Tasks tab: process/agent/question kinds with status, timing, kind-specific
  fields, raw JSON, and a progressively paged output.log viewer
- Cron tab: expression, prompt, recurring/one-shot, created/last-fired
- count badges on both tabs

Tests: +20 (lib + route), all 113 vis-server tests green; web typecheck and
build clean.

* feat(agent-core): persist step retries and tool progress summary

Two transient signals were only ever emitted as live-only loop events, so
nothing survived in the agent record for post-hoc analysis:

- step retries: chatWithRetry gains an onRetry callback; turn-step collects
  the recovered attempts and attaches them to step.end as an optional
  `retries` array (previously only the live `step.retrying` event).
- tool progress: tool-call distills a tool's sparse status/percent updates
  into a bounded `progress` summary (updateCount / lastStatus / maxPercent)
  on tool.result. Streamed stdout/stderr is excluded — it would bloat the
  wire and is already reflected in the result output.

Both are additive optional fields, so the wire protocol version is unchanged
and existing records keep loading. New public types: LoopStepRetryRecord,
LoopToolProgressSummary.

* feat(vis): add execution-analysis timeline and surface retries/progress

Turn the debugger from a flat record viewer into an analysis tool.

New Timeline tab: folds the wire into turns → steps → tool calls (client-side,
no extra round-trip) and derives the metrics the raw list hides — per-turn /
per-step / per-tool duration, per-turn token cost, a context-window fill
sparkline with cache-hit rate, a tool usage table, idle-gap detection, and a
config-change timeline.

Inline elsewhere:
- Wire rows show tool.call → tool.result elapsed time; tool.result detail
  shows truncation, output size, retries, and the progress summary.
- Issues drawer gains tool-error, truncation, filtered, max_tokens, and
  retried categories.
- Tasks tab links agent-kind tasks to the subagent's wire.

Wires up vitest for the web package and adds analysis/issues unit tests.

* feat(vis): import debug zips with a logs view and imported-session filtering

A `/export-debug-zip` bundle is just `manifest.json` plus a flattened session
directory, which vis already knows how to read. Importing one therefore lights
up every existing tab for a session that lives on someone else's machine.

Server:
- zip-import: yauzl extraction with zip-slip path guards and entry-count /
  uncompressed-size caps for untrusted uploads.
- import-store: extract a bundle into <home>/imported/<imp_…>/, validate it
  has a main wire, and record an import-meta.json sidecar.
- session-store resolves imp_-prefixed ids against imported/, so wire /
  context / tasks / cron / blobs / logs all work on imported sessions; agent
  homedirs are re-derived locally (the bundle holds foreign absolute paths).
- POST /api/imports (raw zip body) and GET /api/sessions/:id/logs (structured
  log lines — also available for local sessions).

Web:
- session rail: import button + all/local/imported filter + imported badge.
- new Logs tab: virtualized, level filter, search, session/global toggle.
- manifest card atop the State tab for imported sessions.

SessionSummary/SessionDetail gain `imported` + `importMeta`. Tests cover
extraction, the zip-slip guard, list merge, reading an imported wire through
the existing route, and log parsing.

* fix(vis): read tasks/cron from agent homedirs and stop persisting tool status text

Addresses review feedback on the debug-tooling changes:

- Background tasks and cron jobs are persisted under each agent's homedir
  (<session>/agents/<id>/tasks and /cron), not the session root. The Tasks and
  Cron tabs read the session root, so they showed nothing for normal sessions.
  Both routes now aggregate across detail.agents homedirs; task entries carry
  the owning agentId. The route-test fixtures were writing to the wrong
  (session-root) location too — corrected to the real agents/main layout so
  they actually exercise the path.

- tool.result progress no longer keeps free-form status text, only updateCount
  and maxPercent. A tool's status string can contain sensitive data (e.g. an
  MCP OAuth authorization URL) that must not leak into persisted wire files or
  exported debug bundles.

* fix(vis): stop the main content area from overflowing horizontally

The <main> flex child lacked min-w-0, so it defaulted to min-width:auto and
refused to shrink below its content's intrinsic width. Tabs that lay out in
normal flow with flex-wrap rows (the Timeline tab) then got unbounded width,
never wrapped, and blew the layout out to thousands of pixels wide. Adding
min-w-0 lets the column shrink to the available width so its content wraps,
truncates, or scrolls within its own container.

* fix(vis): resolve local global log path and imported-state agent fallback

- Logs tab: for non-imported sessions the shared global log lives at
  <KIMI_CODE_HOME>/logs/kimi-code.log, not under the session dir (that path is
  only used inside exported bundles). The route now reads the home path for
  local sessions, so the global-log toggle works for them.
- Imported detail: a bundle's state.json is best-effort and may omit the
  agents map. When the inventory is empty, fall back to discovering agents
  from disk so routes that require an agent (wire/context) still resolve main.

* fix(vis): harden imported manifest and task parsing against corrupt input

An imported debug zip is untrusted, so a syntactically valid but type-corrupt
file could crash whole views:

- manifest.json: a non-string field (e.g. workspaceDir: 123) flowed into
  SessionSummary.workDir, where the session rail calls .split('/') and crashed
  the entire list. readManifest/readImportMeta now sanitize declared string
  fields, keeping only strings.
- task JSON: a record that passed the shape guard but held a non-string legacy
  field (e.g. stop_reason: 5) threw in normalization, failing GET /tasks with a
  500 and hiding all of a session's tasks. optionalNonEmptyString now tolerates
  non-strings, and listBackgroundTasks skips any record that still fails to
  normalize — honouring the reader's documented silently-skips contract.

* fix(vis): discover rotated logs; keep tool progress on thrown failures

- Logs tab: the diagnostic log can rotate (kimi-code.log.1, .2, …) and an
  exported bundle may contain only the archives. The route now discovers the
  active file plus its rotated siblings and concatenates them oldest-first, so
  a rotated-away log still surfaces (covered by node-sdk's rotated-export case).
- agent-core: a tool that reported sparse progress and then threw lost its
  progress summary, because the catch path built the error tool.result without
  it. Thread progressSummary through that path too, matching the success and
  malformed-return paths.

* fix(vis): skip type-corrupt agent entries in imported state

readImportedDetail's empty-inventory fallback never ran when a bundle's
state.json had a non-empty but type-corrupt agents map (e.g.
`{ "agents": { "main": null } }`): inventoryAgents dereferenced the null entry
and threw, so readSessionDetail returned 500 instead of recovering main from
the on-disk agents/main/wire.jsonl. inventoryAgents now skips non-object
entries, letting the disk-discovery fallback take over.

* fix(vis): reset timeline agent on session change; preserve context on zero-usage steps

- Timeline tab kept the previously-selected agent id across session navigation,
  so a subagent selection would 404 against the next session. Reset it to main
  on sessionId change, mirroring WireTab/ContextTab.
- A zero-usage step.end (e.g. a content-filtered response) reset the
  context-window fill to 0, pushing a false drop into the Timeline chart and the
  Context tab. agent-core's ContextMemory keeps the prior count in that case;
  the analysis lib and the context projector now do the same.

* revert: drop agent-core retries/tool-progress persistence

These were the only changes in this branch that touched agent-core. They
persisted two previously live-only signals (step retries, tool progress) to
the wire purely so the visualizer could display them — marginal features that
did not justify modifying the core loop or extending the wire surface.

Reverts the agent-core loop/type/export changes (restored to main, keeping
#1209) and its changeset, and removes the vis-side rendering and types that
consumed step.end.retries / tool.result.progress. The rest of vis is unchanged
and reads only data agent-core already persists.
2026-06-30 13:40:37 +08:00
github-actions[bot]
b41f108584
ci: release packages (#1197)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-30 12:37:57 +08:00
Haozhe
14d9e98903
feat(server): auto-refresh provider model catalog and push change events (#1207)
* feat(server): auto-refresh provider models and push change events

- add scheduled provider-model refresh in the daemon (configurable
  interval + refresh-on-start) plus manual endpoints:
  POST /providers:refresh and POST /providers/{id}:refresh
- publish global event.model_catalog.changed when a refresh changes
  the catalog so connected clients can resync
- extract the refresh orchestrator into @moonshot-ai/kimi-code-oauth so
  the CLI and server share managed/open-platform/custom-registry logic
- wire the web daemon client to the new refresh endpoints

* chore: add changeset for provider model auto-refresh

* fix(web): reload model and provider caches on catalog change events

When the daemon's scheduled refresh changes the catalog, the pushed
event.model_catalog.changed only advanced the websocket sequence, leaving
the web composer's model/provider refs stale until an unrelated reload.
Reload both caches when the event arrives.

* test(sdk): cover event.model_catalog.changed in event exhaustiveness
2026-06-30 12:29:10 +08:00
qer
636ccc40f1
fix(web): fix mobile Safari composer toolbar overlap and focus zoom (#1212)
* fix(web): keep composer visible above the mobile Safari toolbar and keyboard

* fix(web): scope the mobile Safari composer fix to the toolbar case

* fix(web): prevent page zoom when focusing the mobile composer
2026-06-30 12:15:14 +08:00
Kai
063538744f
refactor(agent-core): align malformed tool args with schema validation (#1209)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core): repair malformed tool args JSON

Attempt jsonrepair when tool call arguments fail JSON.parse, then continue schema validation. Return malformed JSON errors with a concise expected schema hint so the model can retry with corrected arguments.

* chore(nix): update pnpm deps hash

Update the fixed-output pnpmDeps hash after adding jsonrepair so the Nix build can fetch dependencies.

* refactor(agent-core): drop tool args JSON repair

Stop repairing malformed tool call arguments. Fall back to an empty object on JSON parse failure and let schema validation produce the retry error, while preserving valid arguments for unknown tools in the transcript.

* chore: add changeset for tool args validation
2026-06-29 23:24:03 +08:00
7Sageer
53e79e4bae
chore(telemetry): add ripgrep fallback telemetry (#1203)
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
2026-06-29 18:30:09 +08:00
7Sageer
c82dcf9cd8
refactor(agent-core): use ripgrep for Glob tool (#1068)
* refactor(agent-core): use ripgrep for Glob tool

Glob now shares Grep's ripgrep subprocess plumbing: it respects .gitignore by default, supports brace patterns natively, adds an include_ignored option, and returns only files.

* fix(glob): address review findings on ripgrep migration

- Run rg with cwd pinned to the search root so glob patterns containing
  a slash (e.g. src/**/*.ts) match under an absolute search root.
- Keep include_dirs as a deprecated, ignored parameter so older calls
  are not rejected by parameter validation.
- Surface stdout truncation and drop half-written trailing paths when
  the rg output buffer is capped.
- Document that a bare pattern (e.g. *.ts) matches recursively, and sync
  user docs, the explore profile prompt, and the TUI summary to the new
  files-only / gitignore behavior.
- Add real-ripgrep integration tests covering sort order, recursion,
  brace patterns, and the absolute-search-root case.

* fix(glob): keep partial results on traversal errors

---------

Co-authored-by: hynor <hynor@users.noreply.github.com>
Co-authored-by: Kai <me@kaiyi.cool>
2026-06-29 17:40:58 +08:00
7Sageer
10ffb7d9f9
chore(telemetry): normalize telemetry property keys to snake_case (#1196)
- Rename camelCase telemetry keys to snake_case on compaction_finished, compaction_failed, micro_compaction_finished, and the tool error event (tokens_before, tokens_after, compacted_count, retry_count, thinking_level, error_type, input_tokens/output_tokens, and the micro compaction config/effect keys).

- Emit a fixed client-attribution key set (client_id/name/version/ui_mode, null when absent) from both session_started producers (core-impl and kimi-harness) so they share a stable schema.

- Drop the duplicate current/latest keys on update_prompted and the redundant ui_mode on server_started.

- Additive fields: login.method=oauth and question_answered.answered.

Telemetry-only change; no changeset.
2026-06-29 17:40:50 +08:00
Haozhe
3e98e709b3
docs(changelog): sync 0.20.2 from apps/kimi-code/CHANGELOG.md (#1198) 2026-06-29 16:51:23 +08:00
liruifengv
0df1812502
fix: render provider HTML error messages instead of blank lines (#1191)
When a provider returns an HTML error page (e.g. nginx 413 Request Entity Too Large), the error message carried CRLF line endings and raw HTML. The trailing carriage returns made the TUI render the error line as blank. Extract the page title for the wire message and strip carriage returns before rendering.
2026-06-29 15:49:48 +08:00
github-actions[bot]
52e2719d17
ci: release packages (#1154)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 14:49:34 +08:00
Haozhe
c8d1d76de5
test(agent-core): align turn telemetry expectations with provider_type (#1194)
Update the turn_started/turn_ended and api_error telemetry assertions to
expect provider_type, matching the telemetry property rename in #1184.
2026-06-29 14:44:26 +08:00
Kai
821847cb4b
feat(managed-kimi-code): route anthropic protocol via beta api (#1186)
* feat(managed-kimi-code): route anthropic protocol via beta api

- kosong: add betaApi option to use client.beta.messages.create
- agent-core: thread alias betaApi into the anthropic provider config
- oauth: route managed models on the anthropic protocol through the beta Messages API

* feat(providers): add KIMI_CODE_CUSTOM_HEADERS support

- Add KIMI_CODE_CUSTOM_HEADERS env var for custom outbound LLM headers
- Send User-Agent to non-Kimi providers
- Forward Kimi identity headers to model catalog fetches
- Support defaultHeaders in Google GenAI provider

* feat(agent-core): add protocol attrs to turn and api error telemetry

- Add type/protocol/alias to api_error for per-protocol error attribution

- Add turn_ended event with reason/duration/mode/type/protocol

- Add type/protocol to turn_interrupted

* chore(oauth): remove hardcoded internal dev endpoint from shared OAuth base URLs

---------

Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
2026-06-29 14:24:01 +08:00
liruifengv
04b3492e74
fix(tui): keep working tips out of the agent swarm progress line (#1189)
* fix(tui): keep working tips out of the agent swarm progress line

The activity loader is shared between the activity pane and the agent swarm progress status line. Tips were written into the loader, so they leaked into the swarm progress line and got squeezed against the bar. Keep the inline spinner text used by the swarm progress line free of tips, while the loader's own row in the activity pane still shows them.

* test(tui): stop moon loader timers in tests

MoonLoader starts a real setInterval in its constructor. Stop every loader created in the tests via afterEach so no live timer leaks past the file.
2026-06-29 14:21:16 +08:00
Haozhe
c99bd1c10e
chore(changeset): downgrade web-completion-sound changeset to patch (#1190) 2026-06-29 14:03:29 +08:00
liruifengv
db5fbc53c0
fix(tui): stop full-screen redraw on input and slash panel toggle (#1188)
Remove the global clear-on-shrink setting so typing in the input box and opening or closing the slash panel no longer trigger a full-screen redraw.
2026-06-29 13:51:31 +08:00
liruifengv
97f9263c6f
fix(tui): clear debug timing status on undo (#1187) 2026-06-29 13:31:08 +08:00
7Sageer
49e93893a6
refactor: standardize telemetry property names (#1184)
Align outlier telemetry events with the conventions already used by
tool_call, api_error, permission_approval_result, and the plan events:

- duration / latency_ms / duration_s -> duration_ms
- success boolean -> outcome enum ('success' | 'error')
- bare type -> provider_type

The exit event switches from seconds to milliseconds to match the
duration_ms convention, so its numeric scale changes accordingly.
2026-06-29 12:34:38 +08:00
qer
1dab2c2268
feat(web): preserve side panel and scope input history per session (#1181)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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
* feat(web): preserve open side panel across session switches

* feat(web): scope composer input history to current session

* fix(web): suppress side panel open animation on session switch

* fix(web): preserve per-session scroll position on session switch

* chore: add changeset for per-session scroll position

* fix(web): preserve follow-bottom state per session

* fix(web): scope composer attachments to their session

* fix(web): restore saved scroll position on session switch
2026-06-29 01:52:08 +08:00
qer
fc3d69dbdc
feat(web): add completion sound and question notifications (#1179)
* feat(web): play a sound when a turn completes

Synthesize a short chime when a session finishes a turn. Opt-in via Settings -> Notifications (off by default); the audio context is unlocked on the first user gesture so it also plays while the tab is backgrounded.

* feat(web): notify and play a sound when a question needs an answer

Reuse the existing notification/sound toggles so they also fire when the agent asks a question (the awaiting-answer state). Generalize the Settings labels to cover both cases.

* fix(web): don't queue the chime on a suspended audio context

A suspended AudioContext has a frozen clock, so tones scheduled on it would play stale when the context later resumes (e.g. on the next click). Only schedule the chime when the context is actually running; if it is still suspended, try to unlock it for next time and skip this one.

* fix(web): gate question notifications behind explicit opt-in

Question notifications surface question text, so they must not fire for users who only opted into turn-completion alerts (which default on). Split question notifications into their own persisted preference that defaults off, with a separate Settings toggle. Completion notifications keep their existing default-on behavior.

* fix(web): show the question text in question notifications

Lead with the actionable question text in the desktop notification body, keeping the short header as context (e.g. 'Storage: Which database?'). Previously the header alone was shown, so users had to open the tab to learn what was being asked.
2026-06-28 21:02:00 +08:00
qer
c63edd5bf6
refactor(web): unify new-session creation behind the first message (#1167)
Some checks are pending
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (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
Remove the NewSessionDialog path so every new-session entry in the web UI enters the onboarding composer, creating the session only when the first message is sent. This removes the last web flow that produced an empty session.
2026-06-28 17:01:26 +08:00
qer
dfcfdfd9dd
feat(web): hide empty sessions from the session list (#1166)
* chore(web): remove the /sessions slash command

* feat(web): hide empty sessions from the session list

Add an optional exclude_empty parameter to the session list API; the web client passes it so unused "New Session" entries are hidden by default, with pagination and has_more computed on the filtered set.

* fix(protocol): add exclude_empty to the session list query schema

Keep the shared protocol schema in sync with the server route so clients using the protocol type see the new parameter.

* fix(protocol): keep exclude_empty off the child session list schema

listSessionChildrenQuerySchema aliased the main list schema, so it inherited exclude_empty even though the /sessions/{id}/children route does not filter by it. Split it so generated clients are not misled.
2026-06-28 13:38:46 +08:00
Haozhe
cf558cd742
feat(managed-kimi-code): support Anthropic-compatible protocol (#1170)
* fix(agent-core): recover from context overflow 413

- track provider-observed effective context limit after overflow
- compact with the reduced limit before retrying the turn
- treat large plain 413 responses as recoverable context overflow
- add CLI patch changeset

* feat(managed-kimi-code): support Anthropic-compatible protocol

- switch managed provider to anthropic when models declare anthropic protocol
- add base64 video content blocks to the kosong anthropic provider
- downgrade unsupported media parts to text placeholders by capability
- pass prompt cache key as Anthropic metadata.user_id for session affinity

* feat(agent-core): add protocol/type to request and video upload telemetry

- turn_started now carries `type` (configured provider wire type) and
  `protocol` (effective transport, i.e. alias.protocol ?? provider.type)
- new video_upload event reports mime type, size, latency and
  success/failure, plus type/protocol/model context
- ResolvedRuntimeProvider gains `type` and `protocol` fields
2026-06-28 13:04:03 +08:00
qer
f3b15322da
fix(web): replace the web composer attach plus icon with an image icon (#1165)
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (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-06-28 02:18:11 +08:00
qer
ff6e8bbd7c
fix(web): clear composer draft synchronously on send (#1163)
When the first message of an empty session is submitted, the optimistic
user turn unmounts the empty-session composer before the post-flush text
watcher can persist the cleared draft. The docked composer then mounts
and reloads the stale text from localStorage.

Clear the persisted draft synchronously in the submit / steer / slash
command paths instead of relying on the text watcher, so the next mount
always starts empty.
2026-06-28 01:55:04 +08:00
qer
b0708464f4
feat(kimi-web): rework ask-user-question card as step-by-step wizard (#1162)
* chore(web): show five sessions per workspace

* feat(kimi-web): rework ask-user-question card as step-by-step wizard
2026-06-28 01:24:38 +08:00
qer
d968642384
chore(web): show five sessions per workspace (#1161) 2026-06-28 01:07:51 +08:00
qer
23a553bb91
feat(web): focus composer on /new and /clear, keep viewport scalable (#1159) 2026-06-28 00:48:19 +08:00
qer
794db55538
fix(agent-core): cap compaction output at 128k by default (#1156) 2026-06-27 23:24:57 +08:00
qer
54baf5d07f
chore(deps): upgrade web markdown renderer dependencies (#1155)
Some checks are pending
CI / build (push) Waiting to run
CI / test (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 / Native release artifact (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* chore(deps): upgrade web markdown renderer dependencies

- katex: ^0.16.22 -> ^0.17.0

- markstream-vue: 1.0.3 -> ^1.0.4

- shiki: ^4.2.0 -> ^4.3.0

* chore(deps): update pnpm deps hash in flake.nix

Required after upgrading katex, markstream-vue, and shiki.
2026-06-27 18:18:04 +08:00
Qkunio
d02b5c4984
fix(agent-core): use max output size for compaction budget (#1129)
* fix(agent-core): use max output size for compaction budget

* chore: add changeset for compaction max output size fix

* docs: rephrase compaction changeset

---------

Co-authored-by: qkunio <qkunio@163.com>
Co-authored-by: qer <wbxl2000@outlook.com>
2026-06-27 16:40:31 +08:00
liruifengv
278984dee0
ci: disable windows test job (#1144)
Some checks are pending
CI / typecheck (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (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 / Publish native release assets (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
2026-06-26 21:47:44 +08:00
qer
7eca38aa52
docs: sync 0.20.1 changelog and document plugin hooks (#1142) 2026-06-26 20:06:46 +08:00
github-actions[bot]
da63403207
ci: release packages (#1124)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-26 19:17:49 +08:00
qer
76c643bcb6
feat: cap completion tokens to remaining context window for chat-completions (#1131)
* feat: cap completion tokens to remaining context window for chat-completions

* test: cover dynamic completion budget for kimi and openai-legacy

* fix: leave compaction budget uncapped to avoid one-token summaries

* fix: add hookCount to plugins-selector test mocks
2026-06-26 19:12:04 +08:00
qer
f8a638d7e3
chore: downgrade plugin hooks changeset to patch (#1137) 2026-06-26 18:49:42 +08:00
qer
bf51fb7a10
fix(server): skip Unix-only permission check on Windows for server token (#1135) 2026-06-26 18:38:24 +08:00
liruifengv
b0b2aee8c5
perf(tui): keep long conversations responsive (#1119)
* perf(tui): cache rendered message lines across frames

Cache render(width) output in the transcript container and message components, returning cached lines when content, theme, and width are unchanged. Removes the per-frame full-transcript re-render that caused the TUI to lag as history grew.

* perf(tui): bound transcript with sliding window and step merging

Keep the TUI responsive as conversations grow by bounding the live
transcript:

- Sliding window: keep only the most recent 50 turns in the component
  tree; older turns are destroyed (entry + component).
- Step merging: within each turn, keep only the most recent 30
  thinking / tool steps rendered; older ones collapse into a summary.
- Expand (Ctrl+O) only reaches the most recent 3 turns.

All thresholds are overridable via KIMI_CODE_TUI_* env vars; 0
disables the corresponding feature.

* chore: add changeset for tui transcript window

* chore(tui): remove KIMI_TUI_PERF render timing log
2026-06-26 18:32:32 +08:00
qer
f1c8175f9c
fix(tui): carry server token in /web and print it on exit (#1133)
* fix(tui): show the server token when handing off via /web

The /web slash command opened the session deep link without the bearer token, so the web UI was not authenticated and the token was never shown, unlike the kimi web subcommand. Resolve the persistent server token, append it as the #token= fragment so the browser signs in on load, and show it in green below the status line so it can be copied before the terminal exits.

* test(plugins-selector): add required hookCount to fixtures

PluginSummary/PluginInfo gained a required hookCount field, so the app's typecheck failed on fixtures that did not provide it. Add hookCount: 0 to the test summaries (none of these fixtures declare hooks).
2026-06-26 18:04:51 +08:00
liruifengv
e5eaeb4634
ci: skip server e2e tests on windows (#1126) 2026-06-26 18:01:22 +08:00
qer
81ba48f455
feat(web): auto-grow composer and add expandable editing mode (#1121)
* feat(web): auto-grow composer and add expandable editing mode

- Grow the chat textarea with its content up to a 1/4-viewport cap.
- Add an expand toggle above the send button for a taller editor; in that
  mode Enter inserts a newline and Cmd/Ctrl+Enter or the button sends,
  then the editor collapses back.

* fix(web): reset expanded composer state on session change

The composer instance is reused across sessions (not keyed by session id), so the expanded preference leaked into the next session's draft, leaving it stuck in the tall editor with Enter inserting newlines. Collapse back when the active session changes.

* fix(web): match expand-toggle threshold to theme resting height

The modern/kimi global theme overrides the composer min-height to 40px (the scoped default is 56px), so a hard-coded 56px threshold kept the expand toggle hidden until a third line under the default theme. Read the computed min-height from the element instead.

* fix(web): recompute expand-toggle visibility after collapsing

While expanded the computed min-height is 70vh, so a multi-line draft measured there sets isGrown=false. Collapsing did not recompute it, hiding the toggle even though the collapsed draft was still multi-line. Recompute growth after every toggle via a shared helper. The expanded state itself is unchanged and stays at 70vh until toggled or sent.

* fix(web): collapse expanded editor on slash-command submit

Known slash commands return early from handleSubmit, above the post-send collapse, so sending an expanded /goal, /btw, /compact, or skill command left an empty 70vh editor. Collapse in the slash-command path too.

* fix(web): refocus textarea after toggling expand

Clicking the expand toggle leaves focus on the button, so subsequent keystrokes do not reach the textarea and Enter would activate the button again instead of inserting a newline. Return focus to the textarea after toggling.

* fix(web): refit textarea when collapsing after image-only sends

When the expanded editor collapses on an image-only send, the text is already empty so the draft watcher never re-runs autosize; the textarea kept the inline height measured at 70vh and the collapsed cap left an oversized empty box. Route all send/steer collapses through a helper that re-runs autosize after the 70vh min-height is removed.

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-06-26 17:21:51 +08:00
Haozhe
0886bff2bc
feat(server): add --allowed-host flag for DNS-rebinding allowlist (#1128)
- add `kimi server run --allowed-host <host...>` (repeatable or
  comma-separated; leading dot matches a domain suffix) and thread it
  through daemon spawn into startServer
- merge CLI allowed hosts with KIMI_CODE_ALLOWED_HOSTS for both the HTTP
  and WebSocket Host checks
- include the rejected host and allow guidance in the 403 error message
2026-06-26 17:06:01 +08:00
qer
184acf5db5
feat: support hooks in plugins (#1127)
* feat(agent-core): support per-hook cwd and env in HookEngine

* feat(agent-core): support hooks in plugin manifest and aggregate via PluginManager

* feat(agent-core): merge plugin hooks into session hook engine

* chore: add changeset for plugin hooks
2026-06-26 17:02:14 +08:00
7Sageer
28d358b526
chore: downgrade feedback changeset to patch (#1130) 2026-06-26 16:57:00 +08:00
Kai
9c9716125e
feat: Harden the default system prompt and built-in tool descriptions (#1102)
* feat(agent-core): strengthen default system prompt

Add high-confidence, prompt-only guardrails to the default agent system prompt:

- Personality/candor: extend the HELPFUL/CONCISE/ACCURATE line with CANDID, and
  require plainly stating what could not be run, reproduced, or verified.
- Reminders: avoid cheerleading; voice evidence-based disagreement; deliver
  complete code with no placeholders; update now-stale comments/docstrings after
  a change; re-check the user's latest request before finalizing a reply.
- Context Management: explain automatic compaction — continue from the summary,
  re-establish transient state with tools, do not restart from scratch.
- Output formatting: replies render as Markdown in the terminal; keep lists flat;
  no emojis unless the user uses them first.
- Project Information: frame injected AGENTS.md as project context, not a
  privileged instruction channel that can override system rules.

Prompt text only; no code or template-variable changes.

* feat(agent-core): hoist key working rules into the system prompt

Lift a few high-leverage rules from individual tool descriptions up into the
default system prompt, so they shape default behavior before any specific tool
is in play (kept terse and integrated, not bolted on):

- Planning: for multi-step or multi-file work, maintain a `TodoList` (one item
  in_progress, mark done as it finishes) and prefer `EnterPlanMode` first when
  the approach isn't settled.
- Default to making progress, not asking: once the goal is clear and sanctioned,
  carry it through and work blockers yourself; ask only when the answer would
  change the next step. Explicitly does not override stopping to discuss an
  unclear goal or waiting for go-ahead before writing code.
- Tool routing: prefer dedicated tools (Read/Glob/Grep/Write/Edit) over raw
  shell when one fits; keep Bash for genuine shell work.
- Definition of done: verify with the checks that cover the change before
  marking it complete, independent of whether a TodoList is in use.
- Delegation: explore subagents also keep intermediate file contents out of your
  own context — you get a conclusion back, not a pile of dumps.

Prompt text only; no code or template-variable changes.

* fix: clarify guidelines for file pattern matching and tool usage in explore.yaml and system.md

* fix(agent-core): hide the Skills section from agents without the Skill tool

Subagents (coder/explore/plan) inherit the root system prompt but lack the Skill tool, yet KIMI_SKILLS was rendered unconditionally — leaking the full skill listing into agents that cannot invoke any skill. Gate KIMI_SKILLS on the profile's tool set and wrap the '# Skills' section in {% if KIMI_SKILLS %} so it disappears for those profiles.

Also note in the Working Directory section that Bash enforces none of the workspace/secret-file guards, so the model must hold that discipline itself.

Tests: assert the Skills section renders for the root agent and is absent for Skill-less subagents; update the prompt-rendering fixtures for the new gating.

* fix(agent-core): gate Agent, background-task, and TodoList guidance by tool availability

Subagents (coder/explore/plan) inherit the root system prompt but lack the Agent, TaskList, and TodoList tools, so they were shown usage guidance for tools they cannot call. Derive HAS_AGENT/HAS_TASKLIST/HAS_TODOLIST from each profile's tool set and gate those sections with inline {% if %}, so they render only for agents that hold the tool.

Root rendering is byte-identical (the inline tags collapse to the original text when the flag is set). The cross-tool secret-file guard stays shared, since explore/plan still hold Read/Grep/Glob.

Tests: assert the gated guidance is present for the root agent and absent for explore/plan, while the shared secret-file guard remains.

* refactor(agent-core): move Agent-delegation and Glob-anchor guidance into the tool descriptions

The Agent-delegation paragraph in the system prompt duplicated mechanics already documented on the Agent tool itself (new-vs-resume, zero-context briefing, foreground default / run_in_background threshold), so remove it. HAS_AGENT still gates the explore-delegation bullet, which carries the 'when to delegate' nudge the tool description deliberately omits.

Move the proactive 'anchor the pattern up front' guidance into the Glob tool description (it previously only described the reactive 'refine after hitting the cap' path) and drop the now-redundant Glob bullet from the system prompt.

Tests: drop the assertions tied to the removed Agent paragraph; HAS_AGENT gating stays covered via the explore bullet.

* test(agent-core): add guidance for blast-radius and concrete examples in agent profiles

* docs: update descriptions for skill-tool and fetch-url; enhance web-search citation instructions

* feat(agent-core): disclose enforced constraints in tool descriptions; fix GetGoal field doc

Surface runtime-enforced behavior in the Agent / AgentSwarm / AskUserQuestion / Goal
tool descriptions so the model learns the rules from the tool, not from a failed call:

- Agent: resuming excludes subagent_type (setting both is rejected)
- AgentSwarm: at least 2 items unless resuming, prompt_template required and must
  contain {{item}}, distinct resulting prompts; plus Agent-vs-AgentSwarm fan-out note
- AskUserQuestion: result is {answers}; an empty answers with a dismissal note means
  the user declined — fall back to best judgment instead of re-asking
- CreateGoal: creating fails when a goal already exists (use replace)
- SetGoalBudget: state the hard 1s-24h time-budget band
- UpdateGoal: do not mark blocked merely because work is hard/slow/incomplete
- GetGoal: drop the advertised self-report / evaluator-verdict fields — GoalSnapshot
  never held them, so the tool never returned them

Each change is covered by a description assertion.

* fix(agent-core): soften AskUserQuestion answers-keying wording to match the code

The answers object is passed through from the host/RPC layer (QuestionAnswers is
Record<string, string | true>); this code does not key it by question text. Describe
what the keys identify instead of asserting a guarantee the code does not provide.

* feat(agent-core): tighten Bash/Grep/Write/Edit tool descriptions

- Bash: prefer the cwd argument (or absolute paths) over a cd from an earlier
  call, since each call runs in a fresh shell
- Grep: note that files_with_matches is ordered most-recently-modified first
- Write: do not create documentation/README files unless the user asks
- Edit: frame replace_all with its rename-across-file use-case

Each change is covered by a description assertion.

* feat(agent-core): refine plan-mode/todo/cron tool descriptions

- ExitPlanMode: describe what a good plan contains (specific, verifiable steps
  grounded in the codebase, not vague filler)
- TodoList: stop calling it useful 'in Plan mode' — plan-mode planning goes to
  the plan file; TodoList tracks execution progress
- CronCreate: warn that a one-shot whose pinned day/month already passed this
  year is rejected; document the 50-task session cap and the 8 KiB prompt cap
- CronCreate: drop the bench-only KIMI_CRON_NO_STALE / KIMI_CRON_NO_JITTER env
  knobs from the model-facing description (CI-only; the model never sets them)

Each change is covered by a description assertion.

* refactor(agent-core): dedupe ExitPlanMode options docs into the param schema; trim EnterPlanMode workflow

- ExitPlanMode: the options field mechanics (label format, recommended, count,
  single-option=plain-approval, reserved labels) now live only in the options
  param describe; the tool description routes to it and keeps the yolo/manual UI
  behavior it uniquely documents. The options consistency test now enforces a
  single source of truth (describe) plus the schema-consistency guard, instead
  of requiring the same facts in both surfaces.
- EnterPlanMode: trim the duplicated 'What Happens in Plan Mode' steps to a
  pointer (the full workflow is injected unconditionally once plan mode is
  active), keeping the explore-subagent recommendation.

* fix(agent-core): correct prompt/code inaccuracies found in the final audit

Every item below was re-verified against the live code:

- Skill: drop the never-fired recursion-depth cap (production never seeds depth);
  keep the <kimi-skill-loaded> 'already loaded, don't re-invoke' guard
- TaskOutput: terminal_reason can also be `failed`, not just timed_out/stopped
- Grep: count_matches emits per-file `path:count`, with the total reported separately
- Plan mode: the reminder names TaskStop/CronCreate/CronDelete as blocked (they are
  hard-denied by plan-mode-guard-deny)
- Bash: the failure trailer is non-zero-exit-specific; timeout/interrupt differ
- CreateGoal: replace also covers a blocked goal, not just active/paused
- UpdateGoal: it also injects the completion/blocked outcome prompt, so it does more
  than 'only record the status'
- FetchURL: state the universal http/https contract instead of provider-internal SSRF
  and 10 MiB limits (the primary Moonshot fetcher enforces neither)
- TodoList: query mode triggers on omitting `todos`, not on zero args
- TaskList: command/PID/exit code are shell-task fields only
- CronCreate: the returned fields include `cron`
- SetGoalBudget: turn/token budgets are rounded up to >= 1, not rejected below 1

Each change is covered by a description/param assertion; plan.test.ts snapshots
refreshed for the longer plan-mode reminder.

* fix(agent-core): gate prompt tool guidance on runtime availability, not declared profile tools

The HAS_* / Skills gating computed flags from the profile's declared tools, but
Agent/AgentSwarm only register when a subagentHost exists (ToolManager
.initializeBuiltinTools). A runtime built without a subagentHost (e.g. direct SDK
construction) therefore rendered the explore-delegation guidance for an Agent
tool the model could not call.

SystemPromptContext now carries an optional availableTools; buildTemplateVars
gates on it when present and falls back to the declared tools otherwise. useProfile
passes the profile tools minus Agent/AgentSwarm when no subagentHost is wired, so
the render reflects what the model can actually call. The normal session path
(subagentHost always defaulted) is unchanged.

* fix(agent-core): exempt the plan-mode plan file from the Write *.md ban

Plan mode writes its plan to plans/<id>.md (plan/index.ts) and the reminder tells
the model to create it with Write when missing, which contradicted Write's blanket
'do not create *.md unless asked' guard. Carve the plan file out of the ban.

* fix(agent-core): scope plan-mode prompt guidance and the Write *.md ban to runtime reality

- Gate the TodoList bullet's "enter plan mode via EnterPlanMode" suggestion
  on a new HAS_ENTERPLANMODE flag. A custom profile that keeps TodoList but
  drops EnterPlanMode no longer steers the model toward a tool it cannot call;
  the default profile render is unchanged.
- Reframe the Write *.md prohibition around intent (unsolicited docs) instead
  of a blanket extension ban, so artifacts a task or project instruction
  requires — the plan-mode plan file, a repo-mandated changeset — are no
  longer contradicted by the tool's own rules.

* refactor(agent-core): move tool-coupled guidance into tool descriptions

The default system prompt carried tool-usage guidance behind {% if HAS_* %}
gates that re-derived, in prose, the availability the tool schema already
encodes — and the same guidance was duplicated in each tool's own
description. Drop the four gated blocks (background Bash, Agent/explore,
TodoList, EnterPlanMode) and the compaction TaskList/TodoList bullets; the
tool descriptions, shipped only when the tool is registered, already carry
the same instructions, so subagents and tool-trimmed profiles are no longer
pointed at tools they lack.

Fold the two genuinely unique lines into the tool descriptions: bash.md
gains "return control after starting a background task", agent.md gains the
context-hygiene reason to delegate. Collapse the compaction bullets into one
tool-agnostic sentence. Remove the now-unused availableTools / HAS_* render
machinery.

* fix(skill-tool): clarify no-reinvoke guard and argument handling in tool description

* feat(fetch-url): indicate content retrieval mode in output for better model context

* fix(agent-core): correct goal-budget rounding and task-output failure docs

set-goal-budget.md said turn/token budgets are "rounded up", but the code uses
Math.round — say "rounded to the nearest whole number" instead. task-output.md
implied every failed task carries terminal_reason/stop_reason, but a plain
non-zero command exit carries only status plus exit_code; describe that exit_code
path and reserve terminal_reason for non-exit endings (timeout, explicit stop,
or an internal error with no exit code).

* fix(agent-core): scope free-work guidance by role and steer one-shots near-term

The blast-radius paragraph told every profile that local work — including
editing files — may be done freely, but the read-only explore/plan subagents
render it too; scope it to "work your role permits" so it no longer undercuts
their read-only constraints.

The one-shot cron guidance leaned on a year-boundary heuristic ("avoid a
day/month already passed this year") that misfires across Dec 31 to Jan 1 and
duplicated a limit the code already enforces. Replace it with a plain near-term
nudge and leave the hard future-window guard in code.

* fix(enter-plan-mode): clarify availability of Agent tool in plan mode description

* fix(agent-core): surface Grep count_matches total and pagination in output

count_matches put the aggregate "Found N occurrences" summary and the
"Results truncated... use offset=N to see more" notice on the result's
message field, which normalizeToolResult drops before the result reaches the
model. The model saw only the path:count lines and could miss the total and,
worse, the pagination cue — so it would not know to page through truncated
counts. Append both to output after the path:count lines, the same way the
content and files_with_matches modes already inline their notices.

* fix(grep): reorder count summary and results in output for clarity

* chore(changeset): consolidate prompt-hardening changesets into one

Squash the five per-change changesets for this PR into a single concise
entry; they all bump @moonshot-ai/kimi-code (patch) for the same
system-prompt and tool-description hardening work.

* fix(agent-core): stop the agent from blocking on background tasks

Both the Agent and Bash background-launch messages invited the model to "peek
at progress" via TaskOutput, and the foreground-vs-background guidance had been
thinned to a single parameter hint. Together that led the model to launch a
background subagent and then immediately wait on it through TaskOutput —
defeating the point of background execution.

Make both launch messages take the same anti-wait stance the user-detach path
already uses (do NOT wait, poll, or call TaskOutput on it), restore
foreground-by-default guidance in the Agent background description (run in the
background only when you have other work and do not need the result to proceed),
and add a TaskOutput backstop against using it to sit and wait. Also fold the
fix into the consolidated changeset.
2026-06-26 16:56:40 +08:00
Kai
e9a3b7c83a
feat(cli): add update alias for upgrade command (#1125)
Register a `kimi update` alias for the existing `kimi upgrade` command via commander's .alias(), so both forms run the same upgrade flow. Document the alias in the command reference and add a routing test.
2026-06-26 16:48:45 +08:00
7Sageer
e736349a7c
feat(feedback): support attaching logs and codebase (#1120)
* feat(feedback): support attaching logs and codebase

Add an attachment picker to /feedback (none / logs / logs + codebase).
Codebase uploads scan the working directory with sensitive files excluded
and are sent through a new multipart upload API on the oauth/node-sdk layers.

* fix(feedback): fall back to logs when codebase scan fails

* tiny fix

* fix(feedback): make diagnostic uploads partial-safe

* refactor(feedback): reuse harness session export and normalize upload url types

* docs(slash-commands): note optional feedback attachments

* refactor(feedback): reorganize feedback upload modules

Move the attachment orchestration out of tui/commands/info.ts into a
dedicated feedback/feedback-attachments.ts, and split the former
codebase-upload/attach.ts into a generic multipart uploader
(feedback/upload.ts) and an archive lifecycle module
(feedback/archive.ts). Both session and codebase archives now flow
through a single upload lifecycle, which also removes the temp-dir
leak that occurred when codebase packaging failed.

Rename FeedbackCodebaseArchive to FeedbackArchive and the
codebase-upload/ directory to codebase/ so module boundaries match
their actual responsibilities (scan + package only).
2026-06-26 16:15:08 +08:00
liruifengv
820d77ab4c
feat(tui): show hidden todo status breakdown in collapsed panel (#1122) 2026-06-26 15:50:58 +08:00
liruifengv
36cbdb29c0
ci: bump windows test timeout to 30s (#1123) 2026-06-26 15:35:56 +08:00
liruifengv
b51e13538d
ci: run unit tests on windows (#1037)
* ci: run unit tests on windows

* fix(migration-legacy): align workdir bucket key with agent-core

computeWorkdirBucket used a local node:path-based resolve that yields backslash-separated paths on Windows, while agent-core's encodeWorkDirKey uses pathe (forward slashes on every platform). The SHA-256 inputs diverged, so migrated sessions were written to a bucket that the session picker never reads, making them invisible on Windows.

Alias computeWorkdirBucket to encodeWorkDirKey so both sides stay byte-identical, drop the local slugify copy, and update the workdir-bucket test reference accordingly.

* test(acp-adapter): expect platform-native separators in e2e-fs path

The e2e-fs test asserted the fs/readTextFile wire path as the raw POSIX targetPath, but AcpKaos.toClientPath converts '/' to '\' when the inner LocalKaos reports pathClass 'win32' (Windows). On Windows the wire path became '\Users\test\x.ts' and the assertion failed.

Mirror toClientPath in the test: expect backslash separators on win32 and the raw path otherwise. Implementation is unchanged.

* test(sdk): normalize workDir and skillDir paths in session tests

SessionStore.create/list and the skill loader normalize paths through pathe (forward slashes). The SDK tests compared the resulting workDir and skill loaded-dir against raw mkdtemp / node:path strings, which use backslashes on Windows (and node:fs realpath also returns backslashes for the skill dir), failing three toMatchObject assertions.

Build the expected paths with agent-core's normalizeWorkDir so they match the internal pathe representation on every platform. The skill dir keeps its realpath() (the loader realpaths the root) and only normalizes separators.

* test(skill): normalize realpath to forward slashes in scanner tests

resolveSkillRoots normalizes every root.path through fs.realpath followed by replacing backslashes with forward slashes (scanner.ts). The scanner tests compared root.path against node:fs realpath directly, which returns backslashes on Windows, so twenty assertions failed (toEqual / toContain / toHaveLength) even though the resolved paths were identical.

Wrap realpath at the top of the test file to mirror the implementation's normalization, so every comparison uses the same forward-slash form on every platform.

* test: skip Unix-only permission tests on Windows

The Unix file-permission assertions (mode bits like 0o600 / 0o700 and chmod 000 making a path unreadable) have no equivalent on Windows, which uses ACLs; fs.chmod there can only toggle the read-only bit. These six tests failed on Windows with mismatched mode values or a missing 40411.

Skip them on win32 via it.skipIf(process.platform === 'win32'): oauth FileTokenStorage (0600 file, 0700 dir), agent-core BackgroundTaskPersistence (0700 tasks dir), agent-core createPerIdJsonStore (0700 subdir), migration-legacy atomicWrite (0600 file), and server fs:browse (chmod 000 -> 40411).

* test(tui): make platform-sensitive assertions cross-platform

The TUI implementations are already platform-aware (pathe-style paths, pathToFileURL, quoteShellArg cmd/POSIX quoting, Alt+V on Windows for paste expansion), but the tests hard-coded POSIX expectations and failed on Windows.

Align the assertions with the implementation's platform behavior: footer-goal-badge matches the '[goal' badge prefix instead of /goal/ (toolbar tips contain '/goal'); tool-call expects backslash relative paths on win32; plan-box builds the file:// URL via pathToFileURL; custom-editor sends Alt+V on win32 for paste expansion; file-mention-provider normalizes the expected description to forward slashes; kimi-tui-startup builds the resume command with quoteShellArg; kimi-tui-message-flow builds the expected install path with resolve().

* test: align path assertions with pathe on Windows

Several test suites asserted paths produced by node:path/node:os/node:fs against values that agent-core, node-sdk and kaos normalize through pathe (forward slashes). On Windows the two forms diverge (backslashes vs forward slashes), failing about 19 assertions.

Mirror the implementation's normalization in the assertions via a local toPosix helper (or agent-core's normalizeWorkDir), so expected paths use forward slashes on every platform: kaos LocalKaos, node-sdk export/list/resume/config/transport sessions, cli FileMentionProvider, and agent-core skill-session.

* test(native): build path expectations with node:path.resolve

paths.mjs builds every path with node:path.resolve, which yields backslash-separated absolute paths on Windows. The path-helpers tests asserted against template strings that mixed the backslash appRoot with forward-slash segments, so Object.is failed on Windows even though the strings looked identical.

Build the expectations with the same resolve(appRoot, ...) helper so the separators match on every platform.

* fix: make Windows CI tests pass across all packages

Fix the remaining Windows CI failures so the Windows test job can go green. The changes fall into a few categories:

- Path separators: agent-core/node-sdk/kaos normalize paths via pathe (forward slashes); align test expectations and a couple of implementations (native cache base, workspace registry) with that.

- Platform-only services: skip launchd/systemd manager suites on win32 (Windows uses schtasks).

- Process/signal lifecycle: skip or relax tests that rely on POSIX signals / SIGTERM semantics that Windows does not support.

- Hook shell syntax: rewrite hook test commands from POSIX shell (single quotes, semicolons, stderr redirects, if/then/fi) to node -e / .cjs files that run under cmd.exe.

- CRLF: make Bash tool description stripping tolerate CRLF line endings.

- Misc: realpath short-name divergence, port-retry timing, telemetry spawn, fs-watch timing, snapshot path normalization, etc.

* fix: remove unused basename import in workspaceRegistryService

Fix lint error (no-unused-vars): basename from node:path is no longer used after switching to posixBasename from pathe.

* fix: align resume harness pathClass and wait for banner state on Windows

Two more Windows CI fixes:

- createResumeNoSideEffectKaos now reports pathClass 'win32' on Windows so tool descriptions (e.g. Glob's Windows note) match the live agent in expectResumeMatches, fixing usage/description deep-equal drift.

- kimi-tui-startup once-banner test now waits for writeBannerDisplayState to land before asserting, since the atomic write can lag behind the render on Windows.

* fix: resolve remaining Windows unit test failures

Make the new Windows CI job green across agent-core, kaos, node-sdk and server:

- Align the resume harness kaos pathClass with the live agent so platform-conditional tool descriptions (Glob's Windows note) match in expectResumeMatches instead of drifting on win32.
- Rewrite hook commands in agent-core tests as cross-platform node one-liners; single-quote echo, >&2 and ';' do not work under cmd.exe.
- Add .gitattributes enforcing LF so raw-imported templates (e.g. the compaction instruction) produce byte-identical token counts on Windows and POSIX.
- Terminate the full process tree on Windows in both the hook runner and kaos (taskkill /T /F) so grandchildren cannot outlive their parent and keep the cwd locked.
- Normalize workDir path separators in two kimi-sdk session tests to match the stored canonical form.
- Avoid cmd.exe arg-quoting pitfalls in the kaos cmd.exe test, and run the Windows process-tree kill test from a script file with the pid path passed via argv.
- Give the first fs-git e2e test more time on Windows and retry the temp-dir cleanup; skip the fs-watch overflow-burst assertion on Windows where fs-event coalescing prevents the single-window spike.

* ci: retrigger checks

* fix: resolve remaining Windows failures after merging main

- Terminate the spawned git/gh process tree on Windows in FsGitService (taskkill /T /F on timeout) so a timed-out 'gh pr view' cannot leave a grandchild holding the workspace cwd, which made the fs-git e2e cleanup fail with EPERM.

- Give the fs:git_status e2e suite a longer timeout on Windows and retry the temp-dir cleanup longer to ride out the slower child-process teardown.

- Make the third-party plugin install trust test assert the resolved install path via node:path so it matches the Windows-resolved path (D:\tmp\...) as well as the POSIX one.

* fix: align workspace registry roots and harden fs-git cleanup on Windows

- workspace-registry test: compare normalized (forward-slash) roots, since the registry and session index both store workDir via pathe.resolve (forward slashes on every platform). realpath() yields backslashes on Windows and diverged from the stored root.

- fs-git e2e: bump the temp-dir cleanup retries and the afterEach timeout, since Windows child-process teardown after server.close() is asynchronous and can keep the workspace cwd locked for several seconds.

* test: stub openUrl in kimi-tui-message-flow feedback tests

The /feedback command falls back to openUrl(FEEDBACK_ISSUE_URL) when submission fails, which spawned a real browser window on every test run. Mock #/utils/open-url (matching the existing login/message-replay/server test convention) so the suite never opens a browser.

* test: harden fs-git e2e cleanup against Windows cwd locks

On Windows, git/gh child processes and the session core process can outlive server.close() and keep the temp workspace as their cwd, so rmSync fails with EPERM even after a long retry. Add rmSyncRobust that retries and, if the cwd is still locked, swallows EPERM/EBUSY on Windows — the OS reclaims the temp dir and a cleanup hiccup must not fail an otherwise-passing test.

* test: harden server e2e cleanup against async teardown races

server.close() does not fully await the server's asynchronous teardown, so on a loaded CI runner the temp home/workspace dirs can still be held or written to when the afterEach rmSync runs, failing with EPERM (Windows) or ENOTEMPTY (Linux). Use a rmSyncRobust helper (retry + swallow EPERM/EBUSY/ENOTEMPTY) in the fs-git and question e2e cleanup. Also fix a leftover `throw err` (renamed to `throw error`) that broke the typecheck.
2026-06-26 11:56:41 +08:00
qer
174101278d
chore: update changelog skill (#1115) 2026-06-26 03:32:09 +08:00
qer
258d248020
doc: 0.20.0 changelog (#1114) 2026-06-26 03:30:59 +08:00
4194 changed files with 612376 additions and 96196 deletions

View file

@ -0,0 +1,70 @@
---
name: agent-core-dev
description: Use when developing in packages/agent-core-v2 (the DI × Scope agent engine) — adding or modifying a domain Service, choosing a LifecycleScope, wiring DI dependencies, splitting a domain across scopes, owning or migrating a config section, gating behavior behind an experimental flag, raising coded errors, working on the permission system, writing DI/Scope tests, porting business logic from agent-core (v1) to v2, triaging a main-branch commit against v2, or exposing a v2 domain over server-v2 while keeping the /api/v1 wire contract compatible with released clients. Self-contained guide organized by development stage (orient → design → implement → test → verify) plus align workflows for v1→v2 migration, main-branch commit triage, and server-v2 wire exposure; each file carries the rules, examples, and red lines for its step.
---
# agent-core-dev
> Develop `packages/agent-core-v2` by lifecycle stage. This skill is **self-contained**: every rule, recipe, and red line lives in the stage files below — it does not delegate to `packages/agent-core-v2/docs/`.
`agent-core-v2` is the new agent engine built on the **DI × Scope** architecture (a port of `packages/agent-core`). Everything resolves through the container: a service declares an **identity**, its **dependencies**, and a **lifetime**; the container decides construction, singleton-per-scope, ordering, and disposal. The stage files restate the rules in imperative form so you can work without reading the source docs.
## Lifecycle at a glance
```text
Orient → Design → Implement → Test → Verify
│ │ │ │ │
│ │ │ │ └─ lint:imports · typecheck · test · dep graph · red lines
│ │ │ └─ test.md
│ │ └─ implement.md (+ errors.md · flags.md · permission.md)
│ └─ design.md
└─ orient.md
```
Stages are ordered but not strictly linear: a test failure (stage 4) that reveals a wrong scope sends you back to design (stage 2); a `CyclicDependencyError` sends you to `design.md` §dependency-direction and `implement.md` §cycles.
## Workflows
End-to-end procedures that span the stages. Reach for these before reading the stage files individually.
- [Align (port `agent-core` → `agent-core-v2`)](align.md): split a v1 class into semantic units, fix each unit's domain / scope / Service / dependencies, then migrate the logic and tests. Use when the task is "move feature X from v1 to v2" or "port `IXxxService` to v2".
- [Commit align (triage a `main` commit against v2)](commit-align.md): given one `main` commit hash + a short note, find the v1 logic it changed, check whether v2 already has the corresponding implementation, bucket it (aligned / partial / missing / not-applicable), and recommend a minimal fix. Use in the `kimi-code-v2`-catching-up-to-`main` phase, for one commit at a time; escalate to [align.md](align.md) if the gap is a whole domain.
- [Server align (expose `agent-core-v2` over `server-v2`)](server-align.md): wire a v2 domain into `packages/kap-server` over `/api/v2` (native) and `/api/v1` (v1-compatible mirror), keep the wire schema byte-compatible with the established v1 contract by sharing the `@moonshot-ai/protocol` schema, and isolate v1-only behavior in a `<domain>Legacy` edge adapter instead of distorting the native v2 Service. Use when the task is "expose the new v2 Service on the server", "add a route to the `/api/v1` surface", or "keep server-v2 wire-compatible with released v1 clients".
## Stages
- [Stage 1 — Orient](orient.md): the DI black box (identity / dependencies / lifetime), the four `LifecycleScope` tiers and visibility, and the file-header comment convention. Read before touching business code.
- [Stage 2 — Design a service](design.md): pick a scope, split a domain across scopes, choose a calling style (direct call vs event vs hook), and direct dependencies. Decide *where things live and who knows whom* before coding.
- Topic: [Domain boundaries vs Scope](domain-boundaries.md) — keep `session` / `agent` / `turn` from becoming god objects; data-ownership test and their split conclusions.
- Topic: [Persistence layering](persistence.md) — the three-layer `Store → Storage → backend` model, naming Stores by access pattern, and which layer business code should depend on.
- Topic: [Edge exposure — `resource:action` + WS events](edge-exposure.md) — which Services are exposed over `/api/v2` (per-scope action map) and which events stream over WS; what to wrap in a facade.
- [Stage 3 — Implement](implement.md): the standard Service recipe and the DI building blocks — interface + identity, constructor injection, scoped registration, `Disposable`, eager vs delayed, `invokeFunction`, `createInstance`, child scopes, and the cycle-refactor playbook.
- Topic: [Service authoring](service-authoring.md) — file layout, naming, contract vs impl contents, interface style, constructor/field conventions, events, multi-Service domains, comment rules.
- Topic: [Config](config.md) — the section-registry model, App vs Session split, owning a config section, the TOML format, and the env overlay.
- Topic: [Errors](errors.md) — co-located `XxxError`, the central code registry, wire serialization, boundary translation.
- Topic: [Flags](flags.md) — `registerFlagDefinition`, `IFlagService.enabled(id)`, the `[experimental]` config section, resolution precedence.
- Topic: [Permission](permission.md) — risk-only chain-of-responsibility kernel, harness constraints and product reviews as domain `onBeforeExecuteTool` veto listeners (`veto` / `allow` / `pass` / cold `waitUntil` factories), shared `toolApproval` round-trip, policy registry + composer, `modes`/`agentTypes` metadata, `resolveExecution`/`accesses`.
- Topic: [Telemetry](telemetry.md) — emitting events via `ITelemetryService`, context propagation, and appender destinations (`ConsoleAppender` / `CloudAppender`).
- [Stage 4 — Test](test.md): resolve the system under test by interface, pick `TestInstantiationService` vs `createScopedTestHost`, shared stubs, service groups, teardown.
- [Stage 5 — Verify & submit](verify.md): `lint:imports`, `typecheck`, `test`, and the pre-submit checklist.
## How to use this skill
Jump to the stage you are in and read that one file; each is self-contained and ends with its own red lines. Skim the global red lines below before submitting — they catch most mistakes across every stage. The repo's source of truth remains the code in `packages/agent-core-v2/src/`; this skill codifies the same rules so you do not have to re-derive them.
## Global red lines
Invariants that hold across every stage. Each is expanded in the stage file noted.
1. No `new` on a class whose constructor carries `@IService` deps — inject with `@IX` or `accessor.get(IX)`. (implement.md)
2. `@IX` decorates constructor parameters only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services). (service-authoring.md)
3. Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique. (implement.md)
4. Parent scope never depends on child scope — short-lived may inject long-lived, never the reverse. (orient.md)
5. No cyclic dependencies — refactor (extract a third Service / use an event / re-scope); activation timing does not break dependency cycles. (design.md, implement.md)
6. `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use. (implement.md)
7. Scope follows state identity — no `Map<sessionId, …>` at `App` to fake per-session state. (design.md)
8. Foundational layers never know upstream ones; business code never depends on the edge layer (`gateway`/`rpc`). (design.md)
9. Throw coded errors; register codes centrally; branch on `code` across the wire, never `instanceof`. (errors.md)
10. Gate unreleased behavior behind a flag contributed via `registerFlagDefinition` and resolved through `IFlagService.enabled(id)`; no ad-hoc env toggles. (flags.md)
11. Tests resolve the SUT by interface; shared stubs live under `test/`, never `src/`. (test.md)
12. Config is the preference registry: only preferences that are persistable, schema'd, and user/operator-facing go in `IConfigService`. Domain-specific config (including env-only operational toggles) goes through `registerConfigSection` + `envOverlay`. Facts → `IBootstrapService`, and host invocation arguments (CLI flags, host identity headers, prompt identity) → `BootstrapInput.args` / `IBootstrapService.args` — never new per-domain runtime-options services; domain runtime state (cron/flags/model) never goes onto `IBootstrapService`; session state → Session scope; constants → code. Business domains never call `IBootstrapService.getEnv()` directly. (config.md)

View file

@ -0,0 +1,235 @@
# Subskill — Align (port `agent-core``agent-core-v2`)
Port business logic from `packages/agent-core` (v1) into `packages/agent-core-v2` (v2) by **splitting semantics, then fixing the domain, scope, Service, and dependency relationships**, and finally migrating the logic and tests.
Use this when the task is "move feature X from v1 to v2", "port `IXxxService` to v2", or "align a v1 domain with the v2 architecture". It complements the stage files: orient / design / implement / test explain the *target* architecture; this file explains how to get there *from v1*.
## The one-paragraph mental model
v1 is a **VSCode-style singleton container**: services self-register with `registerSingleton`, resolve as singleton-per-container, and have no explicit lifetime tier — so a single `ISessionService` / `IToolService` tends to accumulate global, per-session, and per-agent state in one class. v2 is a **DI × Scope tree**: every service binds to one of `App` / `Session` / `Agent`, and a domain with state at several lifetimes is split into several Services. Porting is therefore **not** a file copy — it is "find each lifetime of state hiding in the v1 class, give each its own v2 Service at the right scope, then re-wire the dependencies".
## v1 → v2 at a glance
| Concern | v1 (`agent-core`) | v2 (`agent-core-v2`) |
|---|---|---|
| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, ScopeActivation.OnDemand, 'domain')` |
| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/lifecycle'` |
| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Workspace/Session/Agent) — see orient.md |
| Domain granularity | coarse (`session`, `tool`, `loop`) | fine, split by scope + responsibility |
| Test import | `from '@moonshot-ai/agent-core/di/test'` | `from '#/_base/di/test'` |
| Resolve SUT in tests | `ix.createInstance(Impl)` (common) | `ix.get(IX)` by interface — see test.md |
| Scope tests | none | `createScopedTestHost` — see test.md |
| Errors | `from '../../errors'` (central `KimiError`, `ErrorCodes`) | `from '#/_base/errors'` + domain co-located `XxxError` — see errors.md |
| Flags | `flags/` (process-global `FlagResolver`) | `flag/` (App-scope `IFlagService`) — see flags.md |
| Permission | `agent/permission/` (hardcoded chain) | `permission*` (registry + composer) — see permission.md |
## The align workflow
```text
Read v1 → Semantic split → Map domain → Assign scope → Shape Services
→ Direct dependencies → Port logic → Port tests → Verify
```
Each step below states the goal and the concrete action, then points to the stage file that goes deeper. Do them in order; a later step often sends you back to an earlier one (a scope that does not fit means the semantic split was wrong).
### 1. Read v1
**Goal:** build an accurate inventory of what the v1 code actually owns. Read the v1 *source*, not v1 docs.
Actions:
- Locate the v1 entry: contract (`<domain>/<domain>.ts`) + impl (`<domain>/<domain>Service.ts`), plus any helpers under the same folder.
- Inventory three things from the impl:
- **State** — every field / `Map` / cache the class holds. For each, note its *identity* (global? keyed by `sessionId`? by `agentId`?).
- **Behavior** — every public method; group them by which state they touch.
- **Dependencies** — every `@IFoo` constructor injection and every cross-domain relative import (`from '../<other>/...'`).
- Note the v1 registration line (`registerSingleton(...)`) and any `services.set(IX, ...)` overrides at bootstrap (these reveal runtime static args or prebuilt instances the port must preserve).
Do not start splitting yet — an accurate inventory prevents the common mistake of porting the class shape instead of the semantics.
### 2. Semantic split
**Goal:** break one v1 class into independent semantic units, each owning state at exactly one lifetime. This is the heart of the port.
Method — for each piece of state from the inventory, ask:
1. **What is it keyed by?** nothing → a global unit; `sessionId` → a per-session unit; `agentId` → a per-agent unit.
2. **When should it die?** with the process / the session / the agent. State that must outlive its neighbors is a different unit.
3. **Which methods touch only this state?** they travel with the unit.
Worked example — v1 `ISessionService` (one class, ~600 lines) holds:
- a global index of all sessions → **global** unit → v2 `sessionStore` (`ISessionStore`, App);
- this session's metadata → **per-session** unit → v2 `sessionMetaStore` (`ISessionMetaStore`, Session);
- this session's activity / status → **per-session** unit → v2 `sessionActivity`;
- this session's context projection → **per-session** unit → v2 `sessionContext`;
- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **per-workspace** unit → v2 `sessionLifecycle` (Workspace, one per live workspace handler).
A v1 class that maps cleanly to one v1 decorator often becomes **three to five** v2 Services. That is expected and correct — do not try to keep the v1 class shape.
Red lines:
- If two pieces of state have different identities, they belong in different units — do not keep them together "because v1 did".
- Do not split by method count or file aesthetics; split by state identity (design.md §3).
- If a unit has no mutable state (pure behavior), defer its scope decision to step 4 (it is pulled down by its shortest-lived dependency).
### 3. Map to v2 domain
**Goal:** assign each semantic unit to a v2 domain — an existing one if it fits, a new one only if none does.
Actions:
- Search v2 `src/` for an existing domain that owns the same responsibility. Prefer joining an existing domain over creating a new one.
- If creating a domain, name it after the responsibility (camelCase folder, e.g. `sessionActivity`), not after the v1 file.
- Keep a domain's public surface to one contract file (`<domain>.ts`) plus its impl(s).
Reference mapping (a **starting point**, not gospel — verify against the current v2 `src/`, which is the source of truth):
| v1 location | v2 domain(s) |
|---|---|
| `services/session/`, `session/` | `session`, `sessionStore`, `sessionMetaStore`, `sessionActivity`, `sessionContext`, `agentLifecycle` |
| `services/tool/`, `tools/`, `agent/tool/` | `toolRegistry`, `toolStore`, `toolExecutor`, `tooldedup`, `userTool` |
| `loop/`, `agent/` (turn loop) | `loop`, `llmRequester`, `llmRequestLog`, `turn` |
| `agent/context/`, `agent/compaction/` | `contextMemory`, `contextProjector`, `contextSize`, `fullCompaction`, `dynamicInjector` |
| `agent/permission/` | `permission`, `permissionMode`, `permissionPolicy`, `permissionRules`, `approval`, `externalHooks` |
| `agent/goal/`, `agent/plan/`, `agent/swarm/`, `agent/cron/`, `agent/background/` | `goal`, `plan`, `swarm`, `cron`, `background`, `subagentHost` |
| `services/config/`, `agent/config/` | `config` |
| `services/event/`, `base/common/event` | `event`, `eventBus` |
| `services/logger/`, `logging/` | `log` |
| `services/fileStore/` | `filestore`, `blobStore` |
| `services/fs/`, `services/workspace/` | `fs`, `workspace` |
| `services/auth/`, `services/oauth/` | `auth` |
| `services/environment/` | `environment` |
| `services/terminal/` | `terminal` |
| `services/question/`, `services/approval/` | `question`, `approval` |
| `services/prompt/`, `agent/injection/` | `prompt`, `dynamicInjector` |
| `services/mcp/`, `mcp/` | `mcp` |
| `plugin/`, `profile/`, `skill/` | `plugin`, `profile`, `skill` |
| `rpc/`, `services/coreProcess/` | `rpc`, `gateway` |
| `di/` | `_base/di` |
| `errors/`, `errors.ts` | `_base/errors` + co-located domain errors |
| `flags/` | `flag` |
| `telemetry.ts` | `telemetry` |
| `agent/records/` | (records split) — verify in v2 `src/` |
When the table says "verify", or when v1 and v2 have diverged, **read the v2 `src/` tree and decide from the code** — do not invent a mapping.
### 4. Assign scope
For each semantic unit, fix its `LifecycleScope` from the identity you found in step 2. Follow design.md §2 verbatim:
- global → `App`; per `sessionId``Session`; per `agentId``Agent`.
- Stateless unit → default to `App`, pulled down only by a shorter-lived dependency.
- Self-check: "when this scope is disposed, should this state disappear with it?"
This is the decision v1 never had to make — get it right before writing any v2 code, because the scope is fixed at registration and changing it later ripples through every consumer.
### 5. Shape Services
Decide the Service shape per unit, following design.md §3:
- A unit that owns **one instance's** state → a single per-instance Service (`ISessionXxx` / `IAgentXxx`).
- A unit that owns a **global view plus per-instance** state → split into an `App` registry/factory (`XxxStore` / `XxxRegistry` / `XxxCatalog`) **and** a per-instance Service. The `App` half creates or locates the per-instance half.
- Do not pre-split a unit that has state at only one lifetime.
Most consumers inject the per-instance Service; inject the `App` factory only for genuine cross-instance management.
### 6. Direct dependencies
Re-wire the dependencies you inventoried in step 1, now across the new v2 Services. Follow design.md §4§5:
- **Calling style** — need a result / I orchestrate → direct call (`@IX` injection); stating a fact → event; ordered participation that may veto → hook.
- **Scope direction** — a Service may inject only its own scope or an ancestor. If an `App` Service needs something from a `Session` Service, the dependency is backwards: re-scope or invert into an event.
- **Domain direction** — foundational layers must not know upstream ones. A cycle means a v1 relative import is now pointing the wrong way; extract a third Service or invert the notification into an event.
- **Durable facts** — state changes that must be recorded / replayed / projected across agents go on the wire (`wireRecord`), not a direct call alone.
Run `lint:imports` (verify.md) as soon as the dependencies compile — it catches v1 imports and kosong boundary violations early.
### 7. Port the business logic
Move the behavior into the shaped v2 Services, applying the mechanical conversions below. Follow implement.md for the recipe.
**Registration:**
```ts
// v1
import { InstantiationType, registerSingleton } from '../../di';
registerSingleton(IXxxService, XxxService, InstantiationType.Delayed);
// v2
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
registerScopedService(LifecycleScope.Session, IXxxService, XxxService, ScopeActivation.OnDemand, 'xxx');
```
**Imports:**
```ts
// v1
import { createDecorator, Disposable, IInstantiationService } from '../../di';
import { KimiError, ErrorCodes } from '../../errors';
// v2
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { Disposable } from '#/_base/di/lifecycle';
import { IInstantiationService } from '#/_base/di/instantiation';
import { KimiError, type ErrorCode } from '#/_base/errors';
```
**Constructor injection** — unchanged in shape (`@IX` on constructor params, service params after static params). Verify each dependency is resolvable from the new scope (step 6).
**Errors** — move any shared error into a co-located `XxxError extends KimiError` with a registered `code` (errors.md). Do not keep throwing v1's central error codes from a v2 domain.
**Flags** — replace any `FlagResolver` / env check with `IFlagService.enabled(id)`; contribute new flags from the owning domain's `flag.ts` via `registerFlagDefinition` (flags.md).
**Events** — v1's `Emitter` / `Event` from `base/common/event` maps to v2's `event` / `eventBus` domains. Read existing v2 usage in neighboring domains and match it; do not import v1's `Emitter`.
**Runtime static args / prebuilt instances** — if v1 bootstrap did `services.set(IX, new SyncDescriptor(C, [bag]))` or set a prebuilt instance, preserve that behavior at the v2 composition root (the scope that owns the Service). Do not silently drop it.
Red lines:
- Do not copy a v1 file and "fix imports". Re-split first (steps 26); a straight copy carries v1's implicit-singleton assumptions into v2 and creates the `Map<sessionId, …>`-at-`App` anti-pattern.
- Do not leave v1 relative imports (`from '../x/...'`) in v2 — use the `#/...` alias.
- Do not preserve a v1 behavior just because it exists; if the split reveals it was a workaround for the missing scope tree, drop it.
### 8. Port the tests
Convert v1 tests to the v2 harness, following test.md:
```ts
// v1
import { TestInstantiationService } from '@moonshot-ai/agent-core/di/test';
const svc = ix.createInstance(XxxService, 'static-arg');
// v2
import { createServices } from '#/_base/di/test';
// in additionalServices:
reg.define(IXxxService, XxxService);
// in the test body:
const svc = ix.get(IXxxService);
```
- Resolve the SUT by interface (`ix.get(IX)`), never `new` a `@IService`-carrying impl, and prefer `ix.get(IX)` over `ix.createInstance(Impl)`.
- Move shared stubs into `test/<domain>/stubs.ts`; import by relative path, never `#/...`.
- If the port introduced scope-layer behavior, add a `createScopedTestHost` test that asserts resolution from the correct scope (with `_clearScopedRegistryForTests()` + explicit re-registration in `beforeEach`).
- Keep v1's behavioral assertions where they still describe observable behavior; delete assertions that only checked v1's internal class shape.
## Migration checklist
Before submitting a port:
- [ ] Every piece of v1 state landed in a v2 Service whose scope matches its identity (no `Map<sessionId, …>` at `App`).
- [ ] Each v1 dependency now points in the right scope direction; `lint:imports` passes.
- [ ] Registrations use `registerScopedService` with an explicit scope and domain name; no `registerSingleton` remains.
- [ ] Imports use the `#/...` alias; no v1 relative (`../../di`, `../../errors`) imports remain.
- [ ] Errors are co-located coded errors; flags go through `IFlagService`.
- [ ] Tests resolve the SUT by interface; scope behavior is asserted via `createScopedTestHost`; teardown goes through one `DisposableStore`.
- [ ] v1 bootstrap overrides (`services.set(...)`) are preserved at the v2 composition root.
## Red lines (this subskill)
- Porting is semantic splitting, not file copying — never preserve a v1 class shape in v2.
- Decide scope from state identity before writing v2 code; the scope is fixed at registration.
- Verify the domain mapping against current v2 `src/`; the table here is a starting point, not authority.
- One Service owns state at exactly one lifetime; split global-view + per-instance into registry + per-instance.
- A dependency cycle introduced by the port means a v1 import is now backwards — refactor it; activation timing cannot break the cycle.

View file

@ -0,0 +1,155 @@
# Topic — Close vs Dispose
How to shut down a scoped service in `agent-core-v2`: when `dispose()` is enough, when to add an async `close()`, and where cancellation / abort belongs. Read this before putting business shutdown logic into a `Disposable`.
## The one-sentence rule
> **`close()` is async business shutdown; `dispose()` is synchronous resource cleanup.**
`close()` finishes a domain's work: stop in-flight operations, apply shutdown policy, flush persistence, release async resources. `dispose()` releases object resources: event subscriptions, timers, hook registrations, and child disposables.
## Why they must stay separate
`IDisposable.dispose()` is synchronous:
```ts
export interface IDisposable {
dispose(): void;
}
```
The container calls it during scope teardown. Disposal order is deterministic (orient.md): child scopes first, then reverse construction order within a scope. Nothing awaits a Promise returned from `dispose()`.
Business shutdown is usually async. It may need to:
- stop in-flight tasks and wait for settlement;
- decide policy (`kill` vs `keepAliveOnExit` vs `markLost`);
- flush write queues and persistence;
- emit final records / events / telemetry;
- close sockets, child processes, or external clients.
If that logic lives in `dispose()`, it becomes fire-and-forget: the scope keeps tearing down, dependencies may be disposed immediately afterward, and the async continuation can run against a half-dead object graph.
## What `close()` owns
Add `close(): Promise<void>` when a service owns async shutdown work:
```ts
export interface IXxxService {
readonly _serviceBrand: undefined;
close(reason?: string): Promise<void>;
}
```
A good `close()`:
- is idempotent — repeated calls return the same Promise or no-op;
- is called by lifecycle code **before** `scope.dispose()`;
- rejects new work after it starts;
- applies shutdown policy explicitly;
- awaits the work it starts;
- leaves `dispose()` with only synchronous cleanup.
Sketch:
```ts
class XxxService extends Disposable implements IXxxService {
declare readonly _serviceBrand: undefined;
private closed = false;
async close(reason = 'scope closed'): Promise<void> {
if (this.closed) return;
this.closed = true;
await this.stopInFlightWork(reason);
await this.flushPersistence();
}
override dispose(): void {
this.closed = true;
// synchronous cleanup only: clear timers, remove listeners, release handles.
super.dispose();
}
}
```
`flush()` is different from `close()`: `flush()` persists buffered state while the service stays open; `close()` is terminal.
## What `dispose()` owns
`dispose()` releases resources owned by the object instance:
```ts
class WSBroadcastService extends Disposable implements IWSBroadcastService {
declare readonly _serviceBrand: undefined;
constructor(@IEventService event: IEventService) {
super();
this._register(event.subscribe(() => { /* … */ }));
}
}
```
Use `dispose()` to:
- `_register(...)` event subscriptions and hook registrations;
- clear timers;
- remove signal listeners;
- dispose child `IDisposable`s;
- detach from synchronous handles.
`dispose()` must be idempotent and should avoid throwing. If `close()` was already called, `dispose()` should be a no-op for business work and only clean resources.
## Where abort / cancellation belongs
Cancellation is not the same thing as graceful shutdown.
For an operation-scoped object, a cancellation trigger can be disposed:
```ts
const tokenSource = new CancellationTokenSource();
store.add(toDisposable(() => tokenSource.cancel()));
```
This is fine when the contract is **fire-and-forget cancel**: the operation observes the token and settles asynchronously; disposal does not wait for completion.
For a manager/service that owns many tasks and their state, do not use `dispose()` as the graceful abort path. Expose `stop()` / `stopAll()` / `close()` and let lifecycle code await the one it needs.
Background-specific rule: a `background`-style service may use `AbortController` internally to propagate cancellation to process / agent / question tasks, but manager shutdown belongs in `close()` or explicit `stopAll()`. `dispose()` may best-effort abort controllers only as a safety net; it must not be the mechanism that decides terminal status, persistence, or notifications.
## Decision tree
```text
What does the service own?
├─ only event subscriptions / timers / disposable handles?
│ └─ extend Disposable; no close() needed.
├─ async work, in-flight tasks, persistence buffers, sockets, child processes?
│ └─ add close(): Promise<void>; call it before scope.dispose().
├─ a single operation that callers may cancel?
│ └─ expose an AbortSignal / CancellationToken or a fire-and-forget cancel handle.
└─ both async shutdown and disposable resources?
└─ close() for business shutdown; dispose() for resource cleanup.
```
## VSCode parallel
VSCode uses the same split:
- `src/vs/base/common/lifecycle.ts``IDisposable.dispose(): void` for synchronous cleanup.
- `src/vs/base/parts/storage/common/storage.ts``close(): Promise<void>` flushes and closes the database.
- `src/vs/base/common/cancellation.ts``CancellationTokenSource.dispose(true)` / `cancelOnDispose()` cancels operation-scoped work without awaiting it.
The lesson is not "never cancel in dispose". It is: **disposal may trigger cancellation for a scoped operation, but service shutdown policy stays in an explicit async close path.**
## Red lines (this topic)
- Do not put business shutdown in `dispose()``dispose()` is synchronous and is not awaited.
- Do not `await` inside `dispose()`.
- Do not rely on `dispose()` to flush persistence, emit final events, wait for tasks, or send notifications.
- Add `close(): Promise<void>` for async shutdown and call it before `scope.dispose()`.
- Keep `close()` and `dispose()` idempotent; `dispose()` after `close()` must be safe.
- Use disposal as a cancellation trigger only for operation-scoped work, not as a manager/service shutdown policy.

View file

@ -0,0 +1,78 @@
# Subskill — Commit align (triage a `main` commit against v2)
Context: you are on the `kimi-code-v2` branch, in the phase of catching it up to **new commits that landed on `main`**. Those commits change `packages/agent-core` (v1); the job is to decide, for one commit at a time, whether v2 (`packages/agent-core-v2`) already has the corresponding logic — and if not, what the minimal fix is.
Use this when the user hands you **one commit hash plus a short description** ("look at `<commit>` — it fixed the steering race"). It is the small, per-commit sibling of [align.md](align.md): `align.md` ports a whole v1 domain into v2; this file triages a single `main` commit and says *port / adapt / skip*. If the triage reveals a whole missing domain, stop and switch to [align.md](align.md).
## The one-paragraph mental model
A `main` commit edits v1's singleton-container code. The same behavior in v2 lives behind a scoped Service, so a commit lands in one of four buckets: **already-aligned** (v2 has it, possibly by construction), **partial** (v2 has a nearby version whose semantics drift), **missing** (v2 has nothing), or **not-applicable** (the v2 architecture removed the very problem the commit fixes). Your output is a bucket assignment plus evidence, then a fix sized to that bucket — never a blind port of the diff.
## The workflow
```text
Read the commit + the user's note → Locate the v1 logic → Map to a v2 domain
→ Check v2 for a corresponding implementation → Bucket it → Recommend a fix → Verify
```
### 1. Read the commit and the note
**Goal:** know exactly what changed in v1 and *why*. The user's one-liner gives the intent; the diff gives the facts.
Actions:
- Inspect the change scoped to v1: `git show <commit> -- packages/agent-core` (and `--stat` first to see the blast radius).
- From the diff, list: touched files, changed functions/methods, and the observable behavior delta (before → after).
- Reconcile with the user's note: is this a bugfix, a semantic correction, new behavior, or a refactor? The *why* decides whether v2 even needs the change.
Do not skim the user's sentence and guess — the diff is the spec for what "aligned" means here.
### 2. Locate the v1 logic
Pin the change to a v1 place: the contract (`<domain>/<domain>.ts`) + impl (`<domain>/<domain>Service.ts`), or the helper/handler the commit touched. Note which state it reads/writes and which other v1 services it calls — this is the same inventory as [align.md](align.md) §1, scoped to the commit's footprint.
### 3. Map to a v2 domain
Use the v1 → v2 domain table in [align.md](align.md) §3 as a starting point, then **verify against the current `packages/agent-core-v2/src/` tree** — it is the source of truth. Identify the candidate v2 Service(s) that would own this behavior, and their `LifecycleScope`.
### 4. Check v2 and assign a bucket
Search the candidate domain in v2 (Grep the method name, the state field, the error code). For each piece of the commit's behavior delta, decide:
- **Already-aligned** — v2 produces the same observable result (sometimes for free, because the v2 design never had the bug). Cite the v2 file:line.
- **Partial** — v2 has a near miss: same method, different guard/ordering/error; or the state lives at a different scope. Name the exact drift.
- **Missing** — no v2 Service owns this behavior. Confirm it is a single-Service gap, not a whole-domain gap (latter → [align.md](align.md)).
- **Not-applicable** — the v2 architecture removed the condition the commit fixes (e.g. the scope tree already serializes what v1 patched with a lock). Explain why, so a reviewer trusts the skip.
Every claim needs a citation (`path:line`) on both sides; "I couldn't find it" is a finding only after you name where you looked.
### 5. Recommend a fix (sized to the bucket)
- **Already-aligned** — say so and stop; reference the v2 location. No code change.
- **Partial** — propose the smallest edit that closes the drift: which Service, which method, which guard. Stay inside v2 rules — scope/domain direction, no `Map<sessionId, …>` at `App` (see [align.md](align.md) §6§7 red lines).
- **Missing** — sketch the port at commit granularity: target domain + scope, the Service/method to add or extend, the dependency direction, and which [align.md](align.md) §7 conversions apply (registration, `#/…` imports, co-located coded error, `IFlagService` for any gate). If it needs a new scope or a wire change, flag it.
- **Not-applicable** — recommend no v2 change, but call out any test worth adding so the gap stays closed.
Keep the recommendation to the commit's footprint. If it keeps growing, that is the signal to hand off to [align.md](align.md) for a full domain port.
### 6. Verify
Point at the checks that cover the fix, per [verify.md](verify.md): `lint:imports`, `typecheck`, and the relevant `test`. Note the expected outcome rather than asserting you ran it if you did not.
## Output shape
When triaging, answer in this order so the user can act on it directly:
1. **Commit + intent** — one line restating what the commit changed and why (from the note + diff).
2. **v1 location** — file(s) and the behavior delta.
3. **v2 status** — one of the four buckets, with `path:line` evidence on both sides.
4. **Recommendation** — the concrete fix (or the justified skip), scoped to the commit; name the target Service / scope / dependency direction.
5. **Verify** — which checks should pass, and whether to escalate to [align.md](align.md).
## Red lines (this subskill)
- Read the diff and the note before judging v2; never infer "aligned" from the description alone.
- Do not copy a v1 diff into v2. Decide the bucket first; a bugfix commit often maps to **not-applicable** because the v2 design already removed the defect.
- Cite `path:line` on both sides. A recommendation without evidence is a guess.
- Stay in the commit's footprint. Growing scope means "switch to [align.md](align.md)", not "keep porting here".
- Do not break v2 invariants to chase v1 parity — scope direction, domain direction, and no `Map<sessionId, …>` at `App` still hold ([align.md](align.md) red lines).

View file

@ -0,0 +1,312 @@
# Topic — Config
How the `config` domain works and how a domain owns its configuration section. Covers the section-registry model, the App vs Session split, the TOML on-disk format, and the recipe for adding or migrating a config section.
The `config` domain is a thin registry + loader: it does **not** know the shape of any individual section. Each domain owns the schema (and, where needed, the TOML transform) for the config it consumes, contributes the section (statically at module load via `registerConfigSection`, or at runtime as a `ConfigSectionContribution` collection record), and reads it through `IConfigService`. There is no whole-config object passed around.
## What belongs in Config
`IConfigService` is the **preference registry**: it holds values a user or
operator *chooses*, each with a schema and a default, that *can* be persisted to
`config.toml`. It is not a grab-bag for every value a domain needs. Before
registering a section, classify the value along three axes — **decision-maker**,
**preference vs fact**, **mutability / persistence**:
| Type | Decision-maker | Preference/Fact | Persisted? | Examples | Home |
|---|---|---|---|---|---|
| User preference | user | preference | ✅ config.toml | model, theme, log level | **Config** |
| Operational override | operator/deployer | preference | ❌ env / flag | `KIMI_MODEL_*`, `KIMI_LOG_*` | **Config** (env overlay) |
| Per-run intent | invoker | preference | ❌ ephemeral | CLI `--model`, `--config` | **Config** (Memory layer) |
| Host fact | host | fact | ❌ | platform, CI, proxy, home dir | **Bootstrap** |
| Derived convention | code | fact (derived) | ❌ | `configPath`, `logsDir` | **Bootstrap / code** |
| Session runtime state | session/agent | state | ✅ session meta | active model, plan mode | **Session scope** |
| Tuning constant | developer | preference | ❌ compile-time | retry backoffs, buffer sizes | **code** |
A value belongs in Config **iff** it satisfies all of:
1. **Preference** — a choice among valid values, not an observed fact.
2. **Persistable** — it *can* be written to `config.toml`, even when a given
value arrives via env or CLI.
3. **Schema + default** — registerable as a section with validation.
4. **User- or operator-facing** — meaningful to set as a preference.
If it fails any rule, it is not Config:
- **Fact** (CI, platform, proxy, `HOME`) → a structured fact on
`IBootstrapService` (the startup snapshot), not Config.
- **Derived convention** (`configPath`, `logsDir`) → `IBootstrapService` / code.
- **Session runtime state** (active model, plan mode) → a Session-scoped
service in the owning domain (e.g. `IProfileService`), not `config`.
- **Tuning constant** (retry config, buffer sizes) → domain code; promote to
Config only when it becomes user-tunable.
**`IBootstrapService` is domain-agnostic.** It holds only generic facts shared by
all domains — the env bag, resolved paths, and host facts (`platform`, `arch`,
`cwd`, `osHomeDir`, `isCI`, …) — plus the host's process-level invocation
arguments in `args` (explicit `agentFiles` / `skillDirs`, `requestHeaders`,
prompt identity). `args` mirrors VS Code's `NativeParsedArgs` on the
environment service: the host states them once via `BootstrapInput.args` at
the composition root, and downstream services read them from
`IBootstrapService.args` instead of through per-domain runtime-options
services (do not add new `IXxxRuntimeOptions` services or seed functions for
host parameters). What must **never** land on `IBootstrapService` is state
tied to a specific upper domain (no `cron`, no `flags`, no feature-specific
fields): that couples the foundational layer to an upstream one.
Any value that belongs to a specific domain — including env-only operational
toggles (`KIMI_CRON_*`, `KIMI_CODE_EXPERIMENTAL_*`), model parameters, or feature
flags — goes through **Config registration**: the owning domain registers a
section with a declarative `envBindings` map (and a `stripEnv` when the value must
not be persisted) and reads it via `config.get(...)`. Each config value declares
an optional env binding (`{ field: 'ENV_VAR' }`, with optional `parse`/`default`);
IConfig resolves each field by `env > config.toml > default` automatically. This
keeps every domain's config in one registry and keeps Bootstrap free of upstream
knowledge.
Operational env overrides and per-run intent live *inside* Config as layers over
the same persistable key: `model` can be set in `config.toml`, via `KIMI_MODEL_*`,
or via CLI `--model`. They are not separate abstractions — see "Reads vs writes"
and "Layered resolution" below.
Env access is encapsulated: business domains read `config.get(...)` or structured
`IBootstrapService` facts; only the `config` domain reads the raw env bag (from
`IBootstrapService`) to build its overlays. Business domains must not call
`IBootstrapService.getEnv()` directly.
## Layered resolution
`IConfigService` resolves a key by precedence across layers, lowest to highest:
```text
Default registered defaultValue (and code constants promoted to a section)
User config.toml (persisted user preferences)
Operational env overlay (e.g. KIMI_MODEL_*, KIMI_CODE_EXPERIMENTAL_*)
Memory per-run intent (CLI flags); never persisted; highest
```
`set(domain, patch, target?)` writes the `User` layer (persisted) by default;
pass `ConfigTarget.Memory` for a per-run override that is never written to disk.
`inspect(domain)` reports the value at each layer.
## Layout
- `src/app/config/config.ts``IConfigRegistry` / `IConfigService` tokens, `ConfigSection`, `ConfigEffectiveOverlay`, event types.
- `src/app/config/configService.ts``ConfigRegistry` + `ConfigService` impl; self-registers at App scope. The registry is also the fold of the `ConfigSectionContribution` collection: it drains the module-level contributions at construction, then refolds incrementally (`added``registerSection`, `removed``unregisterSection`).
- `src/app/config/configSectionContributions.ts` — the `ConfigSectionContribution` collection token (the runtime channel: a unit contributes with `this.provide(ConfigSectionContribution, …)`) plus the module-level `registerConfigSection` collector (the static channel, import = register).
- `src/app/config/configOverlayContributions.ts` — the module-level `registerConfigOverlay` collector for `ConfigEffectiveOverlay`s (drained at construction like the sections).
- `src/app/config/toml.ts` — generic snake_case ↔ camelCase machinery plus the registry-aware `transformTomlData` / `applySectionToToml` entry points. Per-domain normalization lives in the section owner's `configSection.ts` (registered as `fromToml` / `toToml`); this module stays free of any other domain's semantics.
- `src/kosong/model/thinking.ts` (owner domain, not `config`) — the `resolveThinkingEffort` helper and the authoritative `ThinkingConfig` type (the `thinking` section itself registers from `src/app/kosongConfig/configSection.ts`).
- `src/app/config/configPure.ts``isPlainObject`, `deepMerge`, `omitUndefined`, `describeUnknownError`.
A domain that owns a section keeps the schema in its own `configSection.ts` (e.g. `src/app/flag/flag.ts` for `experimental`, `src/agent/loop/configSection.ts` for `loopControl`). Exception: kosong-owned sections (`providers`, `models`, `thinking`) — kosong is a pure, persistence-free abstraction layer that defines only the types (`src/kosong/{provider,model}`); the section constants, the zod schemas (re-derived from those types and compile-time pinned via `AssertExact<Equal<z.infer<typeof Schema>, Type>>`, see `_base/utils/typeEquality.ts`), the registrations, env bindings, and TOML transforms all live in the persistence wrapper `src/app/kosongConfig/configSection.ts`. (`modelCatalog` has no kosong-side type at all — its section is fully self-contained in `app/kosongConfig`, types derived from the schema.) A cross-section env overlay (e.g. the `KIMI_MODEL_*` synthesis in `src/app/kosongConfig/envOverlay.ts`) lives in the wrapper too and is registered via module-level `registerConfigOverlay`. The session subagent domain owns two sections in `src/session/subagent/configSection.ts`: `[subagent]` (`timeout_ms` on disk) and `[secondary_model]` (`default_model` plus the `[secondary_model.models]` pool, with a lone legacy v1 `model` key honored as a fallback default below `default_model`); neither carries a cross-section overlay. Cross-field pool validation (default present / in-pool / every key resolvable) runs at session creation in `subagentModelsValidationService.ts`, not in the schema. The two-way sync between config sections and kosong's in-memory registries is owned by `IKosongConfigService` (`src/app/kosongConfig/kosongConfigService.ts`).
## Scope
- `IConfigRegistry` / `IConfigService`**App** scope, process-global. One registry of sections; one loader reading `~/.kimi-code/config.toml` (path from `IBootstrapService.configPath`).
All config reads go through `IConfigService` (global config). Per-session runtime state (active model, thinking level, etc.) lives in the owning Session-scoped service (e.g. `IProfileService`), not in `config`.
## The section-registry model
A config section is identified by a camelCase domain key (`'providers'`, `'thinking'`, `'loopControl'`). Each section has:
- `schema?: ConfigSchema<T>` — zod schema used to validate the value (absent ⇒ passthrough).
- `defaultValue?: T` — filled when the file has no value for the domain.
- `merge?: ConfigMerge<T>` — how `set(domain, patch)` combines base + patch (default `deepMerge`).
- `fromToml?: ConfigFromToml` — read-path transform (snake_case file value → in-memory shape). Defaults to a plain key-casing pass; owners register one when the on-disk shape needs custom normalization (record key preservation, nested object conversion, array entries, key renames, reshapes).
- `toToml?: ConfigToToml` — write-path transform (in-memory value → snake_case file value). Defaults to a plain camelCase→snake_case key mapping.
Two contribution channels:
- **Static (import = register)** — the owning domain calls `registerConfigSection(domain, schema, options)` at the top level of its `configSection.ts`; `ConfigRegistry` drains the collected contributions when it is constructed. Every in-repo section uses this channel.
- **Runtime (collection record)** — a unit contributes `this.provide(ConfigSectionContribution, { domain, schema, options })` (e.g. a feature assembled through `IFeatureManager`); the `ConfigRegistry` fold registers the section when the record lands and unregisters it when the record is withdrawn (provider disposed). User TOML values survive a withdrawal — they just stop being validated and effective.
Ownership rules:
- **One owner per section.** `registerSection` throws if a domain is registered twice — the static channel fails fast when `ConfigRegistry` drains it; a conflicting runtime record is reported through `onUnexpectedError` and the first registration wins (the fold is an event path and never throws).
- **The domain that consumes a config owns its schema.** This is what keeps `config` from depending on its consumers: `config` must not import `externalHooks` / `permissionRules` / `provider` / `kosong` / etc. for a section's schema. If a schema needs a domain's types, the schema lives in that domain.
- **Demand-driven.** Do not register sections for config that no domain reads yet; a section appears (with its schema in the owning domain) only when a consumer appears.
## Env bindings
A section can declare how its fields are read from environment variables, so the
value resolves through `config.get(...)` rather than ad-hoc `process.env` reads.
Declare the bindings with `envBindings(schema, { … })` — the field names are
type-checked against the schema (no magic strings), and nested schemas recurse:
```ts
registerConfigSection('thinking', ThinkingConfigSchema, {
env: envBindings(ThinkingConfigSchema, {
effort: 'KIMI_MODEL_THINKING_EFFORT',
}),
});
// nested / record section — outer key is a runtime constant, inner fields are
// checked against the value schema:
registerConfigSection('providers', ProvidersSectionSchema, {
env: envBindings(ProvidersSectionSchema, {
[ENV_MODEL_PROVIDER_KEY]: envBindings(ProviderConfigSchema, {
apiKey: 'KIMI_MODEL_API_KEY',
type: 'KIMI_MODEL_PROVIDER_TYPE',
baseUrl:'KIMI_MODEL_BASE_URL',
}),
}),
stripEnv: stripProvidersEnv,
});
```
Each field is an `EnvBinding` — a string (env var name) or
`{ env, deprecatedEnv?, parse?, default? }`. IConfig resolves every field by
`env > config.toml > default`, sets it on the effective value, and validates the
section. Empty nested entries (no field resolved) are omitted, so a synthetic
entry like `__kimi_env__` only appears when at least one of its env vars is set.
When `deprecatedEnv` is set and `env` itself is absent or fails `parse`, the
deprecated var still supplies the value and a warning diagnostic is reported —
use it to rename an env var without breaking existing setups.
`stripEnv(value, raw?, getEnv?)` removes env-derived fields before `set`/`replace`
persists, so env overrides never leak into `config.toml`. `raw` is the section's
env-free camelCase base (already `fromToml`-normalized), and `getEnv` reads the
live env bag. For fields that are **both
user-persistable and env-overridable**, register
`stripEnv: stripEnvBoundFields(sectionEnvBindings)` (from `#/app/config/config`)
— it derives the guard from the same bindings the read path uses: while a
field's env var resolves to a value, writes restore the field's raw-base value
(or drop it) instead of persisting an echoed env value; an env value that
fails the binding's `parse` owns nothing, so writes pass through. Env-only
fields/sections need no env check — strip them unconditionally (e.g. thinking's
`forcedEffort`, cron's whole-section `() => undefined`).
Business domains read `config.get('section')`; they never read env directly, and
never write their own env-merge logic.
## Add a config section (recipe)
1. Define the schema in the owning domain, e.g. `src/<domain>/configSection.ts`:
```ts
export const MY_SECTION = 'mySection';
export const MySectionSchema = z.object({ /* ... */ });
export type MySection = z.infer<typeof MySectionSchema>;
```
2. Register it at the top level of the same module (import = register):
```ts
// src/<domain>/configSection.ts
import { registerConfigSection } from '#/app/config/configSectionContributions';
registerConfigSection(MY_SECTION, MySectionSchema, { defaultValue: {} });
```
`ConfigRegistry` drains module-level contributions when it is constructed, so the section exists before any consumer resolves `IConfigService` — no owning Service needs to be constructed first. Make sure `src/index.ts` imports the leaf so the top-level call runs.
3. (Runtime variant) a dynamically loaded unit (e.g. one assembled through `IFeatureManager`) contributes the section as a collection record instead:
```ts
this.provide(ConfigSectionContribution, { domain: MY_SECTION, schema: MySectionSchema, options: { defaultValue: {} } });
```
The `ConfigRegistry` fold registers it incrementally and unregisters it when the unit is retracted (user TOML values survive) — see "Late registration".
4. Read it anywhere via `IConfigService`:
```ts
constructor(@IConfigService private readonly config: IConfigService) {}
// ...
const value = this.config.get<MySection>(MY_SECTION);
```
5. React to edits by subscribing `IConfigService.onDidChange` and filtering on `e.domain === MY_SECTION` (see `FlagService`).
6. Write it only through `IConfigService.set(domain, patch)` (merge) or `.replace(domain, value)` (wholesale). Never write `config.toml` directly.
## Reads vs writes
Data flow is one-way by default — reading config never touches the file:
```text
config.toml ──load──▶ IConfigService.effective ──get──▶ services read
▲ │
└──────── IConfigService.set/replace ◀──── only on explicit writes
```
- **Read path** (startup, every service): `config.toml` is loaded into `IConfigService` once; services read via `get()`. This path **never writes the file**.
- **Write path** (rare): `config.toml` is rewritten only when something explicitly calls `IConfigService.set/replace`. The only production writers today are provider CRUD (`ProviderService.set/delete`, e.g. provisioning a provider after OAuth login).
**Runtime service state is not config.** Mutating a service at runtime does **not** rewrite `config.toml`:
- `ProfileService.configure(...)` / `update(...)` / `setModel(...)` / `setThinking(...)` only change **in-memory** fields and append to the session **wireRecord** (for replay). They never call `IConfigService.set`.
- Switching model or thinking level mid-session is session runtime state, not a config edit — the user's `config.toml` is left untouched.
So `configure(...)` never overwrites the local file. Treat `config.toml` as the user's static config; runtime overrides live in memory and the session record.
## Late registration
`ConfigService` loads in its constructor (first `get(IConfigService)`). Static sections are drained before that, but a runtime-contributed section (a `ConfigSectionContribution` record) can register at any later moment. To keep validation and defaults correct:
- `IConfigRegistry` emits `onDidRegisterSection` whenever a section is registered (and `onDidUnregisterSection` when a runtime record is withdrawn).
- `ConfigService` subscribes and, on registration, re-validates the already-loaded raw value for that domain, applies the default if the raw value is absent, re-runs the env overlay, and fires `onDidChange` if the effective value changed. On unregistration it devalidates the domain — `get(domain)` falls back to the raw value.
- Before a section is registered, `get(domain)` returns the raw (transformed, unvalidated) value; consumers that need validated values should read after the section lands, or react to `onDidChange`.
This means registration order is never a correctness concern — you do not need an eager bootstrap.
## TOML on-disk format
`config.toml` stores keys in **snake_case**; in-memory values are **camelCase**. `ConfigService` converts both ways by dispatching to each section's registered transform:
- **Read**: `transformTomlData(fileData, registry)` maps each top-level key to a domain and applies that domain's `fromToml` hook (or a plain key-casing pass when none is registered). Owner domains register their own normalization — e.g. provider `oauth`/`env`/`customHeaders`, permission `deny/allow/ask``rules`, `experimental` keys preserved verbatim. When a section registers after the initial load, `ConfigService` re-applies its `fromToml` against the preserved snake_case raw value (see "Late registration"), so registration order is never a correctness concern.
- **Write**: `applySectionToToml(rawSnake, domain, value, registry)` applies the domain's `toToml` hook (or a plain camelCase→snake_case mapping) into a raw clone of the file, preserving unknown top-level keys and unknown sub-fields (lossless round-trip).
`ConfigService` keeps four views:
- `rawSnake` — snake_case clone of the file; the write base, never carries the env overlay.
- `raw` — camelCase, env-free; the read/set/replace base.
- `validated` — validated `raw`, env-free; the base every live env re-application starts from, so a degraded or removed env value falls back to the file instead of a stale overlay.
- `effective``validated` plus the env overlay, recomputed on load/set; `get()`/`getAll()` re-apply the overlay on a fresh `validated` copy per read rather than caching it.
### Renaming config keys and env vars (deprecations)
Renames are declared once on the section, never hand-rolled in `fromToml`:
```ts
registerSection(MY_SECTION, MySectionSchema, {
deprecations: [{ key: 'old_key', replacement: 'new_key' }], // snake_case, on-disk
env: envBindings(MySectionSchema, {
newKey: { env: 'KIMI_NEW_KEY', deprecatedEnv: 'KIMI_OLD_KEY', parse },
}),
});
```
- A deprecated TOML key is **ignored** (its value no longer applies — the schema only knows the new key) and reports a warning `ConfigDiagnostic` while present; the file is never rewritten, so the warning is the migration guide. Diagnostics are recomputed on every load/reload and surface to clients via `IConfigService.diagnostics()` and `onDidChangeDiagnostics` (kap-server republishes them as the global `event.config.warning` WS event).
- A deprecated env var still **resolves** as a fallback (new var first), with the same warning treatment, and `stripEnvBoundFields` treats it as env-owned for writes.
- See `src/agent/loop/configSection.ts` for a worked example (`max_retries_per_step``max_attempts_per_step`).
### `KIMI_MODEL_*` env overlay
When `KIMI_MODEL_NAME` is set, the `kosongConfig` wrapper's `kimiModelEnvOverlay` (`src/app/kosongConfig/envOverlay.ts`) injects a reserved model alias (`__kimi_env_model__`) into `effective`, points `defaultModel` at it, and merges the request `modelOverrides`; the reserved provider (`__kimi_env__`) comes from the `providers` section env bindings. The overlay is registered via module-level `registerConfigOverlay` and applied **only to `effective`**, never to `rawSnake`, so it is never persisted. Its `strip` (plus the providers section `stripEnv`) is the final guard so a caller that read `effective` (with the overlay) cannot write the reserved entries or the shell API key back to disk. `config` itself only runs registered overlays — it does not know the `KIMI_MODEL_*` semantics.
## Owner-owned sections
`config` holds no monolithic config schema and no whole-config object. Every section is owned by the domain that consumes it: the schema (and any `fromToml` / `toToml` normalization and `stripEnv`) lives in that domain's `configSection.ts`, and the domain contributes it via module-level `registerConfigSection` (or a runtime `ConfigSectionContribution` record). Cross-section env behavior (e.g. `KIMI_MODEL_*`) lives in an owner-registered `ConfigEffectiveOverlay` (module-level `registerConfigOverlay`). To add a section, follow "Add a config section" above in the owning domain — never add schema or normalization to `config` itself.
## Ownership map (generated)
The authoritative, always-current list of registered sections — rendered in the on-disk `config.toml` shape, with owner file, scope, defaults, env bindings, and schema fields — is generated from the live registry:
- `packages/agent-core-v2/docs/config-manifest.toml` (checked in; do not edit by hand).
- Regenerate with `pnpm --filter @moonshot-ai/agent-core-v2 gen:config-manifest` (add `--check` for a freshness check; `test/app/config/configManifest.test.ts` enforces it in CI).
`config` must not import from any of these owner domains; that is the whole reason the schemas, TOML normalization, and env overlays live with their owners.
## Scope & dependencies
- `config` is a low-level capability: domains that own sections import `config` (for `IConfigRegistry` / `IConfigService`), never the reverse — section schemas live in the owning domain.
- Cross-domain type sharing for a config type: prefer importing the type from the owning domain over re-declaring it (e.g. `plugin` imports `McpServerConfig` from the MCP config schema).
- `IConfigRegistry` / `IConfigService` are **App**. Agent scope services may inject App services via ancestor lookup.
- `config` never imports a higher domain and holds no section schemas of its own; if a section needs a type from another domain, that schema lives in that domain.
## Red lines (this topic)
- One owner per section: a duplicate static registration throws when `ConfigRegistry` drains it; a conflicting runtime record is logged (`onUnexpectedError`) and the first registration wins.
- `config` never imports the domains that consume it — keep section schemas in the owning domain.
- Config is the **preference registry**: register only values that are preferences, persistable, schema'd, and user/operator-facing. Facts → `IBootstrapService`; session state → Session scope; constants → code.
- Business domains read `config.get(...)` or structured `IBootstrapService` facts; never call `IBootstrapService.getEnv()` directly — only `config` reads the raw env bag to build overlays.
- Keep `IBootstrapService` domain-agnostic: host invocation arguments (CLI flags, host identity headers, prompt identity) go into `BootstrapInput.args` / `IBootstrapService.args` — never into new per-domain runtime-options services; domain runtime state (cron, flags, model params, …) never goes onto `IBootstrapService` at all. Domain-specific config goes through `registerConfigSection` + `envBindings`, read via `config.get(...)`.
- Do not pass a whole config bag via options; read each section through `IConfigService`. There is no `KimiConfig` object — config is a registry of owner-owned sections.
- `config.toml` is snake_case on disk, camelCase in memory — never write camelCase keys to disk, and never write to `config.toml` except through `IConfigService.set/replace`.
- Reading config / calling `configure(...)` / switching model at runtime must not rewrite `config.toml`; runtime state lives in memory and the session wireRecord, not the file.
- Never persist env overlays (`__kimi_env__` / `__kimi_env_model__` / shell API key / experimental env); overlays live only in `effective` / `Memory`.
- Runtime contribution (a `ConfigSectionContribution` record from a unit at any scope) is fine — the late-registration mechanism keeps validation correct; the static channel needs no eager bootstrap (import = register, drained at `ConfigRegistry` construction).

View file

@ -0,0 +1,289 @@
# Stage 2 — Design a service
Decide *where things live and who knows whom* before writing code. Every rule here derives from two questions:
1. **What is the identity of the state it owns?** → decides the **Scope**.
2. **Who owns the decision, and who needs the result?** → decides the **calling style** and **dependency direction**.
## 1. What a Service is
A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetime**.
- **Behavior** is almost free — the same logic runs anywhere, so it does not by itself decide a scope.
- **State** pins a Service to a scope. State has an **identity** (what it is keyed by) and a **lifetime** (when it is born, when it dies).
- **Dependencies / calling style** answer a different question: who controls whom, and who knows whom.
## 2. Choosing a scope
> Scope = the identity + lifetime of the owned state.
| Scope | State identity (keyed by) | Lifetime |
|---|---|---|
| `App` | none (single global instance) | the process |
| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) |
| `Session` | `sessionId` | one session |
| `Agent` | `agentId` | one agent |
### Decision tree
**Q1. Does it own mutable state?**
- No (pure behavior) → jump to Q3.
- Yes → Q2.
**Q2. What is the identity of that state?**
- one global instance → **`App`**
- one per workspace (shared by every session of that workspace) → **`Workspace`**
- one per session → **`Session`**
- one per agent → **`Agent`**
- a mix (a global registry *and* per-instance state) → **split it** (see §3).
**Q3 (stateless). What is the shortest-lived dependency it must inject?**
A stateless Service is pulled *down* by its shortest-lived dependency: if it injects an `Agent`-scoped Service, it cannot be `App`. Among the scopes that still satisfy every dependency, **default to the longest-lived one** (usually `App`) to maximize reuse. Push it down only when it must inject a shorter-lived Service, or when you want to limit its visibility.
### The core anti-pattern (a litmus test)
> **Do not store per-session state in a `Map<sessionId, …>` inside an `App` Service.**
This is the tell-tale sign of "should have been `Session`-scoped but was parked at `App`". Consequences: nobody cleans the entry up when the session ends (leak); every consumer threads `sessionId` around (loss of type safety); it cannot inject `Session`/`Agent`-scoped collaborators.
### One-sentence self-check
> "When this scope is disposed, should this state disappear with it?"
>
> - Yes → the scope is right.
> - It must outlive the scope → too short; move up one tier.
> - It should be one-per-unit but is shared → too long; move down one tier.
## Scope is not a domain
Scope answers **lifetime and visibility**. Domain answers **responsibility and data ownership**. A Service registered at `Session` or `Agent` scope is not automatically part of the `session` or `agent` domain, and an entity Service must not be named `I{Scope}EntityService` just because its data is scoped that way.
Use the data-ownership test and the `session` / `agent` / `turn` split conclusions in [domain-boundaries.md](domain-boundaries.md) before naming a Service or adding `I{Domain}EntityService`.
## 3. Multi-Scope splitting
> One Service owns state at exactly one identity / lifetime. If a domain owns state at several lifetimes, split it along those boundaries — one Service per lifetime.
The standard split is "global registry / factory" + "per-instance":
| Tier | Role | Naming tends to |
|---|---|---|
| `App` | global registry / catalog / factory — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` |
| `Workspace` / `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` |
Canonical splits in the codebase:
- **`records`** — `ISessionStore` (`App`) + `ISessionMetaStore` (`Session`) + `IAgentRecords` (`Agent`).
- **`config`** — `IConfigRegistry` / `IConfigService` (`App`).
- **`kosong`** — `IProtocolHandlerRegistry` (`App`) + `IProviderManager` (`Session`). Generation is driven by `ILLMRequester` (`Agent`) in the `llmRequester` domain.
- **`tool`** — `IToolDefinitionRegistry` (`App`) + `IToolService` (`Agent`).
Split when the domain genuinely has both a global view and per-instance state. Do **not** split when state lives at only one lifetime (e.g. purely `App` like `log`; purely `Agent` like `prompt`). Do not pre-split for symmetry.
After the split, the `App` Service usually plays the **factory**; most consumers inject the **per-instance** Service. Inject the `App` factory only when you genuinely need cross-instance management.
## 4. Choosing a calling style
Three mechanisms answer three different questions:
| Mechanism | Nature | Coupling | Returns a value? | Consumers |
|---|---|---|---|---|
| **Direct call** | command: A tells B to do | A → B | yes | one (known) |
| **Event** | fact: A announces "X happened" | both depend only on the bus | no | zero / one / many (unknown) |
| **Hook** (`onWill` / `onDid`, `OrderedHookSlot`) | participation: observers step into an operation, in order | both depend only on the bus | can observe / veto | many, but ordered |
### Decision tree
**Q1. Does A need a return value from B?** → Yes: **direct call**. Events cannot return a value (request/reply over events is an anti-pattern).
**Q2. Is B's reaction part of A's responsibility, or B's own concern?**
- A's responsibility *includes* B's behavior (A orchestrates B) → **direct call**. E.g. `session` drives `agentLifecycle`; `loop` drives `llmRequester` / `toolExecutor`.
- B's reaction is B's own concern, A merely states a fact → **event**. E.g. `flag` reacts to `config.onDidChange`.
**Q3. How many consumers?**
- exactly one, known → **direct call**.
- zero / one / many, producer should not know → **event**.
**Q4. Would a direct A→B call create a cycle or violate scope direction?** → A *consequence check*, not a primary reason. Decide by Q1Q3 first; do not turn a genuine direct call into an event just to break a cycle.
**Q5. Is this fact part of the durable record / replay / cross-agent projection?** → Yes: **emit it on the wire** (`wireRecord`). State changes that must be recorded, replayed, or synchronized across agents are projected onto the wire, not handled by a direct call alone (`permission.set_mode`, `goal.create/update/clear`, `plan_mode.enter/exit`). The wire is the *durable record*, not the live notification channel.
### One-sentence rule
> "I am telling you to do this, and I may need the result" → **direct call.**
> "I am announcing that something happened; react if you care" → **event.**
> "I am announcing something, and you may step in, in order, possibly to veto" → **hook.**
### As extension points (open-closed)
The three mechanisms above are also where a domain accepts new behavior without being edited. When adding a scenario would otherwise require changing this domain's `if/else`, expose the right extension point instead:
| Need | Extension point | Typical scope |
|---|---|---|
| Register a new implementation / definition | a **registry / catalog** the domain queries | `App` |
| React to a fact the domain announces | an **event** on the bus | the announcing scope |
| Step into an operation in order / veto | a **hook** (`onWill`/`onDid`, `OrderedHookSlot`) | the owning scope |
| Swap a backend (File ↔ DB ↔ S3) | a **Store / Storage token** at the byte layer (see persistence.md) | `App` (composition root) |
The standard shape of a "registry / catalog the domain queries" row is an L3 contribution point: the target domain owns a `collection<T>` token, contributors call `this.provide(token, record)` from a unit, and a fold service in the target domain injects the `CollectionView` (incremental `onDidChange`; provider death withdraws the record). The four in-repo seams are `ConfigSectionContribution``ConfigRegistry`, `AgentToolContribution``AgentToolActivationService`, `AgentProfileContribution``IAgentProfileRegistry`, and `WireModelContribution``WireService` (file-level pointers: `packages/agent-core-v2/AGENTS.md` §Units and contribution points).
Closed-for-modification means: the domain's own file is not where new scenarios branch. If a new scenario forces an edit here, an extension point is missing or misplaced.
## 5. Dependency direction
Two layers are involved:
- **Scope direction**: short-lived → long-lived, **enforced by the container** (see orient.md).
- **Domain direction**: which domain may depend on which — **a matter of judgment**, not enforced by the container.
> **A depends on B iff A needs B's data or behavior to do its own job.**
Add one anti-rot heuristic to keep the graph from collapsing into a clique:
> **Do not let a more foundational / more-reused Service come to know a more specific / more-upstream one.**
Once a foundational component knows about an upstream scenario, it can no longer be reused by other scenarios and will almost always create a cycle.
### The boundaries of this repo
`agent-core-v2` has no mechanical domain-layer numbering — dependency direction is the judgment rule above, applied per domain. What remains enforceable is a small set of specific boundaries (`lint:imports`, `scripts/check-import-boundaries.mjs`):
- v2 never imports v1 (`@moonshot-ai/agent-core`).
- The kosong subtree keeps its strict internal order (`contract ← protocol ← provider/model`, purity bans, the `provider/bases` registration boundary).
Two standing red lines on top of that:
- The **base substrate** (`_base`, errors, wire types) never depends on any business domain.
- Business logic never depends on the **edge** (`gateway`, `rpc`, the `*Legacy` v1 adapters) — business code should not know REST / WebSocket exist.
- A cycle means knowledge was placed backwards: extract a third, more foundational Service, or invert the "notification" half into an event.
> Capability → orchestrator (e.g. `prompt → turn`) is allowed and present in this repo; the real red line is *inverted reuse* — a foundational / lower Service depending on a specific / upper one.
> When a Service is meant to be reached over the wire (`/api/v2`, WS), see [edge-exposure.md](edge-exposure.md) for the per-scope `resource:action` map, which Services may be exposed directly vs wrapped in a facade, and how events stream.
## 6. New-Service checklist
1. **What does it remember, and what is the state's identity?** → pick the scope (§2).
2. **What is the shortest-lived dependency it must inject?** → the scope cannot be longer than that.
3. **Does it own state at both a global and a per-instance lifetime?** → if yes, split Multi-Scope (§3).
4. **For each collaborator: am I commanding it, notifying it, or letting it participate?** → pick the calling style (§4).
5. **Does each dependency arrow make a more foundational thing know a more specific thing?** → if yes, invert it (§5).
## 7. Render the placement tree
After the checklist, render the result as a plaintext tree — the deliverable reviewers read. Keep it in the design doc or PR description.
```text
domain: `<name>` (owning scope: <Scope>)
├─ serves (who uses me) tag = HOW they reach me
│ ├─ (inject) <ConsumerDomain> @<Scope><what they use me for>
│ └─ (accessor) <ConsumerDomain> @<Scope><what they use me for>
├─ exposes (interfaces I provide, by scope)
│ ├─ App : <IXxxRegistry><role>
│ ├─ Workspace : <IWorkspaceXxx><role>
│ ├─ Session : <ISessionXxx><role>
│ └─ Agent : <IAgentXxx><role>
└─ depends (what I inject) tag = calling style
└─ <DepDomain> @<Scope> direct/event/hook — <what for>
```
Conventions:
- List **only real interfaces**; write `—` for a scope with no exposed interface. Most domains are single-scope — do not invent symmetry.
- On `depends`, tag each arrow with its calling style: `direct`, `event`, or `hook`.
- On `serves`, tag each consumer with its **access mechanism**, grouped `inject` first then `accessor`:
- `inject` — a descendant or peer scope DI-injects me. Resolved by the container; lifetime-safe.
- `accessor` — an ancestor or edge scope borrows me through `IScopeHandle.accessor.get(...)`. Valid only while this scope lives; never cache the result; must run before the child scope is disposed. See the cross-scope borrow diagram below.
- An empty `(inject)` group with a non-empty `(accessor)` group is a signal: the interface is currently an edge / lifecycle command surface — check it is not leaking internals.
- A consumer is upstream of you. If you cannot name one business consumer, the domain may be dead or mis-scoped.
### Cross-scope borrow diagram
When a domain has `accessor` consumers, draw the reverse-direction borrow next to the tree so it is never mistaken for injection:
```text
App scope
<AncestorService> ──holds──► IScopeHandle(<id>)
│ accessor.get(<IMyService>)
│ └── resolve runs inside the child scope
<Child> scope (<id>)
<MyService> ← the interface lives here
```
Read it as:
- `──holds──►` = the ancestor owns a handle to the child scope (it stores the key, not the service). DI allows this.
- `accessor.get(...)` = a **runtime borrow**, not a dependency edge. It must cross an `IScopeHandle`, run on demand, never be cached, and finish before the child scope is disposed.
Worked example — `sessionLifecycle`:
```text
domain: `sessionLifecycle` (owning scope: Workspace)
├─ serves (who uses me)
│ ├─ (inject) — (none)
│ └─ (accessor)
│ ├─ sessionLegacy @App(edge) — v1-compatible create/fork/archive/…
│ └─ gateway / rpc @App(edge) — native v2 session lifecycle actions
├─ exposes (interfaces I provide, by scope)
│ ├─ Workspace : ISessionLifecycleService — owns this workspace's live session scope tree
│ ├─ Session : — — (per-session state lives in sessionMetadata / agentLifecycle / …)
│ └─ Agent : — — (per-agent state lives in agentLifecycle)
└─ depends (what I inject)
├─ workspaceContext @Workspace seed — handler identity + persistence scope
├─ bootstrap @App direct — addresses session storage
├─ hostEnvironment @App direct — gates scope creation on the probe
├─ sessionIndex @App direct — persisted read model for cold resumes
├─ storage @App direct — atomic docs + append logs
├─ workspaceDirs / workspaceSkillCatalog / workspaceMcp / …
@Workspace direct — the handler's shared resource services
└─ event @App direct — broadcasts session-level facts (e.g. archived)
```
Cross-scope borrow for `sessionLifecycle`:
```text
App scope
WorkspaceLifecycleService ──holds──► IScopeHandle(workspaceId) (one per live handler)
│ accessor.get(ISessionLifecycleService)
│ └── resolve runs inside the Workspace scope
Workspace scope (workspaceId)
SessionLifecycleService ──holds──► IScopeHandle(sessionId)
│ accessor.get(ISessionMetadata) …
│ └── resolve runs inside the Session scope
Session scope (sessionId)
sessionMetadata / agentLifecycle / … ← per-session services live here
```
How the three lenses shaped it:
- **Scope (§2)** → the live registry of one workspace's session scopes is per-handler, so it is Workspace-scoped; the process-wide handler registry lives in the App-scoped `workspaceLifecycle`; per-session data stays in Session-scoped services, reached through the handle's `accessor`.
- **Dependency direction (§5)**`sessionLifecycle` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service.
- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `sessionLifecycle`.
For a multi-scope split, the `exposes` block fills more than one scope — see the `records` pattern in §3.
## Red lines (this stage)
- Scope is not a domain; ownership follows write authority and invariants, not read consumption.
- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`) that re-merge multiple domains.
- No `Map<sessionId, …>` at `App` to fake per-session state.
- Scope follows state identity; stateless Services are pulled down by their shortest-lived dependency, otherwise default to `App`.
- Do not pre-split a domain that has state at only one lifetime.
- Need a result / I orchestrate → direct call; stating a fact → event; ordered participation / may veto → hook.
- Foundational layers never know upstream ones; business code never depends on the edge layer.
- A cycle means knowledge is placed backwards — refactor, do not route around it.
- Render the placement tree with real interfaces only — never pad an empty scope for symmetry.
- Tag `serves` consumers with `inject` / `accessor`; an empty `inject` group is a signal to check the interface is not leaking internals.
- An `accessor` consumer is a runtime borrow across a scope boundary, not DI injection — never cache the result and finish before the child scope disposes.
- A `serves` list with no business consumer (or only edge consumers) signals a dead or leaking interface.

View file

@ -0,0 +1,203 @@
# Topic — Domain boundaries vs Scope
How to keep `agent-core-v2` from recreating a god object after splitting one. Read this before naming a Service, adding an `I{Domain}EntityService`, or deciding whether data belongs to `session`, `agent`, or `turn`.
## The one-sentence rule
> **Scope is a lifetime and visibility boundary; a domain is a responsibility and data-ownership boundary.**
A Service registered at `LifecycleScope.Session` or `LifecycleScope.Agent` is **not automatically in the `session` or `agent` domain**. Scope says when an instance is born, when it dies, and who can see it. Domain says which business responsibility it owns and which data it is allowed to mutate.
## Definitions
| Term | Meaning |
|---|---|
| **Scope** | Lifetime / visibility tier. Current code registers Services at `App`, `Session`, or `Agent`. |
| **Domain** | A cohesive business responsibility with its own model, invariants, and write authority. |
| **Entity** | Data with identity and lifecycle, usually suitable for `get/list/create/update/delete` semantics. |
| **Aggregate** | A consistency boundary: the owner that enforces invariants over a cluster of data. |
| **Read model / projection** | Derived data built for queries; it may be shaped like a domain, but it is not the write authority. |
| **Runtime state** | Ephemeral data that dies with its scope; it should not be forced into an entity store. |
## The data-ownership test
Do not ask "does Session / Agent / Turn use this data?". Most data is used by several of them. Ask these instead:
1. **What is the data's identity?** `sessionId`, `agentId`, `turnId`, `taskId`, `workspaceId`, `providerName`, or something else?
2. **Who is the only writer?** The writer is usually the owner. Readers and projectors are not owners.
3. **Who enforces the invariants?** The domain that decides valid transitions owns the model.
4. **What is the authoritative source?** Atomic document, append-log / event stream, blob, query projection, config, or runtime memory?
5. **Can it be named without `Session` / `Agent` / `Turn`?** If yes, it probably deserves its own domain.
Examples:
- `PermissionRules` are Agent-scoped, but `permission` owns rule changes and evaluation.
- `BackgroundTask` is spawned by an Agent, but `background` owns task state and output.
- `ContextMessage` is consumed by the Agent loop, but `contextMemory` / `wireRecord` owns history and replay.
- `SessionMeta` is about a Session, but it is owned by `sessionMetadata`, not by a broad `session` data bag.
## Persistence models are not all entity CRUD
Before introducing `I{Domain}EntityService`, classify the persistence model:
| Persistence model | Use when | Examples |
|---|---|---|
| **Atomic document** | One typed document per key | `SessionMeta`, `config.toml` |
| **Append-log / event-sourced** | The authoritative record is "what happened" | `wireRecord`, `contextMemory`, `goal`, `plan`, `permission` transitions |
| **Blob / key-value** | Large or content-addressed bytes | media offload, blob store |
| **Indexed query / read model** | Derived, queryable view | `sessionIndex`, future `IQueryStore` projections |
| **Registry / catalog** | Global or scoped known items | `workspace`, `toolRegistry` |
| **Ephemeral runtime state** | No durable entity | active turn handle, pending interactions, terminal handles |
See [persistence.md](persistence.md) for the `Store → Storage → backend` rules. A domain EntityService is a business facade over those stores; it is not a replacement for the store layer.
## Naming consequence
Do not name Services after a scope or a god-object-shaped concept:
- ❌ `IAgentEntityService`
- ❌ `IAgentDataService`
- ❌ `ISessionEntityService`
- ❌ `ITurnEntityService` that bundles context, tools, permissions, and telemetry
Name Services after the real owning domain:
- ✅ `ISessionMetadata`
- ✅ `ISessionIndex`
- ✅ `IAgentLifecycleService`
- ✅ `ITurnService`
- ✅ `IBackgroundTaskEntityService`
- ✅ `ICronTaskEntityService`
- ✅ `IPermissionRulesService`
`Session` and `Agent` are valid scope names. They are usually **not** good data-owner names.
## Split conclusion — `session`
`session` is both a Scope and a narrow Domain. Keep the Domain small.
The `session` domain owns only Session-level identity, metadata, lifecycle commands, and Session-level read views:
| Concern | Owner | Notes |
|---|---|---|
| `sessionId`, `workspaceId`, `sessionDir`, `metaScope` | `sessionContext` | Seeded facts; no IO |
| `SessionMeta` | `sessionMetadata` | Durable atomic document; entity-like |
| Open session scope registry | `sessionLifecycle` | Workspace-scope live handles, one registry per workspace handler (the process-wide handler registry is `workspaceLifecycle`); not the persisted entity table |
| Session commands such as `archive()` | `session` | Orchestrates metadata, agent teardown, and events |
| Persisted session list / get / count | `sessionIndex` | Backend-neutral read model |
| Running / idle / awaiting status | `sessionActivity` | Derived from interactions and active turns; owns no state |
`session` must not reabsorb these:
| Data | Real owner |
|---|---|
| Agent instances / handles | `agentLifecycle` |
| Turns | `turn` |
| Context messages | `contextMemory` / `wireRecord` |
| Tool state | `toolStore` / `tool` |
| Permission rules / mode | `permission` |
| Profile / model | `profile` |
| Goal / Plan | `goal` / `plan` |
| Background tasks | `background` |
| Cron tasks | `cron` |
| Pending approvals / questions | `interaction` / `approval` / `question` |
| Workspace | `workspace` |
| Provider / config | `provider` / `config` |
Entity-service conclusion for `session`:
- ✅ `ISessionMetadata` is already an entity-document Service.
- ✅ `ISessionIndex` is a query/read-model Service.
- ❌ Do not create a broad `ISessionEntityService` that owns agents, turns, records, interactions, logs, workspace, and config.
## Split conclusion — `agent`
`agent` is primarily a Scope and composition boundary, not a large data Domain.
Strictly, the `agent` domain owns only Agent-instance concerns:
| Concern | Owner | Notes |
|---|---|---|
| Agent instance identity / handle | `agentLifecycle` | Owns live Agent scope handles |
| Agent creation / removal | `agentLifecycle` | Lifecycle, not a data bag |
| Parent / child relationship | `session` / `agentLifecycle` depending on current code | Do not duplicate it into a new Agent data service |
| Active turn reference | `turn` | Turn is its own domain even though it is Agent-scoped |
Many Agent-scoped Services are **not** in the `agent` domain:
| Data / capability | Real owner | Persistence model |
|---|---|---|
| Wire records | `wireRecord` | Append-log |
| Context messages | `contextMemory` | Event-sourced through `wireRecord` |
| Profile / model config | `profile` | Config + wire records |
| Tool definitions / registry | `toolRegistry` | Runtime registry |
| Tool mutable state | `toolStore` | Wire records |
| Permission mode / rules | `permissionMode` / `permissionRules` | Wire records + config |
| Goal | `goal` | Wire records |
| Plan | `plan` | Wire records + plan file |
| Skill activation | `skill` | Wire records |
| Background tasks | `background` | Task records / output logs, candidate for entity service |
| Cron tasks | `cron` | Task records, candidate for entity service |
Entity-service conclusion for `agent`:
- ✅ Keep `IAgentLifecycleService` for Agent instance lifecycle.
- ✅ If a persisted Agent identity registry is ever needed, name it after that narrow concern, e.g. `IAgentInstanceRegistry`.
- ❌ Do not create `IAgentEntityService` or `IAgentDataService` that bundles profile, records, tools, permission, goal, plan, background, cron, and turn.
## Split conclusion — `turn`
`turn` is a Domain, but it is **not** currently a separate `LifecycleScope` in code; `ITurnService` is registered at `Agent` scope.
`turn` owns one execution round's runtime state and turn-level facts:
| Concern | Owner | Notes |
|---|---|---|
| Active `Turn` handle | `turn` | `id`, `abortController`, `ready`, `result` |
| Turn id allocation | `turn` | Restored from `turn.prompt` records and `context.append_loop_event` turn ids |
| Turn lifecycle hooks | `turn` | `onLaunched`, `onEnded`, `beforeStep`, `afterStep` |
| `turn.started` / `turn.ended` live events | `turn` | Live event stream |
`turn` must not own these:
| Data / capability | Real owner |
|---|---|
| Prompt and context messages | `contextMemory` |
| Append-only record log mechanics | `wireRecord` |
| Step loop | `loop` |
| Tool execution | `toolExecutor` / `tool` |
| Permission decisions | `permission` |
| External hook policy | `externalHooks` |
| Telemetry pipeline | `telemetry` |
| Event transport | `eventSink` |
Entity-service conclusion for `turn`:
- ✅ Keep `ITurnService` as a runtime orchestrator.
- ✅ Add a Turn read model / projection only if history queries are needed.
- ❌ Do not create `ITurnEntityService` with `create/update/delete/list` over a turn table as the authoritative model.
## Migration recipe
When moving data out of a v1 god object or reviewing a proposed EntityService:
1. **Name the data without using `Session`, `Agent`, or `Turn`.** If you cannot, the domain is probably unclear.
2. **Find the writer.** The exclusive writer is the likely owner.
3. **Find the invariant.** The Service that rejects invalid transitions owns the model.
4. **Classify the persistence model.** Atomic document, append-log, blob, query projection, registry, or runtime-only.
5. **Pick the Service shape.**
- Entity document / record → `I{Domain}EntityService` or domain-specific CRUD Service.
- Event-sourced → behavior Service + `wireRecord` record types + optional projection.
- Derived query → read-model Service, not a write authority.
- Runtime-only → scoped Service with no entity store.
6. **Choose the Scope by state identity.** Scope follows what the state is keyed by; it does not decide the domain name.
7. **Render the placement tree** from [design.md §7](design.md#7-render-the-placement-tree).
## Red lines (this topic)
- Scope is not a domain. `Session` / `Agent` scopes do not make data `session` / `agent` owned.
- Ownership follows write authority and invariants, not read consumption.
- Do not create `I{Scope}EntityService` bundles (`IAgentEntityService`, `ISessionEntityService`, `ITurnEntityService`) that re-merge multiple domains.
- Event-sourced domains keep behavior Services and append-log records; do not replace them with arbitrary CRUD.
- Read models may be shaped like a domain, but they are projections, not write authorities.
- A dependency is not ownership. A Service may inject another domain without owning that domain's data.

View file

@ -0,0 +1,183 @@
# Edge exposure — `resource:action` + WS events
How a domain's Services become the wire surface (`/api/v2`) and WebSocket events. This is a **design-time** decision: which Services are exposed, under what public `resource:action` name, and which events stream.
The transport (`/api/v2` over HTTP + WS) lives in the **edge** layer (`gateway`/`rpc`/`transport`). It borrows business Services by interface; business code never imports it.
## 1. The edge model
Four scopes, four URL shapes, one dispatcher:
```text
GET|POST /api/v2/:sa Core
GET|POST /api/v2/workspace/:workspace_id/:sa Workspace
GET|POST /api/v2/session/:session_id/:sa Session
GET|POST /api/v2/session/:session_id/agent/:agent_id/:sa Agent
```
`:sa` is a single path segment of the form `<resource>:<action>` (e.g.
`sessions:list`, `session:read`, `profile:getModel`).
- `:resource` is a **public** name (`sessions`, `session`, `profile`), never an internal domain token (`ISessionMetadata`).
- `:action` is the method. `GET` for reads, `POST` for writes.
- Body = the method's single argument (JSON), omitted for no-arg.
- Response = the project envelope `{ code, msg, data, request_id, details? }`.
- The dispatcher resolves the **scope** from the URL, the **Service** from an `actionMap`, calls the method, wraps the result.
```ts
// actionMap — the allowlist; hides internal domain names.
const actionMap = {
core: { 'sessions:list': { service: ISessionIndex, method: 'list' }, ... },
workspace: { 'skills:list': { service: IWorkspaceSkillCatalog, method: 'list' }, ... },
session: { 'session:read': { service: ISessionMetadata, method: 'read' }, ... },
agent: { 'profile:getModel': { service: IProfileService, method: 'getModel' }, ... },
};
```
The `actionMap` is the single allowlist: only mapped `resource:action` pairs are callable; unknown → `40001`.
## 2. What may be exposed directly
A Service method is directly exposable iff **all** hold:
1. Args are JSON-serializable (no live objects, `AbortSignal`, callbacks, resumer fns).
2. Return is JSON-serializable data or `void` (no `IScopeHandle`, `Turn`, `IProcess`, `AsyncIterable`, `IDisposable`, `Event`).
3. Errors are `KimiError` (coded).
4. It is a command/query, not a factory, stream, byte-store, or sink.
If any fail → add a wire-safe orchestration method to the owning domain Service (e.g. `IAgentPromptService.submit` settles `{turn_id}` instead of returning the live `PromptHandle`) or compose several domain Services at the edge — kap-server's `routes/prompts.ts` is the reference for edge-side composition.
## 3. Per-scope `resource:action` map
Read = `GET`, write = `POST`. `sid` = `session_id`, `aid` = `agent_id`.
### Core (`/api/v2/:resource:action`)
| resource | action | Service.method | verb |
|---|---|---|---|
| `sessions` | `listRecent` | ISessionIndex.listRecent | GET |
| `sessions` | `get` | ISessionIndex.get | GET |
| `sessions` | `count` | ISessionIndex.count | GET |
| `workspaces` | `list` | IWorkspaceService.list | GET |
| `workspaces` | `get` | IWorkspaceService.get | GET |
| `workspaces` | `createOrTouch` | IWorkspaceService.createOrTouch | POST |
| `workspaces` | `update` | IWorkspaceService.update | POST |
| `workspaces` | `delete` | IWorkspaceService.delete | POST |
| `config` | `get` / `getAll` / `inspect` | IConfigService.* | GET |
| `config` | `set` / `replace` / `reload` | IConfigService.* | POST |
| `providers` | `list` / `get` | IProviderService.* | GET |
| `providers` | `set` / `delete` | IProviderService.* | POST |
| `oauth` | `startLogin` / `cancelLogin` / `logout` | IOAuthService.* | POST |
| `oauth` | `getFlow` / `status` | IOAuthService.* | GET |
| `auth` | `summarize` | IAuthSummaryService.summarize | GET |
| `auth` | `ensureReady` | IAuthSummaryService.ensureReady | POST |
| `flags` | `snapshot` / `enabled` / `explain` / `explainAll` | IFlagService.* | GET |
| `fs` | `browse` / `home` | IHostFolderBrowser.* | GET |
| `meta` | `getEnv` / `detect` | IBootstrapService.* | GET |
### Session (`/api/v2/session/:sid/:resource:action`)
| resource | action | Service.method | verb |
|---|---|---|---|
| `session` | `read` | ISessionMetadata.read | GET |
| `session` | `update` | ISessionMetadata.update | POST |
| `session` | `setTitle` | ISessionMetadata.setTitle | POST |
| `session` | `setArchived` | ISessionMetadata.setArchived | POST |
| `session` | `status` | ISessionActivity.status | GET |
| `session` | `isIdle` | ISessionActivity.isIdle | GET |
| `session` | `archive` | ISessionLifecycleService.archive | POST |
| `approvals` | `listPending` | IApprovalService.listPending | GET |
| `approvals` | `decide` | IApprovalService.decide | POST |
| `questions` | `listPending` | IQuestionService.listPending | GET |
| `questions` | `answer` | IQuestionService.answer | POST |
| `interactions` | `listPending` | IInteractionService.listPending | GET |
| `interactions` | `respond` | IInteractionService.respond | POST |
| `workspace` | `workDir` / `additionalDirs` / `resolve` | ISessionWorkspaceContext.* | GET |
### Agent (`/api/v2/session/:sid/agent/:aid/:resource:action`)
| resource | action | Service.method | verb |
|---|---|---|---|
| `goal` | `get` | IGoalService.getGoal | GET |
| `goal` | `create` / `pause` / `resume` / `cancel` | IGoalService.* | POST |
| `plan` | `status` | IPlanService.status | GET |
| `plan` | `enter` / `exit` / `cancel` / `clear` | IPlanService.* | POST |
| `tasks` | `list` / `get` / `readOutput` | IBackgroundService.* | GET |
| `tasks` | `stop` / `detach` | IBackgroundService.* | POST |
| `usage` | `status` | IUsageService.status | GET |
| `context` | `status` | IAgentTokenCountingService.get | GET |
| `swarm` | `isActive` | ISwarmService.isActive | GET |
| `swarm` | `enter` / `exit` | ISwarmService.* | POST |
| `permission` | `getMode` | IPermissionModeService.mode | GET |
| `permission` | `setMode` | IPermissionModeService.setMode | POST |
| `permissionRules` | `list` | IPermissionRulesService.rules | GET |
| `permissionRules` | `addRules` | IPermissionRulesService.addRules | POST |
| `profile` | `get` / `getModel` / `getSystemPrompt` / `getActiveToolNames` | IProfileService.* | GET |
| `profile` | `setModel` / `setThinking` | IProfileService.* | POST |
| `messages` | `list` | IContextMemory.get | GET |
| `messages` | `splice` | IContextMemory.splice | POST |
| `toolStore` | `get` / `data` | IToolStoreService.* | GET |
| `toolStore` | `set` | IToolStoreService.set | POST |
| `mcp` | `list` | IMcpService.list | GET |
| `mcp` | `reconnect` | IMcpService.reconnect | POST |
| `tools` | `list` | IToolRegistry.list | GET |
## 4. Facade-needed (wrap before exposing)
These fail §2 and must be wrapped in a facade that takes ids and returns data:
| Service | Why not direct | Facade shape |
|---|---|---|
| ISessionLifecycleService | returns `IScopeHandle` | `sessions.create` / `fork` / `close` / `archive` → wire Session |
| IAgentPromptService / IAgentTurnService | returns `Turn` handle | `prompts.submit` / `steer` / `abort` / `undo` |
| ILLMRequester | `AsyncIterable` stream | stream over WS, not RPC |
| ISubagentHost | `SubagentHandle` | `subagents.spawn` / `resume` → info |
| IProcessRunner | `IProcess` streams | terminal (separate WS protocol) |
| Storage / Store (IFileSystemStorageService / IAppendLogStore / IAtomicDocumentStore / IBlobStore) | bytes / streams | not for RPC |
| IAgentFileSystem | `withCwd` handle | `fs.read` / `write` → text/bytes |
| IExternalHooksService | server-side outbound | not exposed |
| IWireRecord | write-ahead log | internal |
## 5. WS events
A single WebSocket endpoint multiplexes RPC `call`s and event `listen`s over a JSON protocol (the lean counterpart of VSCode's `IMessagePassingProtocol`, carrying the same safety features — see §6):
```text
WS /api/v2/ws
```
Client → server: `hello` (auth), `call` (scope + `resource:action` + arg), `cancel`, `listen` (scope + event), `unlisten`, `pong`.
Server → client: `ready`, `result`, `error`, `event`, `ping`.
`call` reuses the same dispatcher as the HTTP routes (scope + `actionMap`). `listen` subscribes to an `Event<T>` source and forwards each emission as an `event` message, keyed by the client-chosen `id`.
The `eventMap` binds a public event name to the scope's `Event` source (analogous to the `actionMap`):
| Scope | event | Source |
|---|---|---|
| Core | `events` | `IEventService.subscribe` (process-wide `DomainEvent` bus) |
| Agent | `events` | `IEventSink.on` (per-agent `AgentEvent` stream) |
Session-level `onDidChange` sources (metadata / interactions) carry no payload today, so they are not exposed until there is a concrete consumer.
Safety / reliability (carried over from `packages/server/src/ws/connection.ts` and VSCode's `ChannelServer`):
- request ids + active-request table — `cancel` / `unlisten` disposes them;
- heartbeat — `ping` every 30s, `pong` timeout 10s → `terminate`;
- schema validation — invalid frames are dropped, not fatal;
- graceful close — dispose listeners, cancel pending, reject in-flight calls;
- no stack traces over the wire;
- non-serializable event payloads are dropped, never fatal.
Cursor / replay / resync for events is a future addition (a separate `call` before `listen`); the raw stream is the foundation.
## 6. Red lines (edge exposure)
- Never expose an internal domain token (`ISessionMetadata`) as a URL segment — use a public `resource` name + `action`.
- Never expose a method that returns a handle / stream / bytes / disposable — wrap in a facade.
- Never expose a method that takes a live object / `AbortSignal` / callback / resumer fn — wrap in a facade.
- Session / Agent Services are reached by `accessor.get` with the id from the URL — never cache the result; finish before the scope disposes.
- The `actionMap` is the allowlist — only mapped `resource:action` pairs are callable; unknown → `40001`.
- Events stream over WS (`listen`), never RPC (`call`).
- Business code never imports the edge (`gateway` / `rpc` / `transport`) — the edge borrows business Services by interface.
- Read = `GET`, write = `POST`; do not overload `POST` for reads when caching / browser-friendliness matters.

View file

@ -0,0 +1,40 @@
# Topic — Errors
Error infrastructure for agent-core-v2: base classes, the per-domain code contract, wire serialization, and the conventions domains follow when raising errors. The package-level reference is `packages/agent-core-v2/docs/errors.md`; this topic summarizes the hot-path rules.
Base classes and serialization are **centralized** in `_base/errors`; error **codes** are **decentralized** — each domain owns an `errors.ts` that self-registers its codes and metadata, and the `src/errors.ts` facade aggregates them into the unified `ErrorCodes` const.
## Where things live
- `src/_base/errors/errors.ts`: base classes — `Error2`, `ExpectedError`, `ErrorNoTelemetry`, `BugIndicatingError`, `NotImplementedError`, plus `isError2` and `unwrapErrorCause`.
- `src/_base/errors/codes.ts`: the `ErrorDomain` contract, the registry (`registerErrorDomain` / `errorInfo` / `isErrorCode`), and `CoreErrors` (`internal`, `not_implemented`). The `ErrorCode` union type is derived by `#/errors` from the aggregated domain definitions.
- `src/_base/errors/serialize.ts`: `ErrorPayload`, `isCodedError`, `toErrorPayload`, `fromErrorPayload`. Wire-facing names (`KimiErrorPayload`, `toKimiErrorPayload`) mirror the protocol and are kept as-is.
- `src/_base/errors/unexpectedError.ts`: `onUnexpectedError` / `setUnexpectedErrorHandler` (global handler).
- `src/<domain>/errors.ts`: the domain's `XxxErrors` descriptor (codes + retryable list + per-code info overrides), self-registered on import.
- `src/errors.ts`: the **facade** — imports every domain's `errors.ts`, builds `ErrorCodes`, re-exports the primitives. Throw sites import from here.
## Conventions (hard rules)
- **Throw a coded error, not a bare string.** `throw new Error2(ErrorCodes.X, …)`. Bare `new Error` only for unreachable guards; `BugIndicatingError` for caller bugs; `NotImplementedError('feature')` for stubs.
- **Define codes in the owning domain**, in `<domain>/errors.ts` as an `XxxErrors` descriptor (`satisfies ErrorDomain` + `registerErrorDomain`), then wire it into the facade. Never add domain codes to `_base/errors`.
- **One `code` per failure mode.** Codes read `domain.reason`. The valid code strings are derived from the aggregated domain definitions (`ErrorCode` in `#/errors` is computed from the `ErrorCodes` aggregate): **add new codes to the owning domain's `errors.ts`** — registration throws on cross-domain collisions. Renaming/removing a code is a major.
- **Translate foreign errors at the boundary.** Provider/HTTP, fs, MCP errors are re-thrown as the owning domain's coded error. `_base/errors` never imports a business domain.
- **Translation is idempotent and cause-preserving.** Translators (`toHostFsError`, `toStorageIoError`) pass through an already-translated error and always keep the original as `cause`.
- **`details` is structured and JSON-serializable; `message` is a short human sentence.** Paths/errnos/scope/key go into `details`, not the message.
- **Cancellation passes through untranslated** (`UserCancellationError` from `_base/utils/abort`) — apply only at boundaries that can actually see cancellation; do not sprinkle the check everywhere.
- **Classify wrapped errors via `unwrapErrorCause`** — errno/status predicates test the unwrapped cause, not the coded wrapper.
- **Branch on `code`, never `instanceof`, across the wire.** In-process, `instanceof Error2` / `isCodedError` are fine.
## Reference tiers
- `os.fs``HostFsError` via `toHostFsError` (`os/interface/hostFsErrors.ts`): errno → `os.fs.*`, details `{ path, op, errno?, syscall? }`.
- `os.process``HostProcessError`: `spawn_failed` / `kill_failed`, raw error as `cause`.
- `storage``StorageError` (`persistence/interface/storage.ts`): `not_found` / `decode_failed` / `corrupted` / `io_failed` (retryable) / `locked` (retryable). ENOENT keeps absence semantics, never an error. A locked query store throws `storage.locked`; consumers catch it explicitly and fall back — no silent no-op degradation.
- `wire``WireError` (`wire/errors.ts`): `DuplicateOpError`, `CycleError`, and `wire.unknown_record` (replay skips unknown records, reports via `onUnexpectedError`, returns `{ unknownRecords }`).
## Red lines (this topic)
- Throw a coded error with a `code`, not a bare string (except unreachable guards / `BugIndicatingError` / `NotImplementedError`).
- Codes live in the owning domain's `errors.ts` and self-register; new codes land in the owning domain first.
- Translate foreign errors at the owning domain's boundary, idempotently, with `cause` and structured `details`; `_base/errors` never imports a business domain.
- Branch on `code` across the wire, never `instanceof`.

View file

@ -0,0 +1,108 @@
# Topic — Flags
Experimental feature-flag gating for agent-core-v2 — an App-scope `IFlagService` resolver plus a writable `IFlagRegistry` catalog that domains contribute their flags to, backed by the `[experimental]` config section.
Gate not-yet-public features behind `IFlagService.enabled(id)`, per the repository hard rule that unreleased behavior must be flag-gated. v1 was a process-global `FlagResolver` singleton over a central `FLAG_DEFINITIONS` array; v2 is a scoped DI service whose flag definitions are registered **decentrally** by each owning domain — there is no central catalog to edit.
## Layout
- `src/app/flag/flagRegistry.ts``IFlagRegistry` token + `FlagDefinitionInput` / `FlagId` / `FlagSurface` types + `registerFlagDefinition` / `getContributedFlags` (import-time contribution queue).
- `src/app/flag/flagRegistryService.ts``FlagRegistryService` impl; in-memory catalog seeded from import-time contributions; App scope.
- `src/app/flag/flag.ts``IFlagService` token + resolver types (`ExperimentalFlagMap`, `ExperimentalFlagConfig`, `ExperimentalFlagSource`, `ExperimentalFeatureState`) + `EXPERIMENTAL_SECTION` (`experimental`) / `ExperimentalConfigSchema` (zod) + the module-level `registerConfigSection(EXPERIMENTAL_SECTION, …)` call that owns the section.
- `src/app/flag/flagService.ts``FlagService` impl + `MASTER_ENV` (`KIMI_CODE_EXPERIMENTAL_FLAG`); reads definitions from `IFlagRegistry` and overrides from `IConfigService`; self-registers at App scope.
- `src/app/flag/index.ts`**removed (no barrel)**; `src/index.ts` imports the `flag` leafs precisely instead (e.g. `import './app/flag/flagService'`).
- `src/<domain>/flag.ts` — each domain that owns a flag declares it here and calls `registerFlagDefinition` at the module top level (e.g. `src/agent/toolSelect/flag.ts`). The directory already names the domain, so the file is just `flag.ts`.
## Public surface
- `IFlagService` (DI token, App scope): `enabled(id)`, `explain(id)`, `snapshot()`, `enabledIds()`, `explainAll()`, `setConfigOverrides(overrides)`, `registry`.
- `IFlagRegistry` (DI token, App scope): `register(definition)`, `get(id)`, `list()` — writable catalog. `register` is the **runtime** path (tests, dynamic registration); `IFlagService.registry` exposes the same instance for hosts/UI to enumerate flags without resolving them.
- `registerFlagDefinition(definition)` — the **import-time** path. Domains call this from their `flag.ts` top level; contributions are queued and drained by `FlagRegistryService` when it is instantiated.
- `FlagService` / `FlagRegistryService`: exported for tests and hosts that construct them directly.
## Resolution precedence
Highest wins; env is read live on every call (nothing cached):
1. Master env `KIMI_CODE_EXPERIMENTAL_FLAG` truthy → every flag on.
2. Per-feature `def.env` (e.g. `KIMI_CODE_EXPERIMENTAL_MY_FEATURE`) → forces on/off.
3. `[experimental]` config section per-flag override.
4. Registry `default`.
`explain(id)` returns the winning `source` (`master-env` | `env` | `config` | `default`) plus the effective `configValue`. `explain(id)` returns `undefined` (and `enabled(id)` returns `false`) for an id that no domain has registered.
## Config integration
- The flag domain owns the `[experimental]` section: `src/app/flag/flag.ts` registers it at module load via `registerConfigSection(EXPERIMENTAL_SECTION, ExperimentalConfigSchema, { fromToml, toToml })` (import = register, drained by `ConfigRegistry` at construction); `FlagService` reads overrides from `IConfigService`.
- It subscribes `IConfigService.onDidChange` and refreshes overrides whenever the `experimental` domain changes, so config edits apply live.
- `ConfigRegistry.registerSection` throws if a domain is registered twice — `experimental` is owned exclusively by the flag domain.
- `setConfigOverrides(overrides)` is an imperative escape hatch for tests and hosts without an `IConfigService`; hosts on `IConfigService` should set the `[experimental]` section instead.
Config shape:
```toml
[experimental]
my_feature = false
```
Keys are intentionally loose (`z.record(z.string(), z.boolean())`), so obsolete flags stay inert config.
## Add a flag
Declare the definition in the owning domain's `flag.ts` and call `registerFlagDefinition` at the module top level. There is no central catalog to edit.
`src/<domain>/flag.ts`:
```ts
import { type FlagDefinitionInput, registerFlagDefinition } from '#/app/flag/flagRegistry';
export const myFeatureFlag: FlagDefinitionInput = {
id: 'my_feature',
title: 'My feature',
description: '...',
env: 'KIMI_CODE_EXPERIMENTAL_MY_FEATURE',
default: false,
surface: 'both',
};
registerFlagDefinition(myFeatureFlag);
```
Then ensure the package entry `src/index.ts` imports the flag leaf precisely so the top-level call runs at import time — there is no `src/<domain>/index.ts` barrel:
```ts
// src/index.ts
import './<domain>/flag';
```
`src/index.ts` imports every domain's leaf files precisely (one line per leaf), so the contribution runs during bootstrap, before any scope is created — and therefore before any consumer resolves `IFlagService`.
- `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`.
- `id` must not be `flag`. A duplicate `id` throws when `FlagRegistryService` drains the contributions.
- `FlagId` is `string`, not a literal union: with no central catalog there is nothing to derive it from, so `enabled()` has no compile-time typo-checking. Cover gated behavior with tests instead.
- `surface`: `core` | `tui` | `both` (documentation/grouping only; not used in resolution).
## Consume a flag
Inject `IFlagService` and gate on it. It is resolvable from any scope (App ancestor):
```ts
constructor(@IFlagService private readonly flags: IFlagService) {}
// ...
if (!this.flags.enabled('my_feature')) return;
```
## Layering & scope
- Domain `flag` imports only `config` downward.
- It cannot live in `_base`: registering/reading the config section requires importing `config`, and `_base` is pure infrastructure that must not know any business domain.
- Scope: `IFlagRegistry` and `IFlagService` are both `App`. Env + config are process-global inputs, so there is no per-session/agent state. Flag definitions are contributed at **import time** (top-level `registerFlagDefinition` calls), so they are queued before any scope is created and drained when `FlagRegistryService` is first instantiated — before `IFlagService` is first resolved.
- Tests build `FlagService` + `FlagRegistryService` directly with a real `ConfigRegistry`/`ConfigService` and an injected env map, then `register` the flags they exercise.
## Red lines (this topic)
- Gate unreleased behavior behind a registered flag; no ad-hoc env toggles.
- Contribute each flag from the **owning domain's** `flag.ts` (`src/<domain>/flag.ts`) via a top-level `registerFlagDefinition` call; there is no central catalog to edit. The directory names the domain, so the file is just `flag.ts`.
- `env` must start with `KIMI_CODE_EXPERIMENTAL_`, be unique, and not equal `KIMI_CODE_EXPERIMENTAL_FLAG`; `id` must not be `flag`.
- `FlagId` is `string` (decentralized registration) — do not reintroduce a central `FLAG_DEFINITIONS` array or a derived literal union.
- `flag` lives at `App` scope — never in `_base`, never per-session.

View file

@ -0,0 +1,295 @@
# Stage 3 — Implement
Write the contract leaf, implementation leaf (with its registration), and the package-entry lines that load them. Each section below introduces one DI building block as you need it. Source lives in `src/_base/di/`.
## Standard recipe for a new `IXxxService`
1. **Contract leaf**`src/<domain>/<domain>.ts`: interface (with `_serviceBrand`) + `createDecorator` identity.
2. **Impl leaf**`src/<domain>/<domain>Service.ts`: class with `@IX` constructor deps; top-level `registerScopedService(scope, IX, Impl, activation, '<domain>')`. The fourth argument is activation; the fifth is the domain.
3. **Entry**`src/index.ts`: load each leaf precisely — `export * from './<domain>/<domain>';` for the contract and `import './<domain>/<domain>Service';` for the impl (importing the impl runs the registration). **No `src/<domain>/index.ts` barrel.**
4. **Tests** — see test.md.
There is **no central wiring file**: bindings live in each domain's impl file and are collected through import side effects.
## §1 Interface + identity (a global service, no deps)
```ts
// greet/greet.ts
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface IGreeter {
readonly _serviceBrand: undefined; // type marker: tells DI "this is a service"
hello(): string;
}
export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('greeter');
```
`createDecorator(name)` produces a `ServiceIdentifier` that is three things at once: a runtime key, a parameter decorator, and a compile-time carrier of the `IGreeter` type.
> **The identity name is globally unique.** `createDecorator` caches by `name`; two domains using the same string collide and share one identity.
```ts
// greet/greetService.ts
import { LifecycleScope } from '#/app/scopes';
import { registerScopedService, ScopeActivation } from '#/_base/di/scope';
import { IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined; // mirrors the interface marker
hello(): string { return 'hi'; }
}
registerScopedService(
LifecycleScope.App, // lifetime: process-wide
IGreeter, // identity
Greeter, // implementation
ScopeActivation.OnScopeCreated, // construct when the App scope is created
'greet', // domain name (for diagnostics)
);
```
The scope a class binds to is an **intrinsic property of the class**, decided at the registration point, not the call site.
The impl's top-level `registerScopedService` runs as soon as the module is imported. There is no `greet/index.ts` barrel — instead, add the leafs to the package entry `src/index.ts`, one line per leaf:
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService'; // this import runs registerScopedService
```
Anyone can now `accessor.get(IGreeter)` the single global instance.
## §2 Constructor injection (your service uses others)
```ts
export class SessionMetadata extends Disposable implements ISessionMetadata {
declare readonly _serviceBrand: undefined;
constructor(
@ISessionContext private readonly ctx: ISessionContext,
@IAtomicDocumentStore private readonly store: IAtomicDocumentStore,
@ILogService private readonly log: ILogService,
) {
super();
}
}
```
`@ISessionContext` records "parameter 0 needs `ISessionContext`" on the class metadata; the container fills it when constructing.
Three inviolable constraints:
1. **Do not `new` a class with `@IService` deps**`new` bypasses registration, scope, and the singleton cache. Inject with `@IX` or `accessor.get(IX)`.
2. **`@IX` decorates constructor parameters only.** Decorating a field/method throws at runtime.
3. **Parameter order depends on how the object is built** — for `createInstance` non-singletons, static params come first (see §7); for scoped services, `@IX` params are conventionally first and any static params need defaults. See service-authoring.md §constructor-conventions.
Consumers resolve by interface and never import the impl class:
```ts
const meta = accessor.get(ISessionMetadata); // type is ISessionMetadata
```
> If you need "a config" rather than "a service", model it as a service (e.g. `IConfigService`) and inject it. If you need a per-turn, parameterized, non-singleton object, see §7.
## §3 Scoped registration (not global)
Swap the `scope` argument to bind to a different tier. Use `ScopeActivation.OnDemand` when the service should be constructed only on its first `get()`:
```ts
registerScopedService(
LifecycleScope.Session,
ISessionMetadata,
SessionMetadata,
ScopeActivation.OnDemand,
'sessionMetadata',
);
```
Remember the visibility rule from orient.md: a service may inject services from its own scope or any ancestor; never from a descendant.
## §4 Releasing resources (`Disposable`)
For a service that subscribes to events, starts timers, or holds handles:
```ts
import { Disposable } from '#/_base/di/lifecycle';
export class WSBroadcastService extends Disposable implements IWSBroadcastService {
declare readonly _serviceBrand: undefined;
constructor(@IEventService event: IEventService) {
super();
this._register(event.subscribe(() => { /* … */ })); // collect child resources
}
}
```
- Extend `Disposable`, collect any `IDisposable` with `this._register(d)` (event subscriptions, `toDisposable(fn)`, etc.).
- The container calls `dispose()` automatically when the service is torn down; child resources release in turn.
- Disposal order is deterministic (orient.md): child scopes first; within a scope the Ledger (`src/_base/lifecycle/`) tears entries down in strict reverse registration order, serially — `Disposable` / `DisposableStore` delegate to it.
- Extend `Service` (from `#/_base/di/service`) instead when the unit needs capability calls on `this` (`provide` / `effect` / `on` / `get` / `ref`) — e.g. contributing a record to a `collection` token. `Service` extends `Disposable` (so `_register` is unchanged) and adds the two-phase construction protocol: `provide` / `on` / `effect` calls inside the constructor are buffered and flushed by the kernel after `Reflect.construct`; `get` / `ref` throw inside the constructor — dependencies stay constructor parameters. A manually `new`ed `Service` has no capabilities: every capability call throws.
## §5 Scope activation
`ScopeActivation` is the only construction-timing choice for scoped services:
```ts
export enum ScopeActivation {
OnScopeCreated = 0,
OnDemand = 1,
}
```
```ts
// Default: construct the real instance while the App scope is created.
registerScopedService(
LifecycleScope.App,
ILogService,
LogService,
ScopeActivation.OnScopeCreated,
'log',
);
// Construct the real instance on the first get(IScopeRegistry).
registerScopedService(
LifecycleScope.App,
IScopeRegistry,
ScopeRegistry,
ScopeActivation.OnDemand,
'gateway',
);
```
`ScopeActivation.OnScopeCreated` is the default fourth argument. Scope creation activates every registration using this mode, after constructing its dependencies. An eager constructor failure no longer fails scope creation: the unit lands in sticky `Failed` — scope creation succeeds, resolving the unit rethrows its error, and an explicit `update()` reloads it (see the bootstrap note below). Use it for ordinary services and for constructor side effects that must exist when the scope becomes ready.
`ScopeActivation.OnDemand` stores the descriptor without constructing the service. The first `get()` constructs and caches the real instance directly; later `get()` calls return that same instance. Use it only when construction should wait until the service is actually requested.
Both modes use the same dependency graph and reject cycles with `CyclicDependencyError`.
The complete registration signature is `registerScopedService(scope, id, ctor, activation = ScopeActivation.OnScopeCreated, domain?)`: activation is the fourth argument and domain is the fifth.
**Bootstrap shares the dynamic provide path.** Scope creation (`Scope.createApp` / `Scope.createChild` / `createScopedChildHandle` in `src/_base/di/scope.ts`) submits the scope kind's entire `registerScopedService` batch as ONE cascade transaction via `provideAll`: every token registers before the activation wave runs, so **registration order never matters**, and untracked transitive `createInstance` resolutions succeed inside the batch. A seed occupying a token (the `extra` tuple in `ScopeOptions`) overrides the static registration for that token. `activateScopeServices` is gone — there is no separate static activation path.
## §6 Using a service inside a plain function (`invokeFunction`)
When you do not want a new class and just need a service once, or when you expose a `ServicesAccessor` to the outside:
```ts
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>): T => instantiation.invokeFunction((a) => a.get(id)),
};
```
`invokeFunction(fn)` hands `fn` a `ServicesAccessor` valid **only during that call**.
> **The accessor is valid only during the invocation.** Calling `accessor.get()` after `invokeFunction` returns throws `"service accessor is only valid during the invocation"`. Do not stash it for async use — inject the service in the constructor (§2) if you need it long-term.
## §7 Creating a non-singleton object with deps (`createInstance`)
For a per-turn executor that also has `@IService` deps:
```ts
class TurnRunner {
constructor(
private readonly input: string, // static param: passed by caller
private readonly turn: number, // static param: passed by caller
@ILogService private readonly log: ILogService, // service param: injected by container
) {}
}
const runner = instantiation.createInstance(TurnRunner, 'hello', 1);
```
Static params come first (you pass them), service params follow (the container fills them), then `Reflect.construct` builds the instance. This object is **not** placed in any scope's singleton cache — every call is a fresh instance — and it is not tracked as a cascade unit either: `createInstance` products are cascade-exempt leaves that no cascade tears down or rebuilds; their owner disposes them.
> This is why service params must follow static params **for `createInstance`**: the container sorts by the parameter positions recorded via `@IX`. `_serviceBrand` lets the compiler tell the two kinds apart. Scoped services built by `registerScopedService` follow a different convention (`@IX` params first, optional static params after) — see service-authoring.md §constructor-conventions.
## §8 Spawning a child scope / child container
For a service that "starts a new session / agent" and needs a child scope, inject `IInstantiationService` itself (every container binds itself as `IInstantiationService`):
```ts
export class ScopeRegistry implements IScopeRegistry {
declare readonly _serviceBrand: undefined;
constructor(@IInstantiationService private readonly instantiation: IInstantiationService) {}
createSession(opts: CreateSessionOptions): Promise<IScopeHandle> {
const collection = new ServiceCollection();
for (const entry of getScopedServiceDescriptors(LifecycleScope.Session)) {
collection.set(entry.id, entry.descriptor); // collect Session-tier descriptors
}
const child = this.instantiation.createChild(collection); // spawn child container
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>): T => child.invokeFunction((a) => a.get(id)),
};
const handle: IScopeHandle = { id: opts.sessionId, kind: LifecycleScope.Session, accessor };
this.sessions.set(opts.sessionId, handle);
return Promise.resolve(handle);
}
}
```
Key points:
- `getScopedServiceDescriptors(scope)` returns every descriptor registered at that tier; load them into a `ServiceCollection`.
- `instantiation.createChild(collection)` builds a child container whose parent pointer is the current container — so the child resolves upward to `App` services (the visibility rule).
- Expose the child to the outside by wrapping it in a `ServicesAccessor` via `invokeFunction` (§6).
> Higher-level code usually calls `Scope.createChild(kind, id)` (it does the "filter descriptors + build child" for you, then submits the whole batch through `provideAll` as one cascade transaction — see §5). Drop to the manual `ServiceCollection` form only when you need explicit control; to change bindings on an already-created container, prefer `provide` / `unprovide` / `update` over rebuilding a collection. Before the static batch lands, the scope-creation point runs the kernel's `ScopeUnits` fold (`_base/di/scopeUnits.ts` — materializes the recipes contributed to `ScopeUnits(kind)` as per-scope units) and then the `ScopeOptions.assemble` hook — the session domain uses the hook to construct its seed-adapter units (`session/sessionSeed/sessionSeedAdapters.ts`) so their provided tokens exist before the session services activate.
## §9 Cyclic dependencies (forbidden — refactor)
Business rule: **no cyclic dependencies.** The container rejects them; the correct response is to refactor, not to make it run.
### The container rejects synchronous cycles
If A needs B while being created and B needs A while being created, the container throws `CyclicDependencyError` with a `path` like `['A', 'B', 'A']`. Self-cycles (A depends on itself) are also rejected. This is a protection mechanism telling you the two services' responsibilities are mis-drawn.
### Why cycles are disallowed
- Scope layering makes normal dependencies a DAG (Agent → Session → Workspace → App, resolving upward); a cycle is almost always a design smell.
- "Making the cycle happen to work" turns construction order into an implicit contract — hard to debug.
v2's stance: **the dependency graph must be acyclic.**
### How to refactor (in priority order)
1. **Extract a third service C.** Move the part A and B both need into C; let A and B both depend on C instead of each other. The most common fix.
2. **Decouple with an event.** If A only needs to know about a change in B, have B emit via `IEventService` and A subscribe, rather than A holding a reference to B.
3. **Re-partition scope.** One of them may belong at a different tier — moving it makes the cycle disappear.
### Activation does not break cycles
Both `ScopeActivation.OnScopeCreated` and `ScopeActivation.OnDemand` construct through the same synchronous dependency graph. Changing activation cannot make a cycle valid. On `CyclicDependencyError`, refactor per the above.
## Interface cheat sheet
| Interface | Section | Role |
|---|---|---|
| `createDecorator<T>(name)``ServiceIdentifier<T>` | §1 | identity (runtime key + compile-time type + param decorator) |
| `@IService` | §2, §7 | declare a dependency on a constructor param |
| `registerScopedService(scope, id, ctor, activation, domain)` | §1, §3, §5 | bind an impl to a lifetime tier and construction time |
| `ServicesAccessor.get(IX)` | §2, §6 | resolve an instance by interface |
| `IInstantiationService.invokeFunction(fn, …)` | §6, §8 | obtain a temporary accessor inside a function |
| `IInstantiationService.createInstance(ctor, …args)` | §7 | build a non-singleton object with deps injected |
| `IInstantiationService.createChild(collection)` | §8 | spawn a child container |
| `getScopedServiceDescriptors(scope)` | §8 | retrieve all descriptors registered at a tier |
| `Disposable` / `DisposableStore` / `IDisposable` | §4 | resource management and disposal |
| `Scope` / `LifecycleScope` | §3, §8 | the lifetime tree |
| `ScopeActivation` | §3, §5 | choose scope-created or first-`get()` construction |
| `Service` (`_base/di/service`) | §4 | unit base class — `this.provide/effect/on/get/ref` capabilities, two-phase construction |
| `collection<T>(name)` / `CollectionView<T>` (`_base/di/collection`) | §4 | contribution-point token + the fold's live view (provider death withdraws the record) |
| `SyncDescriptor` | (tests / low-level) | package a constructor + static args into a pending descriptor |
> Legacy export (not used in v2, just recognize it): `refineServiceDecorator` is a VS Code leftover DI helper. v2 src/test has zero references; always use `registerScopedService`.
## Red lines (this stage)
- No `new` on a class whose constructor carries `@IService` deps — inject or `accessor.get(IX)`.
- `@IX` decorates constructor params only; parameter order depends on construction (static-first for `createInstance`, `@IX`-first for scoped services — see service-authoring.md).
- Both interface and impl carry `_serviceBrand`; the `createDecorator` name is globally unique.
- `ServicesAccessor` is valid only during `invokeFunction` — never stash it for async use.
- No cyclic dependencies — refactor (extract / event / re-scope); activation does not change cycle detection.

View file

@ -0,0 +1,110 @@
# Stage 1 — Orient
Understand the DI × Scope black box and the file conventions before touching business code.
## The DI black box
When writing business code you declare three things; the container handles the rest (when to construct, whether it is the same instance, ordering, disposal):
- **Who am I** — an identity that is both a runtime key and a compile-time type.
- **Whom do I need** — the dependencies that provide my capabilities.
- **How long do I live** — which lifetime tier I belong to.
Classes talk only to interfaces and never care how an implementation is constructed.
## The four `LifecycleScope` tiers
Lifetimes form a tree, from longest to shortest:
```text
App process-wide, single global instance
└── Workspace one workspace handler (a materialized workspace root)
└── Session one session
└── Agent one agent
```
```ts
// src/app/scopes.ts — the business layer declares the tiers and their order;
// the DI kernel only knows opaque string kinds plus the declared topology.
export enum LifecycleScope {
App = 'app',
Workspace = 'workspace',
Session = 'session',
Agent = 'agent',
}
```
- Later in the topology = shorter life = closer to a leaf.
- "Singleton" means **one per scope**: `ILogService` is global once; each `Session` scope has its own `ISessionMetadata`.
- `kind` must advance along the declared topology in the parent→child direction.
### Visibility rule
A child scope sees its ancestors; a parent never sees its children. Resolution walks *up* the tree:
- ✅ An `Agent` service injects a `Session` or `App` service (found upward).
- ❌ An `App` service injects a `Session` service (the parent does not look down, and the child may not exist yet).
> **Short-lived may inject long-lived; never the reverse.** The tree structure enforces this — it is not a matter of discipline.
### Disposal order
Deterministic: **child scopes die first; within one scope, teardown runs in strict reverse registration order, one entry at a time.** The mechanism is the Ledger (`src/_base/lifecycle/`): ordered effect bookkeeping, dual-track (sync + async disposers), serial reverse-order teardown (never parallel), with the teardown reason (`'scope-close' | 'cascade' | 'unload'`) passed through to every disposer. `Disposable` / `DisposableStore` (`src/_base/di/lifecycle.ts`) delegate to it — "reverse construction order" is a Ledger property, not a container convention. Business code declares which tier it lives in and never disposes by hand.
## Dynamic DI: units and cascades
Registration is not the end of the story. Every unit a container tracks — static registrations and runtime `provide`s alike — lives in a small state machine owned by the scope's cascade engine (`src/_base/di/cascadeEngine.ts`, one per scope container, orchestrating tree-wide). Vocabulary you will meet in errors, tests, and the debug surface:
- **Unit states**`Pending → Activating → Active`, plus `Unloading` during teardown and a sticky `Failed`. A construction failure parks the unit in `Failed` with no auto-retry: resolving it rethrows its error; an explicit `update()` reloads it.
- **Waiting area** — a unit whose declared dependencies are missing sits `Pending` and auto-activates when they arrive, including cross-scope wake-up when an ancestor gains the token. An `ondemand` unit counts as available: consumers pull it transitively at materialization.
- **Cascade transaction** — every `provide` / `unprovide` / `update` runs as one tree-wide transaction: contagion set from the persistent dependency graph (instance edges, child→parent across scopes) → abort hook → global reverse-topo teardown → apply the change → waiting-area recheck fixpoint → history ring. Static bootstrap shares this path: scope creation submits the kind's whole registration batch as one `provideAll`, so registration order never matters.
## Import boundaries
There is no domain-layer numbering — a domain may import any other domain, guided by the dependency-direction judgment in design.md. The only mechanically enforced import boundaries are (`lint:imports`, `scripts/check-import-boundaries.mjs`):
- v2 never imports v1 (`@moonshot-ai/agent-core` or any subpath).
- The kosong subtree (`src/kosong/{contract,protocol,provider,model}`) keeps its strict internal order (`contract ← protocol ← provider/model`), purity bans (no SDKs in `contract`/`protocol`), and the `provider/bases` registration boundary.
## File-header comment convention
`packages/agent-core-v2/AGENTS.md` mandates a header-only comment style:
- **Header only.** Comments live solely in the top-of-file `/** */` block — never beside functions, methods, or statements. The code is the source of truth for *how*; the header states *what the module exposes and the responsibility it owns*.
- **Identity line first.** Start with `` `<domain>` domain — <one-line role>. `` Keep an existing `(cross-cutting)` label as-is. Write the role as a responsibility ("drives the turn lifecycle"), not a symbol list.
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md). State the same scope in the header so the two never drift.
- **Interface files** (`<name>.ts`) state the public contract + scope: which `IXxx` they define and what it is for.
- **Impl files** (`<name>Service.ts`) add collaborators + scope: list every imported cross-domain collaborator as a role ("persists records through `records`"); read scope from `registerScopedService(LifecycleScope.X, …)`.
- **Contribution files** (`<targetDomain>.ts` / `<what>.contrib.ts`) state what they register into the target domain (e.g. "registers the `log` config section into `config`").
- **Pure-function / `.types` / `.errors` files** state the responsibility only — they own no scoped state, so no scope line.
Impl file example (`sessionMetadataService.ts`):
```ts
/**
* `sessionMetadata` domain — `ISessionMetadata` implementation.
*
* Persists the session metadata document (`state.json`) through the `storage`
* access-pattern store (`IAtomicDocumentStore`), rooted at the `metaScope`
* namespace from `sessionContext`. Loads the existing document on
* construction (creating it on first run), and logs through `log`. Bound at
* Session scope.
*/
```
Contribution file example (`config.ts` inside `log/`):
```ts
/**
* `log` domain — registers the `log` config section into `config`.
*
* Owns the `log` section schema and its env overlay; imported for the
* registration side effect. Bound at App scope.
*/
```
## Red lines (this stage)
- Import via the `#/...` alias (mapped to `src/`); never reach into another domain's internals by relative path.
- Short-lived may inject long-lived; never the reverse.
- File-header comments describe role and scope only; never narrate implementation beside statements.

View file

@ -0,0 +1,213 @@
# Topic — Permission
The target design for the agent-core permission system. Read this when touching `permission`, `permissionMode`, `permissionRules`, or when adding a new permission dimension.
> **The permission system should be a composable, registrable chain of responsibility (a microkernel).** The kernel only runs the chain in order, first hit wins; concrete permission dimensions (policies) are contributed by their owning Domain Services through a registry; tools only declare standardized resource access (`accesses`) in `resolveExecution`, and generic dimensions consume that metadata.
>
> **The chain adjudicates risk only.** A policy node answers "how dangerous is this call, and may the user override that judgment?" — its `ask`/`deny` outcomes are always user-overridable. **Harness constraints are not permissions**: a mechanism that limits the agent for its own correctness (plan-mode write guard, AgentSwarm batch exclusivity, btw side-question fork, goal budget rejection) produces a hard deny with no ask channel and no per-call user exemption. Those live in their owning domains as `onBeforeExecuteTool` veto listeners that call `event.veto(...)` (precedent: `goalService.ts`'s budget/stale rejection). Product reviews (plan review, goal-start review) are likewise not permissions: the owning domain intercepts its tool with a cold `event.waitUntil(factory)` and drives the shared `IAgentToolApprovalService` round-trip itself, so the review only starts once no other listener vetoed the call.
>
> **Do not introduce Casbin** — the hard part here is *decision behavior* (continuations, side effects, RPC, state machines), not "match + scalar decision".
## 1. Problem definition
The permission system answers one question: **for each tool call, in the current agent and current mode — allow / deny / ask the user?** Three traits shape the architecture:
1. **Decisions carry behavior.** Returning `ask` is not an enum value — it is a workflow with an RPC round-trip, hooks, telemetry, state writes, and a continuation; returning `deny` may be the result of running an external hook.
2. **Heterogeneous policies.** Some check a tool-name set, some count same-batch `AgentSwarm` calls, some run a hook, some inspect the plan state machine — no uniform `(sub, obj, act)` shape.
3. **Multi-agent × multi-mode × external extension.** Different agents / modes need different permissions, and outsiders (org admins, plugins) must contribute rules or behavior in a decoupled way.
## 2. Current state (v1) at a glance
Code lives in `packages/agent-core/src/agent/permission/`.
- **Architecture: ordered chain of responsibility, first hit wins.** `PermissionManager` holds `PermissionPolicy[]`; evaluation iterates in order, the first non-`undefined` result wins.
- **`PermissionPolicyResult` is a behavior bundle, not a scalar:** `approve` (with `executionMetadata`), `deny` (with `message`), or `ask` (with `resolveApproval` / `resolveError` continuations).
- **11 dimensions, 19 policies**, hardcoded in `policies/index.ts#createPermissionDecisionPolicies()`. Order is a high-to-low safety cascade: external force → structural deny → state-machine deny → static deny → mode allow → session-memory allow → static ask → static allow → flow allow → sensitive-path ask → default allow → fallback ask.
- **Resource-access declaration:** tools declare accessed resources in `resolveExecution(input)` via `accesses` (`ToolAccesses`, currently `file` and `all`); generic dimensions read `context.execution.accesses`.
### v1 pain points the target design fixes
1. The chain is hardcoded — outsiders cannot contribute.
2. `mode` is an `if` inside each policy (`YoloModeApprove` / `AutoModeApprove` self-guard).
3. No per-agent chain entry point (only scattered `agent.type === 'sub'` checks).
4. No external extension point beyond the single `PreToolUse` hook slot.
## 3. Why not Casbin
- **`policy_effect` is unusable** — composition here is a fixed, intentionally hardcoded safety cascade; the real complexity lives in each policy's `evaluate` behavior, which a Casbin expression cannot absorb. Externally tunable safety knobs are already exposed via `mode` + allow/deny/ask rules.
- **Flexible priority is unusable** — there is no plugin injection point, no multi-subject/RBAC, and a fixed subject (agent/user), so priority collisions do not arise. Casbin's `(sub, obj, act)`, `g()`, and domains would idle.
- **Fundamental mismatch: decisions are not scalars.** `enforce()` maps a request to an effect; agent-core decisions are behavior bundles (continuations, side effects, synthesized results). Even if Casbin computed `ask`, the surrounding behavior would still need to be rewritten — Casbin would degrade to an enum generator.
- **When Casbin becomes worth it:** when the hard part is matching semantics itself — role inheritance, domain isolation, ABAC expressions, policies loaded from a DB. Not before.
## 4. Design-pattern placement
Permission orchestration is a layered combination, not a single pattern:
| Layer | Pattern | Role |
|---|---|---|
| Runtime decision | **Chain of Responsibility** | multiple candidates in order; first hit wins, rest short-circuit |
| Single handler | **Strategy** | each policy is an interchangeable "permission adjudication" algorithm |
| Assembly / external extension | **Plugin / Microkernel** | minimal kernel + explicit extension points + pluggable policies |
| Landing support | **Registry + Factory** | collect plugins; assemble the chain per `(agent, mode)` on demand |
Casbin = single Strategy + data-driven. This design = multiple Strategies + chain-of-responsibility composition. Behavior-heavy systems must choose the latter — behavior cannot be flattened into data rows.
## 5. Target design
### 5.1 Core principles
1. **The chain encodes "permission dimensions", not "tools".** Adding a tool does not lengthen the chain; only adding a dimension adds a node.
2. **Two contribution paths:** high-frequency trivial specifics go through the **data path** (rules); low-frequency new dimensions with behavior go through the **code path** (policies).
3. **Guard/review off-chain, risk on-chain:** harness constraints and product reviews ship with their owning domain as `onBeforeExecuteTool` veto listeners (§5.4); risk dimensions contributed by a domain self-register as chain policies in DI, mirroring v2's "domain self-registers tools".
4. **Tools declare resources; generic dimensions consume them:** bash/write/read only declare `accesses`; file/security dimensions judge centrally.
### 5.2 Core abstractions
```ts
type Phase =
| 'guard' | 'user-deny' | 'mode' | 'session'
| 'user-ask' | 'default' | 'fallback';
interface PermissionPolicyEntry {
name: string;
phase: Phase;
modes?: PermissionMode[]; // declare which modes this applies in (no more in-evaluate if)
agentTypes?: AgentType[];
factory: (accessor: ServicesAccessor) => PermissionPolicy;
}
// App scope — collects every domain's registration
interface IPermissionPolicyRegistry {
register(entry: PermissionPolicyEntry): IDisposable;
list(): readonly PermissionPolicyEntry[];
}
```
`PermissionPolicyService` (Agent scope) changes from a hardcoded list to "assemble by `(agent, mode)`":
```ts
this.policies = registry.list()
.filter(e => !e.modes || e.modes.includes(mode))
.filter(e => !e.agentTypes || e.agentTypes.includes(agentType))
.sort(byPhaseThenRegistrationOrder)
.map(e => e.factory(accessor));
```
Key points:
- `modes` / `agentTypes` are **declarations** — they lift the `if (mode !== 'yolo') return` out of `YoloModeApprove` into metadata.
- `factory`, not `instance`: a node may depend on agent-scoped services (mode, rules) and must be instantiated in the Agent scope — symmetric to `IToolDefinitionRegistry` (App) storing factories and `IToolService` (Agent) instantiating tools.
- **Different `(agent, mode)` produce differently-shaped chains** — under yolo the ask/fallback phases are physically filtered out.
### 5.3 Two contribution paths
| What is being added | Path | Chain length |
|---|---|---|
| New tool, new org rule, new user preference ("deny `Bash(curl *)`") | **Data path**: add a `PermissionRule` to an existing node | unchanged |
| New cross-cutting behavior (custom approval UI, audit log, new mode) | **Code path**: register a new policy node | +1 |
Most growth goes through the data path — node count is bounded by "kinds of behavior"; rule count grows with specifics (rule matching is a cheap Set/glob).
### 5.4 Domain dimensions: guard/review via the executor veto event, policy registration for risk
**Harness constraints and product reviews no longer live on the chain.** A domain that owns one registers an `onBeforeExecuteTool` veto listener and adjudicates through the event:
```ts
// src/plan/planService.ts — constructor
constructor(@IAgentToolExecutorService executor, ...) {
executor.onBeforeExecuteTool((event) => this.guardToolExecution(event));
}
```
- The veto event carries no id and no ordering contract. Listeners answer with `event.veto(result)` (first one wins, ends adjudication), `event.allow()` (final pass, ends everything including the permission gate's own listener), `event.pass(metadata)` (pass with an `executionMetadata` trace, ends nothing), or `event.waitUntil(factory)` (defer to a cold factory).
- **Guard** (hard deny): call `event.veto(denyToolExecution(toolApproval.formatDenyMessage(...)))`. An immediate veto suppresses every pending `waitUntil` factory, so a deny can never be preceded by someone else's approval prompt.
- **Review** (product approval): intercept the tool with `event.waitUntil(() => ...requestToolApproval(event, ask, origin))`. The factory is cold — the executor only invokes it after every listener ran without a veto or an allow, so the review's Interaction starts only once the call is otherwise clear to proceed; abstain (no statement) for every case you do not review so user rules still apply.
- **Plain allow**: do NOT `allow()` casually — prefer putting the tool in `default-tool-approve`'s whitelist so user deny/ask rules keep their precedence; reserve `allow()` for cases like the plan-file write guard that must bypass even the permission chain.
**Risk dimensions contributed by a domain still go through the chain** (the registry path below): a domain whose state changes the *risk* verdict registers its policy via `IPermissionPolicyRegistry`, mirroring v2's "domain self-registers tools". A complex domain may register a single **composite** node externally and run a small internal chain, hiding its internal order from the global chain.
### 5.5 Tools declare resources at runtime (`resolveExecution` / `accesses`)
In `resolveExecution(input)`, before execution, declare accessed resources with the `ToolAccesses.*` builders:
```ts
resolveExecution(args: WriteInput): ToolExecution {
const path = resolvePathAccessPath(args.path, { kaos, workspace, operation: 'write' });
return {
accesses: ToolAccesses.writeFile(path), // declares: write this file
approvalRule: literalRulePattern(this.name, path),
matchesRule: (ruleArgs) => matchesPathRuleSubject(ruleArgs, path, ...),
execute: () => this.execution(args, path),
};
}
```
Current resource types:
```ts
type ToolResourceAccess =
| { kind: 'file'; operation: 'read'|'write'|'readwrite'|'search'; path: string; recursive?: boolean }
| { kind: 'all' }; // non-enumerable side effects (pessimistic, globally exclusive)
```
Two complementary channels:
- **Enumerable resources** (write/read/edit/grep/glob) → use `accesses`; generic file dimensions cover them automatically.
- **Non-enumerable resources** (bash running arbitrary commands) → do not declare `accesses`; use the `matchesRule` DSL (e.g. `Bash(rm *)` globs by command string).
**kaos's role:** kaos is the execution-environment abstraction (fs/process/pathClass) used by the file dimension for path normalization and judgment — it is **not** the permission-dimension abstraction itself. Permission semantics live one layer above kaos, at "file access".
**v2 evolution:** extend the `ToolResourceAccess` union so non-file resources can be declared structurally:
```ts
type ToolResourceAccess =
| { kind: 'file'; operation: FileOp; path: string; recursive?: boolean }
| { kind: 'network'; operation: 'connect'; host: string }
| { kind: 'shell'; command: string }
| { kind: 'datastore'; operation: 'read'|'write'; table: string }
| { kind: 'all' };
```
Each new resource kind can pair with a generic dimension that consumes it; tools always only **declare**.
### 5.6 Dimension ownership
| Dimension | Owner | Type |
|---|---|---|
| external hook veto | `externalHooks` domain | generic |
| tool-batch exclusivity | `swarm` domain — `onBeforeExecuteTool` veto listener | harness constraint (off-chain) |
| plan-mode write guard | `plan` domain — `onBeforeExecuteTool` veto listener | harness constraint (off-chain) |
| plan review | `plan` domain — same listener's `waitUntil` + `toolApproval` | product review (off-chain) |
| goal-start review | `goal` domain — veto listener's `waitUntil` + `toolApproval` | product review (off-chain) |
| goal budget / stale rejection | `goal` domain — `onBeforeExecuteTool` veto listener | harness constraint (off-chain) |
| btw tool disablement | `btw` domain — veto listener on the fork | harness constraint (off-chain) |
| runtime-mode posture (auto/yolo) | `permissionMode` domain (chain nodes, pending the level×routing split) | generic |
| static config rules | `permissionRules` domain | generic (data path) |
| session approval memory | `permissionRules` domain | generic |
| sensitive / special paths | generic "file-access/security" dimension | generic (consumes `accesses`) |
| tool intrinsic risk | core permission (`default-tool-approve`) | generic (consumes tool declarations) |
| workspace write trust | generic "file-access/security" dimension | generic (consumes `accesses`) |
| fallback | core permission | generic |
| approval round-trip | `toolApproval` domain — shared by gate asks and domain reviews | infrastructure |
Pattern: **harness constraints and reviews ship with their owning domain as `onBeforeExecuteTool` veto listeners; risk dimensions ship as chain policies (self-registered once the registry lands); generic dimensions register centrally and apply across tools via the declared `accesses`.**
## 6. Evolution path
Incremental, not big-bang:
1. ~~**Sink domain dimensions.**~~ **Done** — plan guard/review, goal-start review, swarm batch exclusivity, and btw deny-all moved out of the chain into their owning domains as `onBeforeExecuteTool` veto listeners (immediate `veto` / `allow` / `pass` statements plus cold `waitUntil` factories for approval round-trips); the shared approval round-trip was extracted to `IAgentToolApprovalService`; `registerPolicy` was removed (btw was its only production user). The chain now holds 12 risk-adjudication nodes only.
2. **Level × routing split.** Separate "risk level" (read-only / read-write / yolo posture — what `yolo-mode-approve` really is) from "interaction routing" (what `auto-mode-approve` / `auto-mode-ask-user-question-deny` really are: route permission asks and reviews without the user). The routing layer lands on the `session/approval` broker; the three remaining mode policies leave the chain here.
3. **Registry + Composer.** Replace the hardcoded `new`s in `PermissionPolicyService` with reads from `IPermissionPolicyRegistry`; lift mode guards into `modes` metadata. Chain shape becomes selectable per `(agent, mode)` and externally extensible.
4. **(On demand) extend resource types.** When non-file resources (network/DB/shell) need structural dimensions, extend the `ToolResourceAccess` union.
5. **(On demand) swap the matching kernel for Casbin.** Only when external rules genuinely need RBAC/ABAC semantics, swap the data-path rule-matching kernel for Casbin. Not before.
## Red lines (this topic)
- Do not introduce Casbin — decisions are behavior bundles, not scalar effects.
- The chain adjudicates risk only. A node whose deny/ask the user cannot per-call exempt is a harness constraint: implement it as an `onBeforeExecuteTool` veto listener in the owning domain (`event.veto(...)` / `event.allow()`), never as a chain policy.
- Product reviews (plan/goal) are not permissions either: the owning domain intercepts its tool with a cold `event.waitUntil(factory)` and drives `IAgentToolApprovalService` itself; the gate only handles chain asks.
- The chain encodes dimensions, not tools: a new tool must not lengthen the chain.
- New specifics go through the data path (rules); only new risk behavior goes through the code path (a policy node).
- Tools only declare `accesses`; generic dimensions consume them. kaos is the execution environment, not the permission abstraction.
- Use `factory` (Agent-scope instantiation), not `instance`, for registered policies.

View file

@ -0,0 +1,204 @@
# Topic — Persistence layering
How business code persists data in `agent-core-v2`: the three-layer model (`Store → Storage → backend`), the naming rules for each layer, and how to decide which layer a domain should depend on. Read this before adding any persistence to a domain.
A domain `I{Domain}EntityService` is a business facade over these layers, not a replacement for them. Before naming or bundling EntityServices by `session` / `agent` / `turn`, read [domain-boundaries.md](domain-boundaries.md).
## The three-layer model
Persistence is split into three layers, each hiding one kind of change:
```text
Business Service
│ inject
┌────────────────────────────────────────┐
│ Store (semantic layer) │ ← access-pattern facade
│ IAppendLogStore / IAtomicDocumentStore│ append-log / atomic-doc / blob
└────────────────────────────────────────┘
│ inject
┌────────────────────────────────────────┐
│ Storage (byte layer) │ ← byte primitives
│ IFileSystemStorageService │ read/write/append/list/delete
└────────────────────────────────────────┘
│ implements
┌────────────────────────────────────────┐
│ Backend (deployment-specific) │ ← File / Postgres / Redis / S3
│ FileStorageService / PostgresStorage │
└────────────────────────────────────────┘
│ uses
┌────────────────────────────────────────┐
│ Platform primitives │ ← hostFs / dbClient / redisClient
└────────────────────────────────────────┘
```
Each layer hides exactly one concern:
| Layer | Hides | Business code sees |
|---|---|---|
| **Store** | how an access pattern works (append-log reads, atomic-doc serialization) | "append this record" / "save this document" |
| **Storage** | byte primitives (atomic write, ordered append, prefix list) | `read/write/append/list/delete` over `(scope, key)` |
| **Backend** | deployment environment (file vs DB vs Redis vs S3) | nothing — chosen at the composition root |
## The one-sentence rule
> **Business code expresses *what* to store or fetch, never *how* to store it.**
If business code contains any "how to persist" detail, it has punched through the layer it should depend on:
| Business code contains | It has punched through | Depend on instead |
|---|---|---|
| `INSERT INTO …` / `SELECT …` | Storage + backend | a Store |
| file paths / `rename` / `fsync` | Storage | Storage or a Store |
| `JSON.parse` / `JSON.stringify` | Store (serialization) | `IAtomicDocumentStore` |
| append offsets / sequential cursors | Store (log semantics) | `IAppendLogStore` |
| `hash(data)` used as a key | Store (blob semantics) | `IBlobStore` |
| `pathe.join / relative / basename` on `homeDir` etc. | Bootstrap (path layout) | `IBootstrapService.scope(...)` / scope contexts |
| only `read/write/list/delete` on bytes | nothing — this is the byte layer | `IFileSystemStorageService` directly ✅ |
## Where scopes come from — `IBootstrapService` and scope contexts
Business code **never assembles scope strings from paths**. Scope strings come from three places:
1. **`IBootstrapService.scope(name)`** — well-known top-level scopes (`'config' | 'sessions' | 'blobs' | 'store' | 'logs' | 'cache' | 'credentials'`). App-scope, deployment-agnostic contract.
2. **`ISessionContext.scope(subKey?)`** — persistence scope rooted at the current session; `scope('agents/main')` etc.
3. **`IAgentScopeContext.scope(subKey?)`** — persistence scope rooted at the current agent; `scope('cron')`, `scope('blobs')` etc.
The bootstrap layer decides how each semantic scope maps to concrete addressing. In the file deployment, `FileBootstrapService` reads a `ResolvedEnvironment` (the paths bag) and returns homeDir-relative scopes; a server deployment could bind a different `IBootstrapService` implementation that maps `'sessions'` to a DB table without any business change.
```ts
// ❌ Wrong — path arithmetic on homeDir/sessionDir leaks the file layout
const scope = relative(bootstrap.homeDir, join(session.sessionDir, 'agents', agentId, 'cron'));
// ✅ Right — the agent already knows its own scope root
const scope = agentCtx.scope('cron');
```
Absolute paths (`sessionDir`, `agentHomedir`) are still available on `IBootstrapService` for the very small number of legacy APIs that expose on-disk paths (session log rotation, background task tail file). Prefer scope strings; ask before adding a new absolute-path caller.
## Which layer to depend on — decision tree
```text
Need to persist
├─ read-whole / write-whole, JSON-serializable?
│ └─ IAtomicDocumentStore
├─ append-only writes / sequential reads, independent records?
│ └─ IAppendLogStore
├─ large object, addressed by content hash?
│ └─ IBlobStore
├─ custom byte layout (index / cache / binary) that read/write/list cover?
│ └─ IFileSystemStorageService directly
├─ new, reusable access semantics (multi-field query / time-range / graph)?
│ └─ add a new Store; business depends on the Store
└─ business-specific, trivial, one or two lines?
└─ IFileSystemStorageService directly; if it grows, extract a private Store
```
## Naming — Store by access pattern, not by business
A Store abstracts an **access pattern**, not a business data type. Name it after the pattern so its reusability is obvious from the name.
| Access pattern | Store name | Backend examples |
|---|---|---|
| append-log (append / sequential read) | `IAppendLogStore` | `FileAppendLogStore` / `PostgresAppendLogStore` |
| atomic-document (read/write whole) | `IAtomicDocumentStore` | `FileDocumentStore` / `RedisDocumentStore` |
| blob (hash-addressed large object) | `IBlobStore` | `FileBlobStore` / `S3BlobStore` |
**Do not name a generic Store after a business concept.** `IRecordStore` / `IConfigStore` make a reusable access pattern look like a private store for one feature. Any domain that needs an append-log uses `IAppendLogStore`; any domain that needs an atomic document uses `IAtomicDocumentStore`.
**Exception — business-specific Stores are named after the business.** When a Store captures one domain's unique query semantics (not a generic access pattern), name it after the domain:
```text
ISessionIndex query / enumerate sessions by workspace ← business-specific
```
Test: is the Store's semantics a *generic access pattern* (append-log / atomic-doc / blob) or *one domain's unique query*? Generic → name by pattern; unique → name by domain.
## Storage — a filesystem-specific byte layer
The byte layer is a single `IFileSystemStorageService` interface (read / readStream / write / append / list / delete / watch / flush / close). As the name says, it is **filesystem-specific**: it exposes the two irreducible durable primitives a local filesystem implements optimally — atomic whole-value replacement (`write`, via tmp + rename) and ordered durable extension (`append`, via `open('a')`). The node-fs Store backends (`AppendLogStore`, `JsonAtomicDocumentStore`, `BlobStoreService`) are built on it.
```ts
export interface IFileSystemStorageService {
read(scope: string, key: string): Promise<Uint8Array | undefined>;
readStream(scope: string, key: string): AsyncIterable<Uint8Array>;
write(scope: string, key: string, data: Uint8Array, options?: { atomic?: boolean }): Promise<void>;
append(scope: string, key: string, data: Uint8Array, options?: { durable?: boolean }): Promise<void>;
list(scope: string, prefix?: string): Promise<readonly string[]>;
delete(scope: string, key: string): Promise<void>;
watch?(scope: string, key: string): Event<void>;
flush(): Promise<void>;
close(): Promise<void>;
}
```
Two backends implement it today, both bound at the composition root:
```ts
// Production — local filesystem rooted at homeDir
collection.set(IFileSystemStorageService, new FileStorageService(homeDir));
// Tests — in-memory backend seeded by the test harness
collection.set(IFileSystemStorageService, new InMemoryStorageService());
```
**Non-filesystem backends (Postgres, S3, Redis) do not implement this interface.** Atomic-rename and byte-append have no native equivalent in those stores, so they implement the **Store** interfaces directly via their own clients instead:
```ts
// Server profile — append-logs on Postgres, atomic documents on Redis.
// Each Store is backed by a native client; IFileSystemStorageService is not involved.
collection.set(IAppendLogStore, new PostgresAppendLogStore(db, 'records'));
collection.set(IAtomicDocumentStore, new RedisDocumentStore(redis, 'config'));
```
Use the `scope` parameter to express **business namespace** within a backend. Do not overload `scope` to route backends — bind a different Store implementation at the composition root instead.
## Store `acquire(scope, key)` — flush-on-dispose handle
Stores that buffer writes expose an `acquire(scope, key)` handle so a business can flush them on disposal:
```ts
export interface IAppendLogStore {
// …
/**
* Acquire a disposable handle for `(scope, key)`. Register it with your
* `Disposable` (via `this._register(...)`); when you are disposed, pending
* appends for that log are flushed. The shared store itself is not disposed.
*/
acquire(scope: string, key: string): IDisposable;
}
```
`IAppendLogStore.acquire` flushes the log's pending appends on dispose — it exists because `append` is fire-and-forget. `IAtomicDocumentStore.acquire` is a no-op today (atomic documents are durable on write) and exists for interface symmetry. Businesses that do not need flush-on-dispose simply do not call `acquire`.
## When the byte layer does not apply
`IFileSystemStorageService` covers only the local-filesystem byte primitives. It is not a universal storage abstraction:
- **Non-filesystem backends** (Postgres / S3 / Redis) implement the **Store** interfaces directly via native clients — they never implement `IFileSystemStorageService`.
- **Blobs** are a Store-level interface (`IBlobStore`) with their own backends; the node-fs `BlobStoreService` sits on `IFileSystemStorageService`, but an `S3BlobStore` would not.
- **A backend has a fast primitive the Store interface cannot express** (e.g. Postgres `COPY`) → as an exception, extend that backend's Store implementation directly. This is an exception, not the default.
## Platform primitives are deployment-coupled, not core abstractions
`hostFs` (local filesystem) is a **platform primitive** used only by local backends (`FileStorageService`, `LocalFileSystemBackend`, `LocalSkillCatalog`, `HostFolderBrowser`). It is **not** a core abstraction and must not appear in business-domain dependency graphs. A server deployment swaps those backends for DB / S3 implementations and never registers `hostFs`.
## Red lines (this topic)
- Business code never contains "how to persist" details (serialization / paths / SQL / append offsets) — if it does, drop a layer.
- Business code never assembles scope strings from paths (`pathe.join / relative / basename` on `homeDir` / `sessionDir` / …). Use `IBootstrapService.scope(name)` for well-known scopes, `ISessionContext.scope(subKey?)` for session-rooted scopes, and `IAgentScopeContext.scope(subKey?)` for agent-rooted scopes.
- Name generic Stores by access pattern (`IAppendLogStore` / `IAtomicDocumentStore` / `IBlobStore`), never by business concept (`IRecordStore` / `IConfigStore`).
- Business-specific Stores (unique query semantics) are named after the domain (`ISessionIndex`).
- `IFileSystemStorageService` is the filesystem byte-layer interface; non-filesystem backends implement the **Store** interfaces directly. Route backends by binding a different Store implementation at the composition root, not by overloading `scope`.
- `hostFs` is a local-only platform primitive; business domains must not import `node:fs` or `hostFs` directly.
- Only the file-backed bootstrap (`FileBootstrapService`) and file backends import `pathe`; business domains do not.
- Do not create a pass-through `Store` that only forwards `read/write` — a Store must hide a real access-pattern concern, or it is noise; use `IFileSystemStorageService` directly instead.

View file

@ -0,0 +1,253 @@
# Subskill — Server align (expose `agent-core-v2` over `server-v2`)
Wire a v2 domain into `packages/kap-server`, and — when the endpoint is part of the established `/api/v1` wire contract — keep the wire shape **byte-for-byte compatible** with what released v1 clients expect. This is the server-side counterpart of [align.md](align.md): `align.md` ports v1 *business logic* into v2; this file exposes the v2 result over HTTP / WS, reusing the v1 wire contract where it already exists.
Use this when the task is "expose the new v2 Service on the server", "add a `/sessions/:sid/...` route to the `/api/v1` surface", or "keep server-v2 speaking the same `/api/v1` contract released clients rely on".
## The one-paragraph mental model
`server-v2` serves **two HTTP surfaces** off the same `agent-core-v2` scope tree:
- **`/api/v2/:sa`** — the native v2 RPC surface, driven by the `actionMap` allowlist (`packages/kap-server/src/transport/actionMap.ts`). One `resource:action` segment maps to one `Service.method`. New v2-native capabilities land here. See [edge-exposure.md](edge-exposure.md).
- **`/api/v1/...`** — the v1-compatible surface, hand-written routes in `packages/kap-server/src/routes/*.ts` that **implement the established v1 wire contract path-for-path and schema-for-schema**, mounted by `registerApiV1Routes.ts`. This surface IS the v1 contract now (the legacy v1 server is gone); it exists so existing v1 clients keep working against server-v2 unchanged.
The two surfaces can point at **different Services** for the same feature. v2's native `IAgentPromptService` serves `/api/v2`; a v1-shaped `IAgentPromptService` serves `/api/v1`. Keeping them separate is what lets v2's domain design stay clean while the wire stays compatible.
## Decision: which surface?
```text
Is the endpoint part of the established /api/v1 wire contract (protocol schema
+ released-client expectation)?
├─ YES → /api/v1 mirror route (this file, §schema-fidelity + §legacy-service).
│ Reuse the protocol schema; add a LegacyService if v2 semantics diverge.
└─ NO → /api/v2 native action (edge-exposure.md).
Add to actionMap, wrapping in a facade if the method fails §2 there.
```
A feature often needs **both**: the v1 mirror so old clients keep working, and the v2 action so new clients get the cleaner shape. Do them as two routes / two action-map entries over the same scope tree.
## The server-align workflow
```text
Pick surface → Read the v1 route (if any) → Reuse / add the protocol schema
→ Choose native Service vs LegacyService → Wire the route / actionMap entry
→ Map errors → Test against the v1 wire shape → Verify
```
### 1. Pick the surface
Apply the decision above. For a v1-matched endpoint, the **spec** is the protocol schema plus the existing mirror routes:
- `packages/kap-server/src/protocol/rest-<resource>.ts` — the wire schema you must match.
- `packages/kap-server/src/routes/<resource>.ts` — the file you are writing (create it if missing); sibling route files show the conventions.
The protocol schema is the source of truth. Do not re-derive the wire shape from memory or from the v2 domain model.
### 2. Reuse (or add) the protocol schema
The wire schema lives in **`packages/kap-server/src/protocol`** under `rest-<resource>.ts` (e.g. `promptSubmissionSchema`, `promptListResponseSchema`, `configResponseSchema`) — or in the owning `agent-core-v2` domain contract when the engine's service speaks the shape. Every `/api/v1` route in `packages/kap-server` imports from it — that single import is what guarantees the server speaks the same shape released clients expect.
Actions:
- **Schema already in protocol** → import it in the server-v2 route and use it in `defineRoute` (`body`, `success.data`, error `dataSchema` / `detailsSchema`). Do **not** re-declare the schema inline in server-v2.
- **Schema missing** → add it to `packages/kap-server/src/protocol/rest-<resource>.ts` first (or to the owning v2 domain contract if its service speaks the shape), then consume it from the route. The shared schema is the source of truth; server-v2 never re-declares a v1 wire schema inline.
- **Schema exists but only v1 uses it** → keep it in `packages/kap-server/src/protocol` and import it into server-v2; do not fork a copy.
#### Schema-fidelity rule (the hard rule)
For a `/api/v1` endpoint, the request and response schemas **must be the established protocol schema** (or a strict superset):
- ✅ **Adding** an optional field is allowed (`field: z.string().optional()`). Old clients ignore it; new clients may send it.
- ❌ **Renaming** a field, **changing** its type, **tightening** its validation, or **changing its meaning** is a wire break — do not do it in a mirror route. If the v2 domain genuinely needs a different shape, that shape belongs on `/api/v2`, not on the `/api/v1` mirror.
- ❌ Re-declaring the schema inline in server-v2 (even if it "looks identical") is forbidden — it drifts. One schema, one home: the owning `agent-core-v2` domain contract or `packages/kap-server/src/protocol`.
Self-check: "would a released v1 client get a byte-identical envelope from `packages/kap-server` for this request?" If you cannot answer yes from the shared schema, the route is wrong.
### 3. Choose native Service vs LegacyService
Resolve the v2 Service that will back the route. Two cases:
**Case A — the v2 native Service already matches the v1 contract.** Use it directly. Most data/command Services (`IConfigService`, `IWorkspaceService`, `IApprovalService`, `IQuestionService`, `IFileStore`, …) land here: the route is a thin adapter that resolves the scope, calls the method, and wraps the result. Examples: `routes/config.ts`, `routes/messages.ts`, `routes/questions.ts`, `routes/files.ts`.
**Case B — the v1 contract needs behavior that would distort the v2 domain.** Introduce a **`*LegacyService`** — an edge adapter that implements the v1 contract **on top of** the v2 native Service, leaving the native Service untouched. The v2 native Service keeps serving `/api/v2`; the LegacyService serves `/api/v1`.
Reach for a LegacyService when **any** hold:
- The v1 endpoint carries state the v2 domain deliberately dropped (e.g. a FIFO queue, a `prompt_id`, idempotent `abort`/`steer`, auto-start-next).
- The v1 method returns a handle/stream that v2 wraps differently, and the v1 clients expect the old envelope shape.
- Matching v1 would force a `Map<sessionId, …>`-at-`App` anti-pattern or a scope/domain-direction violation into the native Service (see [align.md](align.md) red lines).
- The native Service's error set / return type would have to grow v1-only branches.
Do **not** put v1 quirks into the native v2 Service "to keep the route simple". That is the conflict this rule exists to prevent: the native Service serves the v2 architecture; the LegacyService serves the wire contract.
#### LegacyService recipe
A LegacyService is a normal v2 Service (service-authoring.md) with one extra convention: its contract is shaped by the **protocol** types, not by the v2 domain model.
```text
packages/agent-core-v2/src/<domain>Legacy/
├── <domain>Legacy.ts ← contract: protocol-typed interface + decorator
├── <domain>LegacyService.ts ← impl: delegates to the native v2 Service(s)
└── errors.ts ← v1-compatible error codes (KimiError codes)
```
Skeleton (matches `prompt/`):
```ts
// prompt.ts — contract shaped by the v1 wire schema (kap-server/src/protocol)
import type { PromptSubmitResult, PromptSubmission } from '../../protocol/rest-prompt';
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface IAgentPromptService {
readonly _serviceBrand: undefined;
submit(body: PromptSubmission): Promise<PromptSubmitResult>;
// ...the rest of the v1 contract, typed by protocol
}
export const IAgentPromptService: ServiceIdentifier<IAgentPromptService> =
createDecorator<IAgentPromptService>('agentPromptLegacyService');
```
```ts
// promptService.ts — impl delegates to the native v2 Service
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
constructor(@IAgentPromptService private readonly prompt: IAgentPromptService /*, ... */) {}
// submit() builds v2-native input, calls the native Service, projects the result
// back into the protocol PromptSubmitResult.
registerScopedService(
LifecycleScope.Agent, // scope = the lifetime of the legacy state
IAgentPromptService,
AgentPromptLegacyService,
ScopeActivation.OnDemand,
'prompt',
);
```
Conventions:
- **Name** the domain `<domain>Legacy` and the interface with the scope prefix, `I<Scope><Domain>LegacyService` (e.g. `prompt` / `IAgentPromptService`), per service-authoring.md.
- **Header comment** must say it is an `edge adapter` and name both the v1 contract it implements and the native v2 Service it leaves untouched (see `prompt.ts`).
- **Scope** = the lifetime of the *legacy* state it holds (the `prompt` queue is per-agent → `LifecycleScope.Agent`). Apply [orient.md](orient.md) / [design.md](design.md) normally — a LegacyService is not exempt from scope rules.
- **Delegate, do not duplicate** business logic. The LegacyService translates the v1 contract into native-Service calls and translates results back; the real work stays in the native Service.
- **Contract types come from the v1 wire schema homes** (the owning v2 domain contract or `kap-server/src/protocol`), so the interface cannot drift from the wire shape.
### 4. Wire the route / actionMap entry
**For `/api/v1` (mirror):** add a route file under `packages/kap-server/src/routes/<resource>.ts` using `defineRoute`, then register it in `registerApiV1Routes.ts`. Resolve the scope from the URL (`session_id` → Session scope, agent → Agent scope via `IAgentLifecycleService.getHandle`), then `accessor.get(IX)` the native or Legacy Service. Match the established verbs, paths (`:sid` / `{session_id}`), and `parseActionSuffix` actions (`:steer`, `:abort`) exactly — sibling routes under `packages/kap-server/src/routes/` are the reference.
```ts
const route = defineRoute(
{
method: 'POST',
path: '/sessions/{session_id}/prompts',
body: promptSubmissionSchema, // ← from kap-server/src/protocol
params: sessionIdParamSchema,
success: { data: promptSubmitResultSchema }, // ← from kap-server/src/protocol
errors: {
[ErrorCode.SESSION_NOT_FOUND]: {},
[ErrorCode.SESSION_BUSY]: {},
[ErrorCode.PROMPT_ALREADY_COMPLETED]: { dataSchema: z.object({ aborted: z.literal(false) }) },
},
operationId: 'submitPrompt',
tags: ['prompts'],
},
async (req, reply) => {
try {
const result = await resolveLegacy(core, req.params.session_id).submit(req.body);
reply.send(okEnvelope(result, req.id));
} catch (error) {
sendMappedError(reply, req.id, error);
}
},
);
app.post(route.path, route.options, route.handler);
```
**For `/api/v2` (native):** add a `resource:action` entry to `actionMap` ([edge-exposure.md](edge-exposure.md) §3). If the method fails the direct-exposure rules (returns a handle / stream / bytes, takes a live object), add a wire-safe orchestration method to the owning domain Service first — as `prompts:submit` maps to `IAgentPromptService.submit`, which settles `{turn_id}` engine-side instead of returning the live `PromptHandle`.
### 5. Map errors
The route translates domain `KimiError` codes into protocol `ErrorCode` numbers. Two registries must stay in sync:
- **Domain code** — register in `agent-core-v2/src/errors.ts` (`ErrorCodes`) and throw from the Service (errors.md). Co-located domain errors go in `<domain>Legacy/errors.ts` (e.g. `prompt.not_found`, `session.busy`).
- **Wire code** — register the matching number in `packages/kap-server/src/protocol/error-codes.ts` and reference it in the route's `errors` map and `sendMappedError`.
```ts
function sendMappedError(reply, requestId, err) {
if (isKimiError(err)) {
switch (err.code) {
case 'session.not_found':
case 'agent.not_found':
return reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
case 'prompt.not_found':
return reply.send(errEnvelope(ErrorCode.PROMPT_NOT_FOUND, err.message, requestId));
// ...
}
}
return reply.send(errEnvelope(ErrorCode.INTERNAL_ERROR, String(err), requestId));
}
```
Match the v1 route's status codes and idempotent-conflict envelopes (e.g. `prompt.already_completed``40903` with `{ data: { aborted: false } }`). The error envelope is part of the wire contract — it is covered by the same schema-fidelity rule.
### 6. Test against the v1 wire shape
Add a `packages/kap-server/test/<resource>.test.ts` that boots the server and hits the route. Assert on the **envelope + protocol shape**, not on the v2 domain internals:
- success envelope `{ code: 0, data: <protocol shape>, request_id }`;
- each declared error envelope `{ code: <ErrorCode>, msg, data, request_id }`;
- the fields v1 clients read are present with the same names/types.
Where the route mirrors v1, the test is the regression guard for the schema-fidelity rule: if someone drifts the protocol schema or the projection, this test breaks.
### 7. Verify
- `pnpm -C packages/kap-server test` — server routes green.
- `pnpm -C packages/kap-server test` — server routes green (incl. any wire-schema guards).
- `pnpm -C packages/agent-core-v2 test` — native + Legacy Service tests green.
- `pnpm -C packages/agent-core-v2 run lint:imports` — the import boundaries (v1 ban, kosong subtree) still hold for a LegacyService.
- `pnpm -C packages/klient test` (optionally with `KIMI_SERVER_URL` for the live legacy suites) when a v1 parity scenario exists.
## Worked example — porting v1 `/sessions/:sid/prompts`
This is the reference alignment (commits `feat(server-v2): port v1 /sessions/:sid/prompts routes`, `feat(server-v2): return turn ids for prompt actions`). It shows all three decisions at once.
**The mismatch.** v1 `IPromptService` is a per-agent *scheduler*: it owns a FIFO queue, assigns `prompt_id`s, supports `steer`/`abort`, and auto-starts the next queued prompt when a turn settles. v2's native `IAgentPromptService` is a *turn driver*: a submission *is* a turn, there is no queue and no `prompt_id`. Forcing the queue into the v2 native Service would distort the v2 domain.
**The split.**
- `/api/v2` keeps the native shape — `prompts:submit` / `steer` / `undo` / `clear` / `cancel` map to the domain Services (`IAgentPromptService.submit` / `submitSteer`, `IAgentConversationUndoService.undo`, `IAgentLoopService.cancelFromUser`) in `actionMap`.
- `/api/v1` gets an `AgentPromptLegacyService` (`prompt/`, `LifecycleScope.Agent`) that re-implements the v1 scheduler — queue, `prompt_id`, steer/abort, auto-start-next — **on top of** the native `IAgentPromptService`. The `/api/v1` routes consume the LegacyService.
**The schema.** Both surfaces import `promptSubmissionSchema` / `promptSubmitResultSchema` / `promptListResponseSchema` / `promptSteerRequestSchema` / `promptSteerResultSchema` / `promptAbortResponseSchema` from the shared v1 wire schemas (see `packages/kap-server/src/protocol`). The `/api/v1` and `/api/v2` routes are therefore compatible with released clients by construction; the LegacyService projects v2 turn results back into those protocol shapes.
**The errors.** v1 codes (`prompt.not_found`, `session.busy`, `prompt.already_completed`) are registered in `agent-core-v2` (`prompt/errors.ts`) and in `packages/kap-server/src/protocol` (`error-codes.ts`), then mapped in the route's `sendMappedError` — including the idempotent `prompt.already_completed``40903 { data: { aborted: false } }`.
**The lesson.** When the v1 contract and the v2 domain disagree, add an adapter (LegacyService) at the edge; do not let the wire contract leak into the native domain. The two surfaces share the protocol schema but not the Service.
## Migration checklist
Before submitting a server-align change:
- [ ] Surface chosen deliberately: `/api/v1` mirror for a v1-matched endpoint, `/api/v2` for a new native capability (both if needed).
- [ ] For a `/api/v1` mirror, the route matches the established v1 contract (protocol schema + sibling routes) path-for-path, verb-for-verb, action-for-action.
- [ ] Request and response schemas come from their owning home (the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`); no inline re-declaration in server-v2.
- [ ] Existing schema fields are unchanged in name, type, and semantics; only optional fields added (if any).
- [ ] Native v2 Service left clean; v1-only behavior isolated in a `<domain>Legacy` / `I<Domain>LegacyService` edge adapter when the semantics diverge.
- [ ] LegacyService registered with the correct `LifecycleScope` and a header comment naming it an edge adapter + the native Service it preserves.
- [ ] Domain error codes registered in `agent-core-v2`; wire codes registered in `packages/kap-server/src/protocol`; route maps them in `sendMappedError`, matching v1's status codes and idempotent envelopes.
- [ ] Route resolves the scope from the URL by `accessor.get(IX)`; no cached scope; finishes before disposal.
- [ ] Tests assert the wire envelope + protocol shape; wire-shape guards added/updated where the route mirrors v1.
- [ ] `lint:imports` passes; the LegacyService did not invert scope direction.
## Red lines (this subskill)
- One wire schema, one home: the owning `agent-core-v2` domain contract or `packages/kap-server/src/protocol`. Never re-declare a v1 wire schema inline in server-v2.
- A `/api/v1` mirror route must keep every existing schema field's name, type, and semantics; only optional additions are allowed. A different shape belongs on `/api/v2`, not on the mirror.
- Do not distort the native v2 Service to satisfy a v1 quirk — add a `<domain>Legacy` edge adapter instead. The native Service serves the v2 architecture; the LegacyService serves the wire contract.
- A LegacyService is still a v2 Service: it follows scope, domain-direction, and DI rules. "Edge adapter" describes its role, not an exemption.
- The established wire schema (in its owning home — the `agent-core-v2` domain contract or `packages/kap-server/src/protocol`) plus the existing mirror routes are the spec for a `/api/v1` route — match them; do not re-derive the wire shape from the v2 domain model or from memory.
- Register every new error code in **both** `agent-core-v2` and `packages/kap-server/src/protocol/error-codes.ts`; an unmapped code is a wire break.
- Events stream over WS (`listen`), never over the REST mirror; do not invent REST polling for something v1 pushed as an event.

View file

@ -0,0 +1,354 @@
# Topic — Service authoring
How to write a Service in `packages/agent-core-v2`: file layout, naming, what goes in the contract vs the impl, interface style, constructor / field conventions, events, multi-Service domains, and the comment rules. This is the day-to-day reference for stage 3 (implement.md covers the DI *mechanics*; this file covers the *authoring details*).
## File layout
One folder per domain, **camelCase**: `session/`, `sessionActivity/`, `contextMemory/`, `toolDedup/`. Inside, six kinds of files:
```text
<domain>/
├── <name>.ts ← interface file: exactly one IXxx + its createDecorator + the types it owns
├── <name>Service.ts ← impl file: exactly one class + exactly one registerScopedService(...)
├── <concern>.ts ← pure function(s): no Service suffix, no class, no registration
├── <targetDomain>.ts ← contribution file (common): registers into another domain's extension point
├── <what>.contrib.ts ← contribution file (uncommon / ad-hoc)
└── <domain>.types.ts ← shared types that no single interface owns
```
- **Strictly one service per file.** An interface file holds exactly one injectable interface and exactly one `createDecorator(...)`; an impl file holds exactly one service implementation class and exactly one `registerScopedService(...)`. No exceptions for "tightly-coupled" groups: even same-scope collaborators each get their own `<name>.ts` + `<name>Service.ts` pair.
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)). The header comment restates the same scope.
- A domain therefore has as many impl files as it has services (e.g. `logService.ts` for the App `ILogService`, `sessionLogService.ts` for the Session `ISessionLogService`). See [Multi-Service domains](#multi-service-domains).
The package entry `src/index.ts` imports and `export *`s every domain's leaf files precisely (one line per leaf), so importing the package still runs every `registerScopedService(...)` side effect — exactly as the old per-domain barrels did.
## Naming
### Interfaces and classes
| Artifact | Rule | Example |
|---|---|---|
| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Workspace` / `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `IWorkspaceDirs`, `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) |
| Class | the interface name minus the leading `I`, plus `Service` if it does not already end in `Service`; `implements` the interface | `SessionLogService implements ISessionLogService`, `AppendLogStoreService implements IAppendLogStore` |
| Decorator string | lowerCamelCase of the interface name minus the leading `I`; **globally unique and stable** (it surfaces in `CyclicDependencyError.path` and "no service registered" errors) | `createDecorator<ISessionLogService>('sessionLogService')` |
| Model / non-service types | PascalCase, no `I` prefix | `SessionMeta`, `LogEntry`, `ConfigSection` |
The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Workspace, Session and Agent services always carry `Workspace` / `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names.
> Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, and `ISessionEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md).
### File names
File names derive from the interface / class names so that scope and role are visible in the tree:
| File kind | Rule | Example (interface → file) |
|---|---|---|
| Interface file | interface name minus leading `I`, minus trailing `Service` if present; acronym-aware lowerCamelCase | `ISessionLogService``sessionLog.ts`; `IAppendLogStore``appendLogStore.ts`; `ILogService``log.ts` |
| Impl file | the class name; acronym-aware lowerCamelCase | `SessionLogService``sessionLogService.ts`; `AppendLogStoreService``appendLogStoreService.ts` |
| Pure-function file | the function / concern name; no `Service` suffix | `formatLogEntry.ts`, `levelEnabled.ts` |
| Contribution file (common) | the **target** domain name | `config.ts` (registers a config section), `tool.ts`, `flag.ts` |
| Contribution file (uncommon) | `<what>.contrib.ts` | `slackWebhook.contrib.ts` |
| Shared-types file | `<domain>.types.ts` | `log.types.ts` |
| Errors file | `<name>.errors.ts` | `appendLogStore.errors.ts` |
Acronym-aware lowerCamelCase lowercases a leading acronym as a group: `ILLMRequester``llmRequester.ts`, `IWSGateway``wsGateway.ts`, `IOAuthToolkit``oauthToolkit.ts`, `IMcpServerService``mcpServerService.ts`.
Because the impl class always ends in `Service` and the interface file never does, the two files of one service never collide — even for `Store` / `Registry` / `Resolver` interfaces (`IAppendLogStore``appendLogStore.ts` + `appendLogStoreService.ts`).
## The contract file (`<domain>.ts`)
Holds the public surface of the domain. A typical contract:
```ts
/**
* `greet` domain (Ln) — one-line role.
*
* Defines the `Greeting` model and the `IGreeter` used by … Bound at … scope.
*/
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface Greeting { // model — no _serviceBrand
readonly message: string;
}
export interface IGreeter { // injectable service — carries _serviceBrand
readonly _serviceBrand: undefined;
hello(): Greeting;
}
export const IGreeter: ServiceIdentifier<IGreeter> =
createDecorator<IGreeter>('greeter');
```
What belongs here:
- **Model types** (`type` / `interface`) the domain exposes — `SessionMeta`, `LogEntry`, `ConfigSection`.
- **Service interface(s)** — the contract consumers depend on.
- **Decorator(s)** — one `createDecorator` per injectable service.
- **Helper types and pure functions** tightly bound to the contract — e.g. option bags, `satisfies`-checked seeds, predicate functions like `levelEnabled`.
### Which interfaces carry `_serviceBrand`
Only interfaces used as a **DI token** carry `readonly _serviceBrand: undefined`. Everything else does not:
- ✅ Service interface resolved via `@IX` / `accessor.get(IX)` → carries `_serviceBrand`.
- ❌ Base interface extended by a service (e.g. `ILogger` extended by `ILogService`) → no `_serviceBrand`.
- ❌ Plain model / data interface (`LogEntry`, `SessionMeta`) → no `_serviceBrand`.
```ts
export interface ILogger { // base interface — no brand
info(message: string): void;
}
export interface ILogService extends ILogger { // DI token — branded
readonly _serviceBrand: undefined;
setLevel(level: LogLevel): void;
}
```
## Interface style
- **Sync methods** return a concrete type; **async methods** return `Promise<T>`. Do not wrap a sync return in `Promise`.
- **Readonly fields** for immutable exposed state: `readonly ready: Promise<void>`, `readonly modelAlias: string | undefined`.
- **Optional members** with `?`: `flush?(): Promise<void>`, `close?(): Promise<void>`.
- **Generics** where the caller supplies the shape: `get<T = unknown>(domain: string): T`.
- **Extend** a base interface to share method groups: `interface ILogService extends ILogger`.
- **Events** as `readonly onDid…` / `onWill…` properties typed `Event<T>` — see [Events](#events).
```ts
export interface IConfigService {
readonly _serviceBrand: undefined;
readonly ready: Promise<void>;
readonly onDidChange: Event<ConfigChangedEvent>;
get<T = unknown>(domain: string): T;
set(domain: string, patch: unknown): Promise<void>;
reload(): Promise<void>;
}
```
## The impl file (`<domain>Service.ts`)
Holds the concrete class(es) and the top-level registration. A typical impl:
```ts
/**
* `greet` domain (Ln) — `IGreeter` implementation.
*
* … collaborators as roles ("logs through `log`") … Bound at App scope.
*/
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { ILogService } from '#/log';
import { type Greeting, IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined;
constructor(@ILogService private readonly log: ILogService) {}
hello(): Greeting {
this.log.info('hello');
return { message: 'hi' };
}
}
registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet');
```
What belongs here:
- **Imports**`LifecycleScope` + `ScopeActivation` + `registerScopedService` from `'#/_base/di/scope'`; collaborators via the `#/<domain>` alias; the contract's types + decorator via a relative `./<domain>` import.
- **Class**`XxxService implements IXxxService`, with `declare readonly _serviceBrand: undefined`.
- **Helper classes / functions** used only by this impl (e.g. a built-in writer, an `extractError` helper) — co-located in the same file.
- **Top-level `registerScopedService(...)`** — one per Service the file owns; importing the impl file runs the registration.
Base class: extend `Service` (from `#/_base/di/service`) when the unit needs capability calls on `this``provide` / `effect` / `on` / `get` / `ref` (e.g. contributing a record to a `collection` token). `Service` extends `Disposable`, so `_register` keeps working; constructor-time `provide` / `on` / `effect` calls are buffered and flushed by the kernel after construction, while `get` / `ref` throw inside the constructor (dependencies stay constructor parameters). Otherwise extend `Disposable` — both are full DI units; a service whose own members collide with the `Service` vocabulary (`name` / `state` / `config` / `get`) must stay on `Disposable` (leave a NOTE comment saying so).
## Constructor conventions
- Declare every dependency with `@IX` on a constructor parameter.
- Use `private readonly` (or `protected readonly`) to store a used dependency as a field.
- For an injected dependency the class does **not** directly use (e.g. passed through, or only needed to force construction order), drop the visibility modifier and prefix with `_`: `@IEventService _event: IEventService`.
- Service parameters and static parameters may both appear; the ordering rule depends on how the object is created — see below.
### Parameter order: scoped service vs `createInstance`
- **`registerScopedService` services** — the container injects only the `@IX` parameters; any static parameters must have defaults and are left at their default when the container builds the instance. Order is therefore not enforced by the container, but the common style is **`@IX` parameters first, optional static parameters after**:
```ts
constructor(
@ILogWriterService protected readonly writer: ILogWriterService,
private readonly bound: LogContext = {},
level: LogLevel = 'info',
) {}
```
- **`createInstance` objects** (non-singletons built with `instantiation.createInstance(Ctor, …staticArgs)`) — static parameters **must come first**, service parameters after, because the caller passes the static prefix positionally:
```ts
constructor(
private readonly input: string, // static — passed by caller
@ILogService private readonly log: ILogService, // service — injected
) {}
```
### Factory methods
A scoped Service may expose a factory method that returns a **new** instance of itself (or a related class) with extra context bound — e.g. `ILogger.child(ctx)` returns `new LogService(this.writer, { …this.bound, …ctx }, this._level)`. This is not a DI violation: it is an explicit factory, not a request for the container to build a Service. Do not use it to circumvent scope or singleton semantics.
## Fields and state
- `private readonly` for fields set once at construction (injected deps, derived config).
- `private _name` (underscore prefix) for mutable private state: `private _level: LogLevel`.
- `readonly` public fields only for immutable exposed state; prefer a getter (`get level()`) when the value can change.
- Keep state minimal — a Service owns only the state that matches its scope's identity (design.md §2). Anything else belongs in a different Service.
### Runtime state goes into the per-scope state container
Workspace/Session/Agent-scope Services register their runtime state into the scope's state container (`IWorkspaceStateService` / `ISessionStateService` / `IAgentStateService`, all over `_base`'s `StateRegistry`) instead of holding it in bare instance fields, so per-scope state lives in one observable place (`snapshot()` / `onDidChange`) and dies with the scope. Reference: `session/interaction/interactionService.ts`.
- Declare keys in the domain file and export them: `export const interactionPendingKey = defineState<Map<string, Pending>>('interaction.pending', () => new Map())``<domain>.<field>` naming, factory initializers.
- Inject `@ISessionStateService private readonly states` (or the Agent token) and `this.states.register(key)` per key at the top of the constructor.
- Replace the field with accessors: a getter for collections only mutated in place (`this.foo.add(...)` keeps working — the container stores references, never clones); add a setter routed through `states.set` for reassigned scalars. Call sites stay unchanged.
- Values must be plain data: scalars, arrays, and literal objects/Maps/Sets built from them. Never register class instances, resource handles (disposables, abort controllers, Promise locks), or objects holding service references — the regression precedent: one registry key whose class instances reached the whole DI graph deep-copied to hundreds of MB on `snapshot()` and OOM-killed the server. This means registries whose entries carry resources (the tool registry, the task map, prompt queues) stay as instance fields alongside Emitters, hook slots, disposable slots, waiter arrays, caches, and queue instances.
- `snapshot()` additionally recurses plain data only: values with a custom prototype collapse to a `'(ClassName)'` marker — a `_base`-level backstop, not a license to register resource-bearing values.
- Durable, replayable state does NOT belong here — it stays on wire Models. The container is memory-only.
## Events
v2 has two distinct event mechanisms. Pick by audience:
### `Event<T>` / `Emitter` — typed property on a Service
Use when a Service exposes a typed event its consumers subscribe to. Lives in `'#/_base/event'`.
```ts
// contract
import type { Event } from '#/_base/event';
export interface IConfigService {
readonly onDidChange: Event<ConfigChangedEvent>;
}
// impl
import { Emitter, type Event } from '#/_base/event';
export class ConfigService extends Disposable implements IConfigService {
private readonly _onDidChange = this._register(new Emitter<ConfigChangedEvent>());
readonly onDidChange: Event<ConfigChangedEvent> = this._onDidChange.event;
private notify(changed: ConfigChangedEvent): void {
this._onDidChange.fire(changed);
}
}
```
Conventions:
- Back the public `Event<T>` with a private `Emitter<T>`, registered with `this._register(...)` so it disposes with the Service.
- Naming: `onDid…` for "happened" (past tense, after the fact); `onWill…` for "about to happen" (may allow `waitUntil` participation / veto — see `AsyncEmitter` / `IWaitUntil` in `'#/_base/event'`).
- A service must be constructed before consumers can subscribe to its events. Use the default `OnScopeCreated` activation when subscriptions must be available as soon as the scope is ready.
### `IEventService` — global pub-sub bus
Use to broadcast protocol events across domains. Lives in `'#/event'`.
```ts
export interface IEventService {
readonly _serviceBrand: undefined;
publish(event: ProtocolEvent): void;
subscribe(handler: (event: ProtocolEvent) => void): IDisposable;
}
```
Inject `@IEventService` and `publish(...)`; `subscribe(...)` returns an `IDisposable` to register with `this._register(...)`. This is the bus for "a fact happened, react if you care" (design.md §4) — not for typed per-Service events.
## Multi-Service domains
A domain may define several Services. Each Service gets its own pair of files regardless of scope or coupling:
- **One pair per Service**`<name>.ts` for the contract + `<name>Service.ts` for the implementation.
- **Different scopes** → the scope prefix in the Service name makes this obvious (`logService.ts` for App `ILogService`, `sessionLogService.ts` for Session `ISessionLogService`).
- **Same interface, multiple role tokens** (e.g. `IAtomicDocumentStore` and `IAtomicTomlDocumentStore` share one interface type but are distinct DI tokens) → each token is its own Service identity and must be registered and resolved independently.
There is no `index.ts` barrel: consumers import each contract/impl from its precise leaf path (e.g. `import { ILogService } from '#/log/log'`), never the domain directory.
## No barrel — the package entry loads leafs precisely
A domain has **no `index.ts` barrel**. Its files are the contract leaf (`<name>.ts`) and the impl leaf (`<name>Service.ts`), and consumers import the precise file — never the directory:
```ts
import { IGreeter, type Greeting } from '#/greet/greet';
```
Self-registration is unchanged: `greetService.ts` keeps its top-level `registerScopedService(...)`. The package entry `src/index.ts` loads the domain's leafs precisely — `export *` for the contract, a side-effect `import` for the impl — one line per leaf:
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService';
```
Importing the package therefore fires every `register*` side effect, exactly as the old per-domain barrels did. When you add a new domain, write the contract + impl leafs (with their top-level `register*`), then add the leaf path(s) to `src/index.ts`. **Do not create an `index.ts`.**
- Load the impl file too — its top-level `registerScopedService(...)` only runs when the module is imported.
- `export *` helper modules only if they are part of the domain's public surface.
- Each leaf's file-header comment still names the domain, scope, and (for impls) the `register*` binding it owns.
## Comments
- **File-header comment is mandatory** and the only place comments live (orient.md). State the identity line, the role, collaborators (impls), and scope.
- **Methods and fields carry no comments by default.** Well-named identifiers and types say *what*; the code is the source of truth for *how*.
- Write an inline comment only when the *why* is non-obvious (a hidden constraint, a subtle invariant, a workaround). One short line.
- For unimplemented stubs, throw `NotImplementedError('feature')` rather than `throw new Error('TODO: …')` (errors.md).
## Complete minimal example
```ts
// greet/greet.ts
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
export interface Greeting { readonly message: string; }
export interface IGreeter {
readonly _serviceBrand: undefined;
hello(): Greeting;
}
export const IGreeter: ServiceIdentifier<IGreeter> = createDecorator<IGreeter>('greeter');
```
```ts
// greet/greetService.ts
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { type Greeting, IGreeter } from './greet';
export class Greeter implements IGreeter {
declare readonly _serviceBrand: undefined;
hello(): Greeting { return { message: 'hi' }; }
}
registerScopedService(LifecycleScope.App, IGreeter, Greeter, ScopeActivation.OnScopeCreated, 'greet');
```
```ts
// src/index.ts
export * from './greet/greet';
import './greet/greetService';
```
## Red lines (this topic)
- One folder per domain, camelCase; one service per file pair: contract `<name>.ts` + impl `<name>Service.ts`; **no `index.ts` barrel**`src/index.ts` loads each leaf file precisely.
- Exactly one injectable interface and one `createDecorator(...)` per contract file.
- Exactly one service implementation class and one `registerScopedService(...)` per impl file.
- `IXxxService` / `XxxService` naming; decorator string is lowerCamelCase, globally unique, and stable.
- Name Services by owning domain, never by scope (`IAgentEntityService`, `ISessionEntityService`).
- `_serviceBrand` only on interfaces used as a DI token — never on base interfaces or plain models.
- Sync methods return concrete types, async return `Promise<T>`; do not `Promise`-wrap sync work.
- `createInstance` objects put static parameters before service parameters; scoped services put `@IX` parameters first (static params need defaults).
- Never `new` a `@IService`-carrying Service — except inside an explicit factory method, which is not a DI request.
- Events: typed per-Service event → `Event<T>`/`Emitter` from `'#/_base/event'`; cross-domain broadcast → `IEventService` from `'#/event'`.
- `src/index.ts` must import/export every leaf file (including the impl) so each `register*` side effect runs.
- File-header comment only; methods/fields carry no comments by default; stubs throw `NotImplementedError`.

View file

@ -0,0 +1,97 @@
# Topic — Telemetry
Telemetry infrastructure for agent-core-v2: how business services emit events, how context propagates, and how events reach a destination through appenders.
Telemetry is a **layer-1 root** domain (alongside `log`): the facade lives at `App` scope (a per-Agent ambient context service is bound at `Agent` scope), stateless, with no business-domain dependencies. It is a thin facade — enrichment, batching, and transport belong to the appenders, not to this layer.
## Where things live
- `src/app/telemetry/telemetry.ts`: contract — `ITelemetryService` (facade), `ITelemetryAppender` (destination), `TelemetryProperties`, `nullTelemetryAppender`, and `TelemetryServiceOptions`.
- `src/app/telemetry/events.ts`: event registry — `telemetryEventDefinitions` pairs every business event's property type with review metadata (owner / purpose / per-property comment); the single source of truth for `track2`. Agent-scope events register with `defineAgentTelemetryEvent<P>` and compose the ambient `AgentTelemetryEventContext` (`agent_id`) into their wire schema; all other events register with `defineTelemetryEvent<P>`.
- `src/app/telemetry/telemetryService.ts`: `TelemetryService` impl + `registerScopedService(LifecycleScope.App, …)`.
- `src/app/telemetry/agentTelemetryContext.ts` + `agentTelemetryContextService.ts`: `IAgentTelemetryContextService` — Agent-scoped mutable request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) snapshot into turn telemetry at launch. Agent identity (`agent_id`) is not part of it — identity is bound by the Agent-scoped `ITelemetryService` view.
- `src/app/telemetry/consoleAppender.ts`: `ConsoleAppender` — echoes events to a log function (dev / debug).
- `src/app/telemetry/cloudAppender.ts`: `CloudAppender` — sanitizes + PII-cleans properties, batches + enriches + posts to the telemetry endpoint.
- `src/app/telemetry/cloudTransport.ts`: `CloudTransport` — HTTP transport behind `CloudAppender`.
- `src/app/telemetry/privacy.ts`: outbound PII redaction (`cleanTelemetryProperties`) — URLs, emails, tokens, and absolute file paths become `<REDACTED: ...>` labels; `node_modules/` tails are kept.
## Emitting events (business services)
Inject `ITelemetryService` and call `track2` with a registered event:
```ts
import { ITelemetryService } from '#/app/telemetry/telemetry';
constructor(@ITelemetryService private readonly telemetry: ITelemetryService) {}
this.telemetry.track2('cron_fired', { task_id: taskId, coalesced_count: 0, stale: false, buffered: false, recurring: true });
```
`track2` is checked against the registry in `events.ts` at compile time: the event name must be a key of `telemetryEventDefinitions`, and the properties must match the registered interface exactly (extra or missing keys are compile errors). **New events must be registered first** — add a properties interface, then register it with `defineAgentTelemetryEvent<P>({ owner, comment, properties })` when every emission path goes through an Agent-scoped `ITelemetryService` view, or `defineTelemetryEvent<P>` otherwise (including events with any non-Agent emission path, e.g. `image_compress` from the kap-server prompt routes), documenting every property. For agent-scope events the registered interface is the business payload only: ambient `agent_id` is declared once in `AgentTelemetryEventContext` and composed into the wire schema, so it must not appear in the payload or at call sites. Naming: snake_case for events and properties, unit suffixes (`_ms` / `_count` / `_bytes`), no user content or file paths; `test/app/telemetry/events.test.ts` enforces the conventions. The low-level `track` remains for appender plumbing and tests only.
`TelemetryService.track` merges the bound context into the properties and fans the event out to every registered appender. A single throwing appender is isolated via `onUnexpectedError` and never blocks the rest.
### Context (sessionId / agent_id / turn_id)
The root service carries a bound context (`sessionId`) that is merged into every event, and each Agent scope gets its own telemetry view seeded with `agent_id` (by `agentLifecycle`), so Agent-scoped services emit their identity without call-site plumbing. Mutable per-agent request context (`mode` / `provider_type` / `protocol` / `turn_id` / `trace_id`) lives in `IAgentTelemetryContextService` and is snapshot into a per-turn view at turn launch. Derive a scoped view with `withContext`:
```ts
const child = telemetry.withContext({ agent_id: 'agent-0' });
child.track2('tool_call', { turn_id: 1, tool_call_id: 'c1', tool_name: 'bash', outcome: 'success', duration_ms: 12 }); // wire carries sessionId + agent_id
```
`withContext(patch)` returns a lightweight forwarding view: transport state (appenders, enabled flag) stays with the root, so later `addAppender` / `setEnabled` calls apply to every view, and per-call properties override bound context on key collision. `setContext(patch)` on the root mutates the root context and propagates to appenders that implement `setContext`; on a view it mutates only that view's own context.
## Appenders (destinations)
An appender is the destination an event is fanned out to. It is **not a DI Service** — it is a plain object implementing `ITelemetryAppender`, held by `TelemetryService`.
```ts
export interface ITelemetryAppender {
track(event: string, properties?: TelemetryProperties): void;
withContext?(patch: TelemetryContextPatch): ITelemetryAppender;
setContext?(patch: TelemetryContextPatch): void;
flush?(): Promise<void> | void;
shutdown?(): Promise<void> | void;
}
```
Built-in appenders:
- `ConsoleAppender``[telemetry] <event> <json>` to a log function (default `console.log`); options `prefix` / `pretty` / `log`.
- `CloudAppender` — batches events, enriches with common context (`app_name` / `version` / `platform` / …), and posts to `https://telemetry-logs.kimi.com/v1/event` through `CloudTransport` (Bearer auth, retry, on-disk fallback). Options: `homeDir` / `deviceId` / `sessionId?` / `appName` / `version` / `uiMode?` / `model?` / `getAccessToken?` / `endpoint?` / `flushThreshold?` / `flushIntervalMs?`.
### Registering appenders (bootstrap)
Appenders are added after the App scope exists, by resolving the service and calling `addAppender`:
```ts
const app = createAppScope();
const telemetry = app.accessor.get(ITelemetryService);
telemetry.addAppender(new ConsoleAppender({ prefix: '[dev]' })); // dev echo
telemetry.addAppender(new CloudAppender({ // production
homeDir, deviceId, sessionId,
appName: 'kimi-code', version, uiMode: 'shell', model,
getAccessToken: () => auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME),
}));
```
`addAppender` returns an `IDisposable` that removes the appender when disposed. `setAppender(appender)` resets to a single appender (mainly for tests). `removeAppender(appender)` drops one.
> There is no production bootstrap wired yet — `TelemetryService` defaults to `[nullTelemetryAppender]`, so `track(...)` is a no-op until `addAppender` is called at startup.
## Lifecycle
- `setEnabled(false)` drops `track` (service-level switch); `setEnabled(true)` resumes. `flush` / `shutdown` are unaffected by the switch.
- `flush()` / `shutdown()` fan out to all appenders concurrently; a single rejecting appender is swallowed. Await `shutdown()` before process exit so buffered events (e.g. in `CloudAppender`) are sent.
## Red lines (this topic)
- Business services depend only on `ITelemetryService` — never import an appender class.
- Telemetry is layer-1 root: do not inject any business-domain service into it, and keep the facade at `App` scope (only the ambient context service binds at `Agent`).
- Appenders are plain `ITelemetryAppender` objects, not DI Services — register them with `addAppender`, never via `registerScopedService`.
- `track` is fire-and-forget and must not throw; appender `track` must be synchronous — buffer and send asynchronously via `flush` / `shutdown`.
- Await `telemetry.shutdown()` before process exit when a buffering appender is registered.
- Keep event names stable; register every business event in `events.ts` and emit via `track2` — properties must be JSON-serializable primitives (non-primitives are dropped with a warning by `CloudAppender`).
- Agent identity is ambient: agent-scope events go through `defineAgentTelemetryEvent` and get `agent_id` from the scoped telemetry view — do not pass `agent_id` at business call sites (per-event identities such as `subagent_created` and the cron events are the exception).

View file

@ -0,0 +1,270 @@
# Stage 4 — Test
Exercise the **same path production uses**: a service is reached by its interface through the container, its `@IService` dependencies are resolved from the container, and — where the scope layer matters — through the scope tree. Tests that `new` a service and paper over its constructor with hand-rolled objects bypass that path and let the `registerScopedService(IX → Impl)` binding rot untested.
`@IService` parameter decorators run under vitest (the build uses `experimentalDecorators`), so fixtures declare dependencies exactly like production code. There is **no** `param()` helper, no manual `(Id as …)(Ctor, '', 0)`, and no capturing `accessor` inside a constructor to synchronously `.get()` a peer.
## The one rule
**Resolve the system under test by its interface, through the container. Never call `new` on a production service whose constructor carries `@IService` dependencies.**
```ts
// ✅ resolve by interface — the IX → Sut binding is exercised
ix.set(IMessageService, new SyncDescriptor(MessageService));
const svc = ix.get(IMessageService);
// ❌ construct the implementation directly — the registration is never run
const svc = new MessageService(stubContext);
```
Resolving by interface is what makes `registerScopedService(ISut, Sut, …)` part of the test. Constructing the class directly (or via `ix.createInstance(Sut)`) tests the class in isolation but leaves the binding, the scope layer, and the delayed/eager flag unverified.
Pure functions, value objects, and services with **no** `@IService` dependencies may be constructed directly.
The only other exception is a test that genuinely needs **two independent instances** of the same service with different dependencies (e.g. constructing two `TurnService`s with different `ILoopRunner`s). A singleton-per-container resolution cannot produce both, so `ix.createInstance(Impl)` is acceptable there — annotate it with a comment explaining why.
## Two harnesses
Pick the harness by *whether the scope layer is part of what you are testing*.
| Under test | Harness | Resolve the SUT with |
|---|---|---|
| A single service's behavior (unit) | `TestInstantiationService` (flat) | `ix.get(ISut)` after `ix.set(ISut, new SyncDescriptor(Sut))` |
| Cross-scope wiring, or which layer a service lives in | `createScopedTestHost` (scope tree) | `host.<scope>.accessor.get(ISut)` |
### Unit harness — `TestInstantiationService`
Default for domain service unit tests. It is an `InstantiationService` that also implements `ServicesAccessor` (so you can `ix.get(...)` directly) and owns sinon (so `dispose()` restores stubs).
```ts
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices } from '#/_base/di/test';
import type { TestInstantiationService } from '#/_base/di/test';
import { registerRecordsServices } from '../records/stubs';
describe('XxxService', () => {
let disposables: DisposableStore;
let ix: TestInstantiationService;
beforeEach(() => {
disposables = new DisposableStore();
ix = createServices(disposables, {
base: [registerRecordsServices],
additionalServices: (reg) => {
reg.define(IContextService, ContextService); // 1. real collaborator, by interface
reg.define(IXxxService, XxxService); // 2. system under test, by interface
},
});
});
afterEach(() => disposables.dispose());
it('does the thing', () => {
const svc = ix.get(IXxxService); // 3. resolve by interface
expect(svc.thing()).toBe('…');
});
});
```
`createServices` builds the container from domain **service groups** plus per-test overrides (see Service groups). Reach for `ix.stub(...)` / `ix.set(...)` directly only inside an `it` when a single test needs to swap a registration:
- whole service, partial object: `ix.stub(IId, { method() { return … } })`;
- single method: `ix.stub(IId, 'method', value)` returns a sinon stub; `ix.spy(IId, 'method')` returns a spy;
- a prebuilt instance or descriptor: `ix.set(IId, instance)` / `ix.set(IId, new SyncDescriptor(Impl))`;
- when a collaborator's behavior must vary per test, model it as a `Test*Service` subclass whose methods read suite-scoped `let` variables rather than rebuilding the container each test.
### Scope harness — `createScopedTestHost`
Reach for this only when *which layer a service lives in* is itself the thing being asserted, or when the SUT reads from parent/child scopes. It builds the real `Scope` tree and resolves through it.
```ts
import { beforeEach, describe, expect, it } from 'vitest';
import { LifecycleScope } from '#/app/scopes';
import {
ScopeActivation,
_clearScopedRegistryForTests,
registerScopedService,
} from '#/_base/di/scope';
import { createScopedTestHost, stubPair } from '#/_base/di/test';
describe('XxxService (scoped)', () => {
beforeEach(() => {
_clearScopedRegistryForTests();
registerScopedService(
LifecycleScope.Agent,
IXxxService,
XxxService,
ScopeActivation.OnDemand,
'xxx',
);
});
it('resolves from the Agent scope with ancestor deps injected', () => {
const host = createScopedTestHost([stubPair(ILogService, stubLog())]);
const agent = host.child(LifecycleScope.Agent, 'main');
const svc = agent.accessor.get(IXxxService); // by interface
expect(svc.thing()).toBe('…');
host.dispose();
});
});
```
Always `_clearScopedRegistryForTests()` and re-register explicitly in `beforeEach`. Do not rely on a production module's top-level `registerScopedService(...)` side effect: import order then becomes part of the test, and another suite's `_clearScopedRegistryForTests()` can wipe it.
## Register the SUT by interface
Whichever harness you use, the SUT is registered under its interface (`ix.set(IX, new SyncDescriptor(Impl))` or `registerScopedService(scope, IX, Impl, …)`) and resolved by that interface. This is non-negotiable: it is the only thing that keeps the production registration honest.
A test that does `ix.createInstance(Impl)` is testing the class, not the service. Convert those (see Migration).
## Shared stubs
Hand-rolled stubs (`noopLog`, `noneEvent`, `unusedRecords`, …) must not be copied between test files. Each domain that owns a frequently-stubbed interface exports a stub from a `stubs.ts` **in the `test/` tree**, never from `src/`:
```text
test/log/stubs.ts → stubLog() / stubLogger()
test/turn/stubs.ts → stubTurn()
test/records/stubs.ts → stubAgentRecords()
test/environment/stubs.ts → stubEnvironment()
```
All test support lives under `test/` so test-only code stays out of the production source tree. Because `tsdown` builds from `src/index.ts`, anything under `test/` is unreachable from the entry and is never bundled into `dist/`.
Conventions:
- export a **factory** (`stubXxx()`), not a shared singleton, so tests cannot leak state through a stub;
- name it `stub<Interface>` — e.g. `stubAgentRecords`;
- the stub satisfies the full interface so the compiler, not a cast, guarantees it stays in sync;
- import it with a **relative path**`./stubs` from the same domain's tests, `../<domain>/stubs` from another domain. Never import stubs from `#/…` (that alias is for production `src/`) and never import one test file from another;
- a `stubs.ts` may import its domain's production types via `#/<domain>/…`.
If a stub is needed by two test files, it belongs in that domain's `test/<domain>/stubs.ts`.
## Service groups
Most unit tests stub the same handful of collaborators (`ILogService`, `IAgentRecords`, `IConfigService`, `ITelemetryService`, …). Rather than repeat `ix.stub(...)` lines in every `beforeEach`, each domain exports a `register*Services` function from its `stubs.ts` that registers the default test doubles for that domain:
```ts
// test/log/stubs.ts
export function registerLogServices(reg: ServiceRegistration): void {
reg.defineInstance(ILogService, stubLog());
}
```
`createServices(disposables, { base, additionalServices })` composes them:
- `base` — an ordered list of service groups. Each group's registrations are deduped (first writer wins), so groups supply safe defaults without clobbering each other.
- `additionalServices` — applied after `base`. Registrations here **overwrite** any base default, so a test can swap a stub for a spy, register the system under test, or supply a one-off collaborator.
```ts
ix = createServices(disposables, {
base: [registerLogServices, registerConfigServices, registerRecordsServices],
additionalServices: (reg) => {
reg.definePartialInstance(IAgentKaos, {}); // one-off collaborator
reg.define(IAgentRecords, spyRecords); // override a base default
reg.define(IXxxService, XxxService); // system under test
},
});
```
`ServiceRegistration` offers three verbs:
- `define(id, Ctor)` — lazy `SyncDescriptor`; the service is instantiated on first resolve. Use for real collaborators and the system under test.
- `defineInstance(id, instance)` — a fully-built instance (a fake such as `stubLog()`, or `new ConfigRegistry()`).
- `definePartialInstance(id, { ... })` — a partial mock; only the supplied members are provided. Use for collaborators the test does not exercise.
Conventions:
- a group registers the domain's services **as dependencies** (a fake, or a `{}` partial when no fake exists yet). When a service is the system under test, the test registers the real implementation via `additionalServices` and does not rely on the group's default for it;
- keep groups small and domain-local. A service that is almost always the system under test, or that every consumer configures differently, should not have a group — register it inline via `additionalServices`;
- import groups with a **relative path** (`../<domain>/stubs`), never from `#/…`.
`createServices` defaults to `strict: false` (missing dependencies warn rather than throw), matching `new TestInstantiationService()`. Pass `strict: true` to surface unregistered `@IService` dependencies.
## Declaring dependencies
Always use `@IService` constructor decorators — in fixtures and in production services alike.
```ts
// ✅
class Consumer {
constructor(@IGreeter private readonly greeter: IGreeter) {}
}
// ❌ no param() helper, no inline cast
class Consumer {
constructor(private readonly greeter: IGreeter) {}
}
param(IGreeter, Consumer, 0);
```
Because the decorator runs when the class is defined, the `createDecorator` identifier must be initialized **before** the class that uses it. Declare the identifier, then the class:
```ts
const IDep = createDecorator<IDep>('dep');
class Consumer {
constructor(@IDep private readonly dep: IDep) {}
}
```
For two services that depend on each other (a cycle), declare both identifiers first, then both classes, so neither class references an uninitialized binding.
Declare fixtures at module top, interface + decorator + implementation co-located, and keep `_serviceBrand` on the interface when it represents a real service — `GetLeadingNonServiceArgs` relies on the brand to tell service parameters apart from static ones. Pure throwaway fixtures may omit `_serviceBrand`.
## Lifecycle / teardown
One `DisposableStore` per suite. Add the **container** and any event subscriptions to it; dispose in `afterEach`.
```ts
beforeEach(() => { disposables = new DisposableStore(); /* … */ });
afterEach(() => disposables.dispose());
```
Do **not** add the system-under-test itself to the store. `TestInstantiationService` disposes every service it creates when the container is disposed, so `ix.get(IX)` instances are cleaned up automatically via `disposables.add(ix)`. Wrapping the SUT in `disposables.add(...)` would double-dispose it. For the same reason, do not call `svc.dispose()` at the end of a test unless you are asserting something about disposal itself.
Scope-host tests call `host.dispose()` in `afterEach` (or at the end of the `it`). Route teardown through the store so ordering is deterministic and nothing leaks when a test fails mid-way.
## Cascade: asserting unit state
The cascade engine's test vocabulary lives in two files: `test/_base/di/cascade.test.ts` (the mechanism matrix, including cross-scope orchestration) and `test/_base/di/provide.test.ts` (provide/unprovide semantics).
- **Assert unit states, not internals.** Every container exposes its engine as `container.cascade`: `stateOf(IX)``'Pending' | 'Activating' | 'Active' | 'Unloading' | 'Failed'`; `failureOf(IX)` → the sticky error of a `Failed` unit; `pendingSnapshot()` → the waiting-area contents.
- **The waiting area parks units with unregistered dependencies** — a unit whose declared deps are missing stays `Pending` (no throw), so a test must seed the full dependency chain. Example: a root→agent chain with no session container must seed the session-scope dependency explicitly — `ix.set(ISessionStateService, new SessionStateService())` in `test/session/agentLifecycle/agentLifecycle.test.ts` — or the dependent unit never activates.
- **Eager activation failure is sticky `Failed`, not a scope-creation throw.** Assert state + rethrow: `expect(ix.cascade.stateOf(IX)).toBe('Failed')`, then `expect(() => ix.invokeFunction((a) => a.get(IX))).toThrow(…)`. Do not expect scope/host creation itself to throw for a failing eager constructor.
## Assertions and naming
- One behavior per `it`; describe observable behavior (`child shadows parent registration`), not implementation (`calls _getOrCreateServiceInstance`).
- For cycles, assert `CyclicDependencyError` and its `path` array (e.g. `['A', 'B', 'A']`), not merely `toThrow`.
- For disposal order, capture events in an array and assert the sequence (`['C', 'B', 'A']` — children before parents).
## Migrating existing tests
Most legacy tests build the SUT with `ix.createInstance(Impl)`. Converting one is mechanical:
1. import the interface (`IX`) and the descriptor;
2. register the SUT by interface — `reg.define(IX, Impl)` inside `additionalServices` (or `ix.set(IX, new SyncDescriptor(Impl))`);
3. replace `ix.createInstance(Impl)` with `ix.get(IX)`;
4. drop the `disposables.add(...)` wrapper around the SUT and any trailing `svc.dispose()` — the container disposes it;
5. replace any hand-rolled collaborator object with the domain's shared stub or service group (or add one to `test/<domain>/stubs.ts` if it does not exist);
6. delete now-unused imports.
Before / after:
```ts
// before
const svc = ix.createInstance(MessageService);
// after — registration in beforeEach additionalServices
reg.define(IMessageService, MessageService);
// after — resolution in the test body
const svc = ix.get(IMessageService);
```
## Red lines (this stage)
- Resolve the SUT by interface — never `new` a production service with `@IService` deps; prefer `ix.get(IX)` over `ix.createInstance(Impl)`.
- Shared stubs live in `test/<domain>/stubs.ts` (never `src/`); import by relative path, never `#/...`.
- Scope tests call `_clearScopedRegistryForTests()` and re-register explicitly in `beforeEach`; do not rely on production import-order side effects.
- One `DisposableStore` per suite; add the container, dispose in `afterEach`; do not add the SUT itself.
- Declare fixture dependencies with `@IService`; initialize `createDecorator` identifiers before the classes that use them.

View file

@ -0,0 +1,32 @@
# Stage 5 — Verify & submit
Run the guards and re-scan the red lines before submitting.
## Commands
Run from the package (or with `--filter @moonshot-ai/agent-core-v2`):
- `pnpm --filter @moonshot-ai/agent-core-v2 lint:imports` — import-boundary guard (`scripts/check-import-boundaries.mjs`). Catches v1 imports (`@moonshot-ai/agent-core`) and kosong subtree violations.
- `pnpm --filter @moonshot-ai/agent-core-v2 typecheck``tsc -p tsconfig.json --noEmit`.
- `pnpm --filter @moonshot-ai/agent-core-v2 test``vitest run`.
## Changesets (when the change ships through the CLI)
If the change is user-facing and ships through the CLI, generate a changeset with the repository's `gen-changesets` skill (root `AGENTS.md` workflow). `agent-core-v2` is an internal package; if its change enters the CLI bundle, the changeset lists `@moonshot-ai/kimi-code` and describes the real change — do not present an internal-only change as a user-facing feature. Never write a `major` bump without explicit user confirmation.
## Pre-submit checklist
Walk the stages you touched and confirm:
- **Design** — scope follows state identity; no `Map<sessionId, …>` at `App`; dependency arrows do not make a foundational layer know an upstream one; no cycle was routed around.
- **Implement** — no `new` on `@IService`-carrying classes; `@IX` on constructor params only (service params after static params); interface + impl carry `_serviceBrand`; decorator names unique; coded errors only; flags for unreleased behavior.
- **Test** — SUT resolved by interface; stubs under `test/`; scope tests re-register after `_clearScopedRegistryForTests()`; teardown through one `DisposableStore`.
- **Files** — header comments describe role + scope only; registration runs from the impl file's top level; the new domain is exported from `src/index.ts`.
Then re-read the [global red lines](SKILL.md#global-red-lines) once — they catch most cross-stage mistakes in a single scan.
## Red lines (this stage)
- Do not skip `lint:imports` — it is the only automated check for the v1-import ban and the kosong subtree rules.
- Do not list internal packages in a changeset when the change enters the CLI bundle — list `@moonshot-ai/kimi-code` and describe the real change.
- Never write a `major` changeset without explicit user confirmation.

View file

@ -0,0 +1,21 @@
---
name: agent-core-review
description: Use ONLY for code review and test write/review guidance in `packages/agent-core-v2` (the DI × Scope agent engine). Does NOT apply to the legacy `packages/agent-core` or to any other package — for those, do not load this skill. Groups the review and testing lenses used for agent-core-v2 — `slop` (single-level-of-abstraction / layered error-handling review, invoked only on explicit request) and `test` (contract-driven per-test rules for both authoring and reviewing tests). Apply the sub-skill that matches the task; do not apply `slop` unprompted.
has-sub-skill: true
---
# kc-review
> **Scope: `packages/agent-core-v2` only.** These lenses are calibrated for the v2 engine (DI × Scope). Do not apply them to the legacy `packages/agent-core` or to other packages.
A bundle of the lenses used when reviewing or testing `packages/agent-core-v2`. Each sub-skill is self-contained; invoke the one that matches the task.
## Sub-skills
- **`slop/`** — Single Level of Abstraction & layered error handling. A *review dimension*: a function should read as a straight-line description of its own layer, with errors handled above or below. The agent reports detections and measurements, not severity grades. **Invoke only when the user explicitly asks for this lens** — do not apply it unprompted to general reviews or refactors.
- **`test/`** — Per-test rules behind "test the contract / responsibility, not the implementation," serving two modes. **Write mode:** author a test — one behavior per `it`, drive through the public surface, stub only the true external boundary, control time/config via documented knobs, keep tests clear, isolated, and refactor-resilient (CCCR). **Review mode:** audit existing tests against the same rules and report findings with `file:line`. Use when writing, modifying, or reviewing tests, or when asked how to write a good single test.
## Routing
- Reviewing code structure / abstraction layers / where error handling belongs → `slop` (only on explicit request).
- Writing or modifying tests, reviewing test quality, or advising on a single test → `test`.

View file

@ -0,0 +1,133 @@
---
name: slop
description: Invoke only when the user explicitly asks to review code through the "single level of abstraction / layered error handling" lens — a function does only its own layer's business logic while errors are handled above or below. The agent reports detections, raw-count measurements, and move directions. Apply only when the user explicitly requests this lens.
---
# Single Level of Abstraction & Layered Error Handling
North star: **a function should read as a straight-line description of what its own layer does. Anything that is not that — input validation, error handling, error-to-response translation, logging, retries, low-level mechanics — belongs to a layer above or below, not inline.**
This is a review dimension, not a hard rule. See "Exemption checklist" at the end.
## Scope of this skill — detect and measure
The agent applying this lens is a **sensor**. Its one job is to report *whether* a function mixes levels and *by how much*; deciding *how serious* it is belongs downstream. Severity labels (`Block` / `Request changes` / `Nit`) compress a continuous quantity into an uncalibrated three-point scale and are the main source of review-to-review variance, so they are produced downstream — by a deterministic rubric, anchored examples, or a human — from the facts the agent reports.
The agent's output is exactly these four things:
- **Detection (yes/no):** does this statement / block / function violate a rule of the lens?
- **Measurement (raw factual counts only):** mechanically countable quantities — body size, control-flow keywords, named syntactic shapes (see "Quantify"). Anything that first requires classifying a line (core/foreign, happy/error, high/low level) is recorded under detection, not here.
- **Direction (where it moves):** for each foreign concern, the destination layer — push **down** into a value / parser / infra helper, or push **up** into the edge handler.
- **Exemption flags:** which items, if any, hit the exemption checklist — recorded, not weighed.
Severity grades, merge/block verdicts, and "is splitting worth it" calls live downstream, derived from the four items above.
## When to use
Apply this lens only when the user asks for it explicitly (for example "用单一抽象层次审视一下", "check whether this function does too much", "errors should be handled above/below, right?"). Leave general reviews and refactors to other lenses unless the user names this one.
## The principle
One function, one level of abstraction, one responsibility. Three mutually reinforcing rules:
1. **Single Level of Abstraction (SLAP).** Every statement inside a function sits at the same conceptual level. High-level intent ("reserve inventory, charge payment, create the order") must not be interleaved with low-level mechanics (building headers, escaping strings, opening sockets, parsing bytes). If some lines read as "what" and others as "how", they belong in different functions.
2. **Error handling is its own concern (Clean Code).** A function either does the work or handles the error — not both. Business logic describes the happy path and *signals* failure (throw or return a result); the catch, mapping, logging, and recovery live in a dedicated handler, usually one layer up. Prefer exceptions / result types over threaded check-and-return ladders that interrupt the main flow.
3. **Separation of concerns by layer.** Each layer owns exactly one kind of knowledge: low-level code knows formats and protocols; mid-level code knows business rules; edge code knows the outside world (HTTP / CLI / UI). A function that knows two of these at once is leaking a layer.
The combined test: **could you explain this function to someone without using the word "and"?** If the explanation is "it reserves stock AND validates the email format AND maps the error to a status code AND logs to metrics", it is doing more than its layer's job.
Concerns that usually do **not** belong in a business function:
- Format / range / null validation that a lower value or parser could guarantee once.
- Mapping domain failures to an external protocol (status code, exit code, UI message) — that is the edge layer's job.
- Catch-and-swallow, retry loops, backoff, timeout, circuit breaking around a single call — infrastructure, push down.
- Cross-cutting telemetry / log / metric noise woven through every step — extract or push to a wrapper.
- Check-and-return ladders that occupy more space than the business core — replace with signal + a handler above.
## Methodology — fixing a function that violates it
Work top-down. Never start by shuffling lines.
1. **Name the level.** In one sentence, write what this function is for at its own layer. If you cannot, the function has no clear level — split before polishing.
2. **Classify every statement.** Tag each line or block as: **core** (this layer's business), **down** (a detail a lower abstraction should own), **up** (a concern an upper / edge layer should own), or **cross-cutting** (log / metric / retry). Unlabeled lines are where the mess hides — do not "just leave them".
3. **Decide down vs. up for each foreign item.**
- Push **down** when it is a guarantee a lower building block can provide: a value that can only be constructed valid, a parser that returns a typed result, an infra helper that already retries. The business function then assumes validity and stays clean.
- Push **up** when it is about translating or reacting to failure for the outside world: status codes, messages, exit codes, aggregation of many errors. The edge layer catches once and maps; business code just signals.
- Rule of thumb: if removing it would change what the business rule says, it is core and stays; if removing it only changes how a failure is reported or a detail is computed, it moves.
4. **Extract, do not interleave.** Pull each foreign concern into its own named function or layer. Keep the original function as a readable sequence of same-level calls. For error handling specifically, separate the work body from the recovery body into distinct functions so neither clutters the other.
5. **Signal, do not handle, in the middle.** Mid-layer business functions throw / return and let the right layer react. Do not catch-and-log-and-continue in business code unless continuing is itself the business rule.
6. **Re-read for level.** After the moves, every remaining line should be explainable at the same altitude. If not, repeat from step 1.
Keep the change minimal: move the smallest thing that restores the level. Do not invent abstractions, frameworks, or generic "handler" machinery beyond what the function actually needs. Three straight-line, same-level calls beat a premature pipeline.
## Review method — applying the lens to a diff
Read each changed or touched function and, for each check, record only: **the hit (yes/no) plus evidence (`file:line`)**, and — where the check points at a construct — a raw factual count from "Quantify".
1. **Altitude check.** Are all lines at the same level of abstraction? Record each place where a "what" line is immediately followed by a "how" block (or vice versa) inside the same function, with `file:line`.
2. **Happy-path check.** Can you read the business intent top to bottom without stepping through error branches? Record whether error handling sits inline between business steps (yes/no + `file:line`), supported by raw counts from "Quantify" (e.g. number of `catch` clauses, `continue` statements).
3. **Ownership check.** For each validation, catch, mapping, log, retry: is this layer the rightful owner, or is it borrowed from above / below? Record each borrowed item with `file:line` and its destination (down / up), using the rules from the methodology.
4. **Layer-leak check.** Does a business function mention an external protocol (status code, exit code, UI text, wire field)? Does an edge function contain a business rule? Record each leak candidate with `file:line` and whether it names an *external* protocol or an *internal* domain shape.
5. **Explanation test.** Describe the function in one sentence with no "and". Record whether "and" was needed; if so, list the proposed split as candidate moves (down / up).
### Quantify — report only raw factual counts
Report only quantities that can be counted **mechanically from the text**. Anything that first requires classifying a line (core vs foreign, happy-path vs error-handling, high-level vs low-level) is recorded under detection (the five checks above) as evidence, not as a number here.
Report, per function:
- **Body size** — lines and/or statements of the function body; state the basis (e.g. "statements, excluding lone braces").
- **Control-flow keywords (raw counts)**`if`, `continue`, early `return`, `throw`, `try` / `catch` / `finally`, `await`, loops (`for` / `while` / `.forEach`).
- **Named syntactic shapes a check points at** — when a check cites a construct, count it verbatim and name the exact token: e.g. number of object literals, string literals, `.trim()` calls, `.length` reads, `origin.` property reads, spread `[...x]` operations.
- **Recovery presence (raw)** — number of `catch` clauses, and number of log / metric calls inside them.
Quantities that embed a prior classification — out-of-level vs core counts, guard-to-core ratios, happy-path vs error-handling volume, "repeated boundary checks a lower layer could guarantee once", "low-level literals in a high-level flow" — are captured as evidence under the relevant check (`file:line` + the verbatim tokens). A downstream rubric derives any ratio from those raw facts.
### Red flags
Record each as evidence (yes/no + `file:line`); these are candidates, not verdicts:
- A body that is mostly check-and-return / check-and-throw ladders around a thin core.
- A recovery block that logs, maps, and returns inline, sitting next to business steps.
- A function that both computes a value and decides how that value's failure is shown to the user.
- Low-level literals (byte offsets, header strings, format codes) inside a high-level workflow.
- A name that needs "And" / "Or" / "With" to be honest, or a name so vague ("handle", "process", "do") that it hides multiple levels.
- Catch-and-swallow that hides a failure the caller needed to see.
- Defensive null / format checks repeated at every call site instead of guaranteed once at the boundary.
### Severity grading belongs downstream
The agent's facts (detections, raw counts, directions, exemptions) feed a downstream grade; the agent reports those facts and stops there. Grades compress a continuous quantity into an uncalibrated three-point scale and are exactly where identical evidence gets labeled differently across runs. Grading happens above the agent:
- A **deterministic rubric** — a versioned threshold table over the raw counts from "Quantify"; or
- **Anchored examples** — the reviewer judges relative to repo-known reference functions rather than against an absolute adjective like "materially"; or
- A **human**, for items that land near a threshold boundary.
If a downstream consumer still asks the agent for a grade, the agent returns the underlying facts and the threshold band it would fall under, with `confidence: low` on boundary cases; the grade itself is produced downstream.
### How to report findings
Report **evidence + direction**. Lead with the location and the level, then the proposed move. Prefer "this block is one level lower than the rest of the function (`file:line`) — move it **down** into X" over "this is ugly" or "this is a request-changes". The destination layer (down into a value / parser / infra helper, or up into the edge handler) is the actionable output and the deliverable. Attach the "Quantify" numbers and any exemption flags to each finding.
## Exemption checklist
This is a lens, not a law. For each foreign concern, check whether any exemption below applies and **record the hit (yes/no) plus the reason**. The agent records exemptions as facts; a recorded exemption is then used downstream to cap the grade (e.g. to `Nit`) deterministically.
- **Tiny function:** the function is small enough that splitting would add indirection with no reader benefit.
- **Foreign concern is the single job:** the "foreign" concern is in fact the function's one purpose — a dedicated error mapper, a validator, an infra wrapper, or an index-bookkeeping helper whose low-level arithmetic *is* its level.
- **Atomicity / correctness / performance:** the steps genuinely must stay together (e.g. a re-check after an `await` to guard state that may have changed).
- **Edge-translator role:** an edge / handler function whose job is to translate an external event into internal indices; naming the wire fields is its job.
Keep a split that would make the code harder to read as a recorded candidate for downstream review. When the evidence lands on an exemption boundary, record both sides and set `confidence: low`.
## Output contract
Return, per function, items 15 only:
1. **Level statement** — one sentence: what the function is for at its own layer.
2. **Per-check results** — for each of the five review checks: `hit: yes/no`, evidence `file:line`, and (only where the check points at a construct) a raw factual count.
3. **Measurements** — the raw factual counts from "Quantify".
4. **Exemptions** — checklist hits (yes/no + reason).
5. **Proposed moves** — for each foreign concern: `file:line` → destination (down into X / up into Y). This is the actionable deliverable.
Severity grades, block/merge verdicts, and "worth splitting" calls live downstream, derived from items 14. When a consumer asks for a label, hand back items 14 and the threshold band, with `confidence: low` on boundary cases.

View file

@ -0,0 +1,115 @@
---
name: test
description: Use when writing or reviewing tests, or when asked how to write a good single test. Encodes the per-test rules behind the "test the contract / responsibility, not the implementation" principle — name and structure one behavior per `it`, drive through the public surface, stub only true external boundaries, control time and config via documented knobs, and keep tests clear, isolated, and refactor-resilient. The same rules drive both authoring (write mode) and auditing existing tests (review mode).
---
# Tests — write & review
Per-test rules that operationalize one principle: **test the contract / responsibility, not the implementation**. This is the how-to for a single `it`, and the lens for reviewing one.
## Two modes, one rule set
- **Write mode** — authoring a test. Apply the rules below to produce it.
- **Review mode** — auditing an existing test or test diff. Apply the same rules as a checklist; report each violation with `file:line`, the rule it breaks, and the fix. See "Review mode" near the end.
The rules are identical in both modes — only the posture changes (produce vs. audit).
## Test contract, not implementation
- Drive the system through its **public control plane** and assert on **observable effects** (returned values, persisted state, emitted events, injected messages), never on source details.
- Resolve collaborators through their contract — the interface plus its identifier — not the module that binds a concrete implementation.
- Do not reach into private fields or add backdoors "for testing". If you feel the need, the seam is wrong — fix the design, not the test.
## One behavior per `it`
Each `it` covers exactly one responsibility / scenario. If the name needs "and", split it.
```ts
it('returns 401 when the caller is unauthorized', ...);
it('does not double-fire when the same tick repeats', ...);
```
## Name and structure
- `describe('<slice> (<responsibilities>)'` — name the **responsibility**, not the class.
- An `it(...)` reads as a sentence, but it must still encode three things — the **behavior / method**, the **state or condition**, and the **expected outcome**: `it('<behavior> when <condition>, <outcome>')`. A name like `does X when Y` with no result is too vague to fail usefully.
- Use spaces, not the Java-style `method_state_outcome` underscores — that convention exists only because Java test methods cannot contain spaces. A string-named test reads fine as a sentence.
- Good: `it('returns 401 when the caller is unauthorized')` · `it('advances the cursor and does not double-fire on a repeat tick')`
- Bad: `it('works')` · `it('handles auth correctly')` — no condition, no outcome
- Arrange / Act / Assert. A short `// Given` `// When` `// Then` is fine when it aids reading; do not paste it mechanically on trivial tests.
## Build a small rig
When several tests share setup, write a factory (`rig()`, `createHost()`, whatever fits the codebase) that returns the **smallest surface the test needs**. Tests reach into the rig; they do not rebuild the world each time. Keep the rig dumb: wiring only, no assertions.
## Stub only the real external boundary
Default to real collaborators wired the way production wires them. Stub the **minimum seam** that is genuinely external:
- A remote / model / service boundary — spy on the contract method (the interface), and capture what the system sends across it. Do not stand up the real external thing.
- Network / other-process boundaries — stub at the boundary, not the internals.
- Time, timers, jitter — use the documented control knobs the system exposes (env, an injected clock, a manual tick). Do **not** use fake timers or real `setTimeout` to drive time.
- Env / config knobs are usually snapshotted at bootstrap — set them **before** building the system under test, and restore them in `afterEach`.
## Keep tests DAMP and keep cause next to effect
- DAMP over DRY: use **literal expected values** in assertions; do not compute the expectation with the same logic as the code under test.
- Keep the key preconditions inside the `it` (or its rig), where the reader can see cause next to effect. Reserve `beforeEach` for cross-cutting plumbing (env snapshot, cleanup), not for hiding the scenario's setup.
```ts
// Good — the expected value is a literal the reader can check.
expect(discount).toBe(15);
// Bad — re-derives the expectation; mirrors the implementation.
expect(discount).toBe(price * rate);
```
## Assert only what is relevant
Assert the effect that proves the contract. Use matchers / partial-object matching to ignore incidental fields. Do not assert internal counters, call orders, or shapes the user cannot rely on.
## Isolate and clean up (no flakes)
Every test must be hermetic and order-independent. In `afterEach`:
- restore every mock / spy
- restore every env var you touched (snapshot in `beforeEach`)
- dispose the host / container and reset its reference
No dependence on wall-clock time, run order, or leftover on-disk state — give each scenario its own isolated identity / workspace when state persists.
## Quality bar: CCCR
Before finishing, check each test against:
- **Clarity** — a stranger can tell what broke from the failure message alone.
- **Completeness** — covers the responsibility's success, error, and boundary paths.
- **Conciseness** — no duplicate or speculative cases; one scenario per `it`.
- **Resilience** — survives an internal refactor with no test change (because it asserts contract, not implementation).
## Per-file scenario header
Start each test file with a short header comment: the **scenario**, the **responsibilities** asserted, the **wiring** (which collaborators are real vs. the single stubbed boundary), and how to run it.
## Review mode — auditing existing tests
Apply the rules above as a checklist against each test in scope (a file, a diff, or a named `it`). For every hit, report `file:line` + the rule it breaks + the fix; do not rewrite unless asked. Lead with the contract question: *what observable behavior does this test prove, and would it survive a refactor?*
Check, in order:
1. **Contract, not implementation** — asserts observable effects, not private fields, call order, or internal shapes the user cannot rely on.
2. **One behavior per `it`** — the name carries behavior + condition + outcome; "and" in the name means a split is owed.
3. **Boundary discipline** — only the true external seam is stubbed; time is driven by documented knobs, not fake timers / real `setTimeout`.
4. **DAMP expectations** — expected values are literals, not re-derived by the code under test's logic.
5. **Isolation** — mocks / spies / env / host restored in `afterEach`; no wall-clock, run-order, or leftover on-disk dependence.
6. **CCCR read-through** — Clarity, Completeness (success / error / boundary), Conciseness, Resilience.
Report findings as evidence + fix, e.g. "`foo.test.ts:42` asserts on `service.internalMap` (contract) — assert the returned value instead." If a test passes the lens, say so briefly; silence on a rule means it held.
## Quick checklist (write & review)
- Resolved through the contract; no concrete-impl import
- One behavior per `it`; name carries behavior + condition + outcome; AAA
- Stubbed only the true external seam; time via knobs, not fake timers
- Literal expectations; relevant assertions only
- Mocks / env / host restored in `afterEach`; hermetic, no flakes
- CCCR read-through done

View file

@ -11,23 +11,34 @@ description: Use when generating changesets in the kimi-code repository, includi
All other `@moonshot-ai/*` packages are treated as internal packages, including `@moonshot-ai/kimi-code-sdk`, `agent-core`, `kosong`, `kaos`, `kimi-code-oauth`, `kimi-telemetry`, and `migration-legacy`.
`@moonshot-ai/pi-tui` is a special internal package: it is a private fork (`private: true`) that is never published, but it keeps its own changelog through changesets. It is an exception to Core Rule 4 — see the dedicated section below.
Only the CLI changelog gets a curated, user-facing presentation (the docs-site changelog sync). The SDK and other internal package changelogs are raw changesets output kept for version history — nobody curates them, so write those entries honestly and technically; their wording does not need to suit end users.
## Core Rules
1. **Inspect the actual changes first.** Use `git status` / `git diff --name-only` to identify which packages were actually changed.
2. **List packages that changesets can release.** If a changed package is ignored in `.changeset/config.json`, do not put that ignored package in frontmatter together with a non-ignored package; changesets rejects mixed ignored/non-ignored frontmatter.
3. **Map ignored internal changes to the affected released package.** If an ignored internal package changes CLI output or behavior, list `@moonshot-ai/kimi-code` and describe the actual user-visible or release-artifact change in the changelog text.
4. **Internal package source changes that enter the CLI bundle must manually list the CLI.** `@moonshot-ai/kimi-code` inline-bundles `@moonshot-ai/*` source, but those internal packages are devDependencies from the CLI's perspective, so changesets will not automatically propagate bumps. If a change enters the CLI output, list `@moonshot-ai/kimi-code`.
- **Web app (`@moonshot-ai/kimi-web`) changes always enter the CLI bundle.** `@moonshot-ai/kimi-web` is ignored by changesets (see `.changeset/config.json`) and cannot be mixed with `@moonshot-ai/kimi-code` in one changeset frontmatter. Describe the web change in the changelog text, but list `@moonshot-ai/kimi-code` so the CLI release carries the bundled `dist-web` output.
4. **Internal package source changes that enter the CLI bundle must manually list the CLI — when they get a changeset at all.** `@moonshot-ai/kimi-code` inline-bundles `@moonshot-ai/*` source, but those internal packages are devDependencies from the CLI's perspective, so changesets will not automatically propagate bumps. If a change enters the CLI output and is user-perceivable, list `@moonshot-ai/kimi-code`. See rule 6 for when to skip the changeset entirely.
5. **Docs-only and tests-only changes usually do not need a changeset.** README, internal docs, and `test/` changes that do not enter package output do not trigger a CLI bump.
6. `@moonshot-ai/vis` / `vis-server` / `vis-web` are ignored by changesets and should not be handled.
6. **Skip changes users cannot perceive — write no changeset at all.** The CLI changelog is user-facing; a changeset is a changelog entry, not a shipping gate. Internal changes merged to `main` still ship in the next release triggered by any user-facing changeset, so skipping the changeset loses nothing. Do not write changesets for:
- `agent-core-v2` internal architecture: new services, refactors, config-persistence or journal/wire mechanisms.
- `kap-server` WebSocket / REST protocol changes consumed only by the bundled web UI, kimi-inspect, or other dev tooling (new endpoints, subscribe protocols, stream baselines).
- Behavior that only takes effect on the experimental engine (e.g. experimental `kimi -p`), unless it exposes documented user configuration such as a `config.toml` section or env vars that also work on a shipped surface (TUI or `kimi web`).
- When unsure whether users can perceive a change, ask before writing.
7. `@moonshot-ai/vis` / `vis-server` / `vis-web` are ignored by changesets and should not be handled. `@moonshot-ai/kimi-inspect` (a private dev app that never ships) is likewise ignored and must never appear in a changeset frontmatter.
## Workflow
1. List the changed packages and check whether each one is ignored by `.changeset/config.json`.
2. Choose a bump level for each package.
3. If an ignored internal package change enters the CLI bundle, put `@moonshot-ai/kimi-code` in frontmatter instead of mixing the ignored package into the same changeset.
4. Create a short kebab-case file under `.changeset/`.
5. Split unrelated changes into separate changesets; keep one logical change in one file.
2. Decide whether the change is user-perceivable (Core Rule 6); if not, stop — no changeset.
3. Choose a bump level for each package.
4. If an ignored internal package change enters the CLI bundle, put `@moonshot-ai/kimi-code` in frontmatter instead of mixing the ignored package into the same changeset.
5. Create a short kebab-case file under `.changeset/`.
6. Split unrelated changes into separate changesets; keep one logical change in one file.
Before a release, review the accumulated `.changeset/` entries against Core Rule 6 and prune non-user-facing ones; the release PR regenerates from `.changeset/` on `main`, so deleting a changeset removes its changelog entry without affecting the shipped code.
Format:
@ -50,6 +61,8 @@ Format:
When in doubt between `patch` and `minor`: if the change improves an existing feature and the user-facing impact is small, choose `patch` even when the change is technically "new". Reserve `minor` for a substantial new capability that introduces something users could not do before.
New configuration surface is not automatically `minor`. Additions to an existing feature's configuration — env var overlays, config-file fallbacks, global defaults under per-item settings — are `patch`. Examples: a global default MCP timeout when per-server timeouts already exist; env-based credentials for a service already configurable in `config.toml`.
### Major Rule
Never write `major` on your own.
@ -59,13 +72,26 @@ If you believe a change qualifies as major, stop first, explain why, and ask the
## Wording Rules
- Changelog entries **must be written in English**.
- **Keep it short — ideally a single sentence that states what was done.** Do not write a paragraph, do not pile on technical detail, and do not enumerate every sub-change.
- **Keep the whole entry concise.** Aim for one short sentence that states what was done; at most a short sentence plus a one-line usage hint. Do not write a paragraph, do not pile on technical detail, and do not enumerate every sub-change.
- **For new user-facing features, append a brief usage hint** so users know how to try it. Keep it to a single short line — a command name, a subcommand, a flag, or a one-line "how to use". Do not explain design rationale or list edge cases. Skip the hint for bug fixes, internal changes, and refactors.
- Slash command: `Add the /foo slash command to list active sessions. Run /foo to see them.`
- CLI subcommand: `Add the kimi web subcommand to open the web UI. Run kimi web to launch it.`
- Flag: `Add a --bar flag to skip confirmation prompts. Pass --bar to skip.`
- Too long: `Add the /foo command to list active sessions. It accepts an optional --all flag to include background sessions, supports filtering by name with /foo <name>, and writes the result to the transcript...`
- User-facing CLI wording should only be used when CLI users can perceive the change.
- Internal changes that do not affect CLI users can still share a changeset with the CLI, but the wording must describe the real change honestly and must not present it as a user-facing feature.
- Do not mention file names, class names, function names, PR numbers, or commit hashes.
- Do not include real internal endpoints, key names, account names, or service names. If an example is needed, use neutral placeholders such as `example.com`, `example.test`, or `YOUR_API_KEY`.
- Avoid vague words such as `refactor`, `optimize`, and `improve`. Describe the actual change, or use more specific wording.
## When You Are Unsure About a Change
Generate the changeset from what the diff clearly shows. If part of a change is unclear and you cannot confidently describe what it does for users, do not guess or pad the entry with vague wording.
1. Finish the changeset for the parts that are clear.
2. Then ask the user once, in a short list: name the specific change(s) you do not understand, and ask whether you may dig into the repository (read related source, tests, or call sites) to describe it more accurately.
3. Only read more code after the user agrees. If the user says no or does not reply, keep the concise wording you already have and do not invent detail.
## Common Examples
An internal package fixes a bug visible to CLI users:
@ -78,6 +104,36 @@ An internal package fixes a bug visible to CLI users:
Fix occasional loss of tool call results in long conversations.
```
A new user-facing slash command (note the short usage hint):
```markdown
---
"@moonshot-ai/kimi-code": minor
---
Add the /foo slash command to list active sessions. Run /foo to see them.
```
A new CLI subcommand:
```markdown
---
"@moonshot-ai/kimi-code": minor
---
Add the kimi web subcommand to open the web UI. Run kimi web to launch it.
```
A new flag on an existing command:
```markdown
---
"@moonshot-ai/kimi-code": patch
---
Add a --bar flag to skip confirmation prompts. Pass --bar to skip.
```
An internal package has an internal-only change, but it enters the CLI bundle:
```markdown
@ -98,44 +154,48 @@ Only SDK source changed, and the CLI does not use it:
Clarify session status typing for internal SDK callers.
```
## Web app changes
## `@moonshot-ai/pi-tui` changes
`@moonshot-ai/kimi-web` is ignored by changesets and must **never** appear in a changeset frontmatter. Because the web app is bundled into the CLI release artifact, any web change that ships must list `@moonshot-ai/kimi-code` instead and describe the actual web-facing change in the text.
`@moonshot-ai/pi-tui` is a vendored fork that lives in `packages/pi-tui`. It is `private: true` and is never published, but it is **not** ignored by changesets: changesets versions it and writes `packages/pi-tui/CHANGELOG.md` so the fork keeps its own history. Because it is bundled into the CLI like other internal packages, it is an exception to Core Rule 4 — do **not** list `@moonshot-ai/kimi-code` for a change that only touches pi-tui.
- If a PR contains both web UI changes and server API changes, split them into separate changesets so each entry has a focused description.
- Do not enumerate every micro-tweak; keep it to one sentence that captures what the web user gets.
- Changes that only affect pi-tui (build, package, strict-mode cleanup, renderer fixes): list `@moonshot-ai/pi-tui` only. No CLI changeset.
- If the same change is also user-visible in the CLI (for example a terminal rendering fix that CLI users can see), add a **separate** changeset that lists `@moonshot-ai/kimi-code` with CLI-focused wording, in addition to the pi-tui changeset. Do not mix both packages in one frontmatter — the two changelogs need different wording.
Web-only fix:
pi-tui-only change:
```markdown
---
"@moonshot-ai/pi-tui": patch
---
Export the package manifest so the bundled binary can locate its native assets.
```
pi-tui change that is also visible in the CLI (two separate changesets):
```markdown
---
"@moonshot-ai/pi-tui": patch
---
Clamp the differential render to the visible viewport so scrolling up during streaming no longer jumps to the top.
```
```markdown
---
"@moonshot-ai/kimi-code": patch
---
Fix the web chat not scrolling to the bottom after sending a message.
```
Web UI plus server APIs in the same PR (split into two changesets):
```markdown
---
"@moonshot-ai/kimi-code": minor
---
Add the server-hosted web UI, including chat layout and session list behaviors.
```
```markdown
---
"@moonshot-ai/kimi-code": minor
---
Add the server REST and WebSocket APIs that power the web UI.
Fix the transcript jumping to the top when scrolling up through history during streaming output.
```
## Red Flags
- You are about to write `major` without asking the user.
- You are writing a changeset for something users cannot perceive — `agent-core-v2` internals, `kap-server` WS/REST protocol plumbing, experimental-engine-only behavior. Skip the changeset instead (Core Rule 6).
- A new env var overlay or config fallback for an existing feature is bumped `minor` — configuration additions to existing features are `patch`.
- A new user-facing feature entry has no usage hint, or the hint runs to multiple lines and explains design rationale.
- You guessed wording for a change you do not understand instead of asking the user whether you may dig into the repo.
- Internal package source enters the CLI bundle, but `@moonshot-ai/kimi-code` is missing.
- A changeset frontmatter mixes ignored internal packages with non-ignored packages.
- `packages/node-sdk` was not changed, but `@moonshot-ai/kimi-code-sdk` was listed for "internal package sync".
@ -143,3 +203,4 @@ Add the server REST and WebSocket APIs that power the web UI.
- The wording claims more than the diff actually did.
- The CLI wording mentions internal package names, class names, or PR numbers.
- The entry includes real internal identifiers instead of neutral placeholders.
- A change that only touches `@moonshot-ai/pi-tui` lists `@moonshot-ai/kimi-code` instead of `@moonshot-ai/pi-tui`, or mixes both packages in one frontmatter.

View file

@ -37,9 +37,11 @@ If the CLI changelog is not in the diff (for example an SDK-only release), stop
Process the version block exactly as `sync-changelog` does for the docs site, but only in memory:
- **Strip** (`sync-changelog` step 3): drop the H1, the `### Patch Changes` / `### Minor Changes` / `### Major Changes` subheadings, PR links, and commit-hash links; keep only each entry's body text.
- **Classify** (`sync-changelog` step 4): bucket into Features / Bug Fixes / Polish / Refactors / Other; order within each section by reader value.
- **Translate** (`sync-changelog` step 6): translate entry bodies to Chinese; section headings become 新功能 / 修复 / 优化 / 重构 / 其他.
- **Strip** (`sync-changelog` step 3): drop the H1, the `### Patch Changes` / `### Minor Changes` / `### Major Changes` subheadings, PR links, and commit-hash links; keep only each entry's body text. The `Thanks [@user](...)!` credit (including the multi-author form) must be removed every time. Within each entry, drop SDK-only and provider-internal sentences (SDK capability mapping / API exposure, provider wire-format mechanics, internal XML markers, hook/event payload mechanics such as what an event reports or carries) and keep only the user-facing effect and required constraints.
- **Merge and deduplicate** (`sync-changelog` step 4): merge micro-tweaks to the same surface into one higher-level entry; when three or more fixes target the same UI area or the same class of problem, merge them into one higher-level fix entry (do not merge broad or genuinely distinct fixes); and drop a server/API entry that only backs a web feature already listed.
- **Collapse low-signal entries** (`sync-changelog` step 4): keep standalone only entries that pass both gates — the reader-action test (the reader must do or re-evaluate something) and the channel test (the product cannot push it into the user's path: hidden controls, habit invalidations, capabilities users would not know to seek — a control merely sitting in the UI is not surfacing, users do not explore). Polish keeps only must-react items; experiences the product shows at the moment of need (recovery cards, post-install guidance) fold. Fixes keep only behavior-change entries (readers must update a habit, config, or workaround); loud failures fold (the fix itself notifies the victim), and silent past damage folds too — the changelog does not repair the past, and a notice that names no locatable instance and no realistic action is noise, not diligence. Section sizes follow density defaults (about 2 polish, 3 fixes) that yield to genuinely qualifying entries — flag the overflow for the reviewer instead of folding to hit the number. Fold everything else into one catch-all line placed last under 修复 — `修复了一些已知问题。` (or `修复了一些已知问题,并做了若干细节优化。` when non-fix entries were also collapsed; when nothing folded is a fix, place it under 优化 instead as `做了若干细节优化和内部改进。`), followed by a separate pointer sentence: `更详细的变更记录见 [GitHub](https://github.com/MoonshotAI/kimi-code/blob/main/apps/kimi-code/CHANGELOG.md)。` (file link, no version anchor; before the release PR merges, the target does not yet contain this version's block — expected for a preview).
- **Classify** (`sync-changelog` step 4): bucket into Features / Bug Fixes / Polish / Refactors / Other; order within each section by reader value (in Polish, user-visible improvements before protocol/internal adjustments).
- **Translate** (`sync-changelog` step 6): translate entry bodies to Chinese; keep one sentence per entry with a parallel rhythm within a section; section headings become 新功能 / 修复 / 优化 / 重构 / 其他.
If an upstream entry is not in English, flag it and stop (changeset entries must be English).
@ -47,6 +49,10 @@ If an upstream entry is not in English, flag it and stop (changeset entries must
Print the preview directly. Use `<version>(预览)` as the heading because the version is not released yet. Write `无` for empty sections. Do not write any file.
After the preview block, append a reviewer-only section titled `### 审稿参考(不进入文档)`: list every entry folded into the catch-all (short English title, one line each), note any section that exceeds the density defaults, and flag borderline calls for the reviewer to confirm. This breakdown is how reviewers see what was folded — before merge, the catch-all pointer's target does not yet contain the version's block. Never write this section into the docs pages.
The preview is pasted into chat tools (for example Lark), where relative docs links do not resolve. Rewrite every docs link to its absolute published URL: map `../<path>.md[#anchor]` to `https://moonshotai.github.io/kimi-code/zh/<path>.html[#anchor]` — for example `../configuration/config-files.md#loop-control``https://moonshotai.github.io/kimi-code/zh/configuration/config-files.html#loop-control`. Never emit raw relative paths, and never wrap a link in backticks; code-style the link text inside the brackets instead ([`loop_control`](...)).
```
发版 PR: <url>

View file

@ -93,13 +93,13 @@ Use upstream order: newest version first.
Upstream entries look like this:
```markdown
- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) - Clean up lint warnings ...
- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) Thanks [@user](https://github.com/...)! - Clean up lint warnings ...
```
Keep:
Changesets may add a `Thanks ...!` credit, but it must be removed every time. Keep:
- Version headings such as `## 0.2.0`.
- Only the body text of each entry, after the PR/hash decoration.
- Only the body text of each entry, after the PR/hash decoration and any `Thanks ...!` credit have been removed.
Remove:
@ -107,29 +107,55 @@ Remove:
- Changesets subheadings such as `### Patch Changes`, `### Minor Changes`, and `### Major Changes`.
- PR links such as `[#317](...)`.
- Commit hash links such as ``[`2f51db4`](...)``.
- The `Thanks [@user](...)!` credit, including the multi-author form `Thanks [@a](...), [@b](...)!`. Drop the whole `Thanks ...!` segment every time, regardless of whether the feature is enabled.
After stripping, each entry should be only:
After stripping, each entry is `- <body text>`.
```markdown
- <body text>
```
Drop SDK-only and provider-internal detail. This changelog serves `@moonshot-ai/kimi-code` CLI and web users. Within an entry, keep only what CLI/web users can perceive, and remove sentences that document internals instead of user-visible behavior. Apply this on both the English and Chinese pages:
- Drop sentences about how the SDK maps a capability, builds model aliases, or exposes a flag through an API such as `getExperimentalFeatures()` — that belongs in the SDK changelog, not here.
- Drop provider / wire-format implementation mechanics (XML markers like `<tools_added>`, protocol field explanations, "the wire protocol is unchanged", cache-hit mechanics) unless they are the behavior a user perceives.
- Drop hook/event payload mechanics — clauses about what extra fields an event payload carries or what an event reports in a specific case (for example "enrich hook payloads with the session title and client type", "`SessionEnd` reports `archive` when a session is archived"). Keep the new events or capability itself and how to configure it.
- Keep the user-facing effect and any constraints users must follow (for example "question texts must be unique").
Do not change facts or drop a real user-facing behavior — only trim the internal-only scaffolding. For over-long, internal-heavy entries, this trim applies on the English page too, not only in translation.
Upstream language rule: `gen-changesets` requires changelog entries to be English. If the upstream CLI changelog contains a non-English entry, stop and report it to the user. Do not silently rewrite it while syncing docs.
Public-text rule: do not copy real internal endpoints, key names, account names, or service names into docs changelogs. Replace examples with neutral placeholders such as `example.com`, `example.test`, or `YOUR_API_KEY` while preserving the user-visible meaning.
### 4. Classify Entries
### 4. Merge, Deduplicate, And Classify Entries
Before classifying, merge related entries and drop redundant ones from the user-facing changelog:
- **Curate for end users: collapse low-signal entries into one catch-all line.** The docs changelog is the only curated, user-facing outlet; the full entry list always remains in the upstream package changelog, so hiding detail here loses nothing. Apply two gates to every candidate entry. Gate 1, the reader-action test: **after reading this, is there something the reader must do, or something they must re-evaluate?** Gate 2, the channel test: **is the changelog the only channel that can deliver this?** The changelog is the channel of last resort — when the product itself surfaces the information in context, at the moment of need, to exactly the affected users, the entry is redundant no matter how real the improvement is. "Surfaced" means pushed into the user's path, not merely present on screen: an event-triggered card, prompt, or post-install screen forces the encounter, while a toggle, menu item, command, or settings page only waits to be found. Users do not explore — a capability that lives only in ambient UI is effectively undiscoverable, so the changelog must announce it. What in-product surfacing cannot deliver: hidden controls (env vars, config keys, opt-out flags nobody would find unprompted), invalidations of existing habits or expectations (in-product discovery comes as confusion), and capabilities users would not know to seek. An entry that fails either gate folds. Anchor both gates to the changelog's reader, never to the bug's victim: someone who hit a loud failure does not need the changelog to confirm the fix — the product working again is the notification — and a reader who never hit it gets nothing from the entry.
- `Features`: keep when users would try it or must react to it — new capabilities create demand readers did not know to seek. Collapse only behavior that takes effect solely behind an experimental flag.
- `Polish`: keep only must-react items — a notification users may want to turn off, a behavior change to a command they already use, a default flip with an opt-out. Fold improved experiences the product surfaces in context (recovery cards, post-install guidance, progress or status displays): they are discovered at the moment of need, and pre-reading about them helps nobody. Also fold subtle or transient tweaks (status wording, spacing, animations) and internal-behavior adjustments — nobody acts on them.
- `Bug Fixes`: keep only **behavior-change** fixes — the fix changes how something works going forward, so readers must update a habit, a config, or a widely-adopted workaround. Everything else folds, for one of two opposite reasons. Loud failures (crashes, refusals, interrupted runs): the fix itself notifies whoever was hit — announcement value falls as bug visibility rises. Silent past damage (dropped data, wrong results the user never noticed): the changelog cannot repair the past, and in this product the notice names no locatable instance and no realistic action — users cannot enumerate which old sessions were affected, and they do not audit finished sessions; a "some past outputs may be wrong" line is anxiety without an outlet, not diligence. The rare exception is a retrospective notice with a concrete, locatable action (for example rotating a token after a credential-handling flaw); keep those. Never keep a fix merely because it was severe, and never keep one because the bug class feels important.
- Do not grade entries by engineering importance. Severity and effort are already represented upstream; the curated changelog is not a credit ledger — its only job is to change what the reader does or knows.
- **Density, not quota.** Standalone sections stay short so the changelog actually gets read — as a default, expect about 2 Polish and 3 Bug Fixes entries per version, while `Features` is gated by the test alone and has no count. The defaults yield whenever more entries genuinely pass the reader-action test: keep them and flag the overflow for the human reviewer; never fold a qualifying entry just to hit the number, and never pad a section to reach it. The reviewer owns the final cutoff — the curator's job is to surface the borderline calls, not to resolve them silently.
- Everything else collapses into a single catch-all bullet placed last under `Bug Fixes`: `Fix several known issues.` When entries beyond fixes were also collapsed, use `Fix several known issues and make various refinements.` instead (Chinese: `修复了一些已知问题。` / `修复了一些已知问题,并做了若干细节优化。`). End the catch-all line with a pointer to the upstream file so folded entries stay reachable, phrased as a separate short sentence — `See the [changelog on GitHub](https://github.com/MoonshotAI/kimi-code/blob/main/apps/kimi-code/CHANGELOG.md) for more technical entries.` (Chinese: `更详细的变更记录见 [GitHub](https://github.com/MoonshotAI/kimi-code/blob/main/apps/kimi-code/CHANGELOG.md)。`). Link the file itself, never a per-version anchor — GitHub's generated heading anchors are fragile. Keep the pointer wording restrained ("more technical entries"): upstream only contains changes that received a changeset, so never claim the list is complete.
- If no fix survives, the `Bug Fixes` section is the catch-all line alone; if the whole version has no user-facing change, the version block is a single section with that line. Match the catch-all to what was folded — never claim fixes that did not happen: when the folded entries include fixes, use the forms above under `Bug Fixes`; when everything folded is polish or internal work, place the catch-all under `Polish` as `Make several refinements and internal improvements.` (Chinese: `做了若干细节优化和内部改进。`).
- **Merge micro-tweaks to the same surface.** Collapse several small tweaks to the same UI area or feature into one concise entry at the higher level. For example, "change the composer's default height" and "change the composer's default font" merge into "Polish the composer's default styling." Use the most specific common ancestor (composer, settings page, tool card, and so on). Classify the merged entry by its combined effect
- **Merge same-surface or same-kind fixes when you have three or more.** The `Bug Fixes` section tends to accumulate many narrow UI/polish fixes that read as noise when listed one by one. When three or more fixes target the same area (for example several tool cards in the TUI, or the web session/conversation surface) or the same class of problem (for example several "jumping/flickering/collapsing during streaming" fixes), merge them into one higher-level entry. Examples:
- "Fix the Bash tool card collapsing...", "Fix the Edit tool card jumping in height...", "Fix the Edit tool card flickering while its result streams in" → "Fix several TUI tool cards jumping, flickering, or collapsing in height when results stream in or end with short output."
- "Fix the collapsed sidebar not hiding...", "Stop the chat history from replaying its entrance animation...", "Fix tool components jumping the conversation when expanded/collapsed" → "Fix several layout and display glitches when switching sessions, including the collapsed sidebar not hiding, the chat history replaying its entrance animation, and tool components jumping the conversation."
- Classify the merged fixes as `Bug Fixes`.
- **Do not over-merge.** Leave a fix standalone when it is broad, high-value, or genuinely distinct (for example model/provider tool-calling bugs, session-list corruption, file-completion gaps). Merging is for low-reader-value, similar-shape fixes that read as a wall of similar bullets. A merged fix entry must still pass the standalone test from the catch-all rule above; if the merged group is low-signal too, fold it into the catch-all line instead of listing it.
- **Drop server/API plumbing covered by a web entry.** If one entry adds a web UI feature (for example, an Archived sessions page) and another entry only adds the server or REST/WebSocket endpoints that exist solely to power that web feature, keep the web UI entry and drop the API entry. CLI and web users perceive the web page; the backing API is implementation detail with no independent user value on this changelog. Keep the API entry only when it has independent user value — a new public endpoint that SDK or server consumers call directly, or a capability usable outside the web feature. When unsure, keep both and let the reviewer decide.
The docs changelog uses five section types:
| English section | Chinese section | Meaning |
|---|---|---|
| `### Features` | `### 新功能` | New user-facing functionality, such as a new command, flag, mode, or capability that did not exist before |
| `### Bug Fixes` | `### 修复` | Fixes for behavior that was broken |
| `### Polish` | `### 优化` | User-visible improvements to existing functionality, including UX adjustments, behavior tweaks, and performance improvements that are not fixes or new capabilities |
| `### Bug Fixes` | `### 修复` | Fixes for behavior that was broken |
| `### Refactors` | `### 重构` | Internal changes with no user-visible behavior change, including build, CI, tests, dependency cleanup, and internal renames |
| `### Other` | `### 其他` | Anything that does not fit above, such as CDN/endpoint swaps and docs-related artifacts |
With the catch-all rule above, `Refactors` and `Other` rarely appear in newly synced versions: entries with no user-perceivable effect fold into the catch-all, and an entry that does change user-perceivable default behavior (for example an engine default flip with an opt-out flag) is classified by that effect, usually `Polish`. Reserve `Other` for genuinely unclassifiable but user-facing entries. Older versions keep whatever sections they already have — do not rewrite history.
Classification process:
1. Classify from the stripped entry text first.
@ -140,6 +166,8 @@ Classification process:
Features vs. Polish: ask whether the entry introduces something the user could not do before. If yes (new command, flag, mode, viewer, or capability), use `Features`. If it only improves an existing surface (a UI panel that already existed, an existing prompt, an existing tool card, an existing payload pipeline), use `Polish`. Verbs like `Add` do not automatically mean `Features` — a small visual addition to an existing UI is still polish.
Default-behavior changes: changing the default value of an existing capability (for example flipping a feature on by default) is usually `Polish`, because the capability already existed. Use `Features` only when the new default materially changes the out-of-box experience for most users in a way they could not get before. When genuinely ambiguous, flag it and confirm with the reviewer rather than guessing.
Keyword hints:
- **Features**: `Add ... command/flag/option/mode/viewer`, `Introduce`, `Support`, `Allow`, `Enable`, `Implement`, `New ... command/flag/option`
@ -151,14 +179,15 @@ Keyword hints:
Within each version, section order is:
```text
Features → Bug Fixes → Polish → Refactors → Other
Features → Polish → Bug Fixes → Refactors → Other
```
Omit empty sections. Within each section, order entries by reader value, not upstream order:
1. Put the most valuable, obvious, and larger changes first.
2. Prefer broad user-visible features, workflow-changing fixes, high-frequency bugs, and large cross-cutting improvements over small polish, narrow edge cases, and internal cleanup.
3. If entries have similar value, preserve upstream order.
3. Within `Polish`, put directly user-visible UX or performance improvements (something users can see or feel) before protocol or internal-behavior adjustments (something that makes the model or pipeline behave more reliably but is invisible to users).
4. If entries have similar value, preserve upstream order.
Do not reword or exaggerate entries just to make them look more important; only reorder existing entries.
@ -203,6 +232,8 @@ Example:
- Update the native release workflow to use current GitHub artifact actions.
```
Doc links: an entry that changes a documented config surface may end with a pointer to the docs page — `see [X](...) for details` (Chinese: `详见 [X](...)。`). Keep it a real Markdown link into the docs tree with a relative path (for example `../configuration/config-files.md#loop-control`). When the link text is a config key or another identifier, code-style the text inside the brackets: [`loop_control`](../configuration/config-files.md#loop-control). Never wrap the whole link in backticks — `` `[loop_control](...)` `` renders as raw inline code that exposes the relative path instead of a clickable link.
### 6. Translate The Increment Into Chinese
After updating the English page, translate only the newly added English content into `docs/zh/release-notes/changelog.md`.
@ -231,6 +262,53 @@ Chinese page requirements:
- Translate only entry body text. Do not add entries that are not present in English.
- Follow `docs/AGENTS.md` for Chinese typography: full-width punctuation, spaces between Chinese and English, and the glossary.
#### Chinese wording style
Structural fidelity does not mean literal translation. The Chinese entries should read like a concise, idiomatic Chinese changelog. Keep the same facts as the English entry, but rephrase for natural Chinese prose.
Guidelines:
- **One entry, one sentence.** Avoid chaining multiple effects with commas or semicolons. If the English entry is long, split it into shorter sentences or keep only the most important effect.
- **Drop SDK-only and provider-internal detail.** Apply the trim from step 3 while translating: keep the user-facing effect and required constraints, drop SDK-mapping sentences, provider / wire-format mechanics, and internal XML markers. A long internal entry should collapse to one short Chinese sentence about what the user gets.
- **Prefer common changelog verbs**: 新增、支持、修复、优化、改进、调整.
- **Avoid indirect "through... make..." structures**. Do not write "通过 X使 Y"; prefer direct cause-effect or just state the result.
- Bad: `通过缓存已渲染消息行,使终端在长篇对话中保持响应。`
- Better: `缓存已渲染消息行,提升长对话下终端的响应速度。`
- **Be specific, not vague**. Prefer concrete actions over abstract quality words.
- Bad: `加固默认系统提示词和内置工具描述。`
- Better: `优化默认系统提示词与内置工具描述,避免 Agent 阻塞后台任务。`
- **Name concrete files or config keys when it helps clarity**.
- Bad: `插件现在可以在其清单中声明 hooks。`
- Better: `插件现支持在 kimi.plugin.json 中声明生命周期 hooks。`
- **Include required argument placeholders in CLI options**.
- Bad: `--allowed-host`
- Better: `--allowed-host <host>`
- **Keep usage hints to one short clause**.
- Bad: `传入 --allowed-host 以允许额外的 host。例如 ... (多句展开)`
- Better: `例如 kimi web --allowed-host example.com。`
- **Do not translate technical identifiers**: keep command names, flag names, file names, env vars, config keys as-is.
- **Keep parallel rhythm within a section.** When several entries fix similar web surfaces (layout, animation, sizing), phrase them with a consistent structure (for example 修复 <问题>,现 <行为>) so the section reads as a tidy list rather than a mix of shapes.
Example — translating a feature entry:
English source:
```markdown
- Add a --allowed-host flag to kimi web that lets extra Host header values pass the DNS-rebinding check, and include allow guidance in the 403 error message. Pass --allowed-host <host> to allow an extra host.
```
Before (literal, wordy):
```markdown
- 为 `kimi web` 新增 `--allowed-host` 标志,允许额外的 Host 请求头值通过 DNS 重绑定检查,并在 403 错误消息中包含允许指引。传入 `--allowed-host <host>` 以允许额外的 host。例如 `kimi web --allowed-host example.com`
```
After (concise, idiomatic):
```markdown
- `kimi web` 新增 `--allowed-host <host>` 选项,可将指定 Host 加入 DNS 重绑定白名单403 错误会提示如何通过 `--allowed-host``KIMI_CODE_ALLOWED_HOSTS` 放行,例如 `kimi web --allowed-host example.com`
```
### 7. Verify
Review:
@ -246,8 +324,11 @@ Check:
- Each version has the same section set and order on both pages.
- Each section has the same number of entries on both pages.
- Within each section, the most valuable, obvious, and larger entries appear before smaller or narrower entries.
- Low-signal entries were collapsed into the single catch-all line, placed last under `Bug Fixes` — or under `Polish` when nothing folded is a fix (both the reader-action test and the channel test applied); the catch-all wording matches what was folded and never claims fixes that did not happen; section sizes stay within the density defaults (about 2 Polish, 3 Bug Fixes) unless extra qualifying entries were deliberately kept and flagged for review. The catch-all line ends with the upstream changelog pointer (file link, no version anchor).
- PR links and commit hashes were stripped.
- No `Thanks ...!` credit remains (remove it every time).
- Real internal identifiers were replaced with neutral placeholders.
- Doc links are real Markdown links (code-styled text inside the brackets when needed), never wrapped in backticks.
- There are no empty sections.
- Markdown indentation and blank lines are intact.
@ -272,7 +353,7 @@ If the user chooses review:
git diff docs/en/release-notes/changelog.md docs/zh/release-notes/changelog.md
```
2. Summarize synced versions, section counts, and anything that needed manual classification.
2. Summarize synced versions, section counts, and anything that needed manual classification. List every entry folded into a catch-all line (short titles, one line each), any section that exceeds the density defaults, and every borderline call flagged during curation — the reviewer cannot own a cutoff they cannot see.
3. Tell the user to reply when they are done reviewing, or to ask for edits.
4. Do **not** commit, push, or open a PR until the user explicitly says review is complete, or asks to proceed.
@ -362,6 +443,18 @@ Return the PR URL to the user when done.
|---|---|
| Adding entries directly to the English docs page without reading upstream | Use `apps/kimi-code/CHANGELOG.md` as the source |
| Copying PR links or commit hashes into docs | Strip them; keep only body text |
| Leaving the `Thanks ...!` credit in docs | Remove it every time, including the multi-author form |
| Leaving near-duplicate micro-tweaks as separate bullets | Merge small tweaks to the same surface into one higher-level entry (e.g. composer height + font → composer's default styling) |
| Listing many narrow fixes to the same surface as separate bullets | When three or more fixes target the same UI area or the same class of problem, merge them into one higher-level fix entry; keep genuinely distinct or high-value fixes standalone |
| Listing low-signal fixes or internal changes as standalone bullets | Collapse them into the single catch-all line (`Fix several known issues.`) placed last under Bug Fixes; treat the section-size defaults (about 2 Polish, 3 Bug Fixes) as a density guard, not a quota |
| Folding a qualifying entry just to hit the section-size default | The defaults are density guards; keep entries that genuinely pass the reader-action test and flag the overflow for the human reviewer |
| Keeping a fix because it was severe or hard-won | Severity makes the announcement redundant — the fix itself notifies whoever was hit; keep only behavior-change fixes and retrospective notices with a concrete, locatable action |
| Keeping an improvement the product surfaces in context (recovery cards, post-install guidance, progress displays) | The product is the better channel — right users, moment of need; fold it (channel test) |
| Folding a new capability because its control is visible somewhere in the UI | Visible is not discoverable — users do not explore; a toggle, menu item, or settings page that only waits to be found needs the changelog announcement |
| Keeping a silent-impact fix out of diligence (dropped data, wrong results the user never noticed) | The changelog does not repair the past; if the notice names no locatable instance and no realistic action, it is anxiety without an outlet — fold it |
| Overstating the catch-all pointer (for example claiming the upstream changelog is complete) | Keep the pointer restrained — `See the [changelog on GitHub](...) for more technical entries.`; upstream only contains changes that received a changeset |
| Writing `Fix several known issues.` when nothing folded is a fix | Never claim fixes that did not happen; all-polish/internal folds go under Polish as `Make several refinements and internal improvements.` |
| Listing a server/API entry that only backs a web feature already listed | Drop the API entry and keep the web UI entry, unless the API has independent user value |
| Rewording upstream English entries | Upstream is frozen; copy the body text unless the user explicitly asks otherwise |
| Leaving English text untranslated in the Chinese page | The Chinese page must be fully Chinese except preserved technical terms |
| Editing upstream changelog text | Do not edit upstream |
@ -375,6 +468,8 @@ Return the PR URL to the user when done.
| Leaving empty sections | Delete sections with no entries |
| Putting everything under Other for convenience | Classify what can be classified first |
| Translating tool names, command names, or config keys | Keep them as written |
| Wrapping a whole doc link in backticks | Code-style the link text inside the brackets instead, so the link stays clickable: [`loop_control`](...) |
| Keeping hook/event payload-mechanics clauses | Drop what an event reports or carries; keep the new capability and how to configure it |
| Creating a changeset for docs sync | Do not create one |
| Committing or pushing directly on `main` | Create `docs/changelog-sync-<version>`, commit there, then open a PR |
| Committing or opening a PR before the user skips review or confirms review is done | Wait at the human review checkpoint |

View file

@ -20,16 +20,12 @@ All other workspace packages are private internal packages, are not published to
- `@moonshot-ai/kaos`
- `@moonshot-ai/kimi-code-oauth`
- `@moonshot-ai/kimi-telemetry`
- `@moonshot-ai/kimi-web`
- `@moonshot-ai/kosong`
- `@moonshot-ai/migration-legacy`
- `@moonshot-ai/protocol`
- `@moonshot-ai/server`
- `@moonshot-ai/server-e2e`
- `@moonshot-ai/vis`
- `@moonshot-ai/vis-server`
- `@moonshot-ai/vis-web`
- `kimi-migration-legacy`
Version impact from internal dependencies must be judged manually. The published artifacts for CLI and SDK bundle internal workspace packages into the artifact itself; runtime `dependencies` of published packages must not include any `@moonshot-ai/*` internal workspace packages.

View file

@ -1,5 +1,5 @@
{
"changelog": ["@changesets/changelog-github", { "repo": "MoonshotAI/kimi-code", "disableThanks": true }],
"changelog": ["@changesets/changelog-github", { "repo": "MoonshotAI/kimi-code" }],
"commit": false,
"fixed": [],
"linked": [],
@ -7,11 +7,10 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"@moonshot-ai/server-e2e",
"@moonshot-ai/vis",
"@moonshot-ai/vis-server",
"@moonshot-ai/vis-web",
"kimi-migration-legacy"
"@moonshot-ai/kimi-inspect"
],
"snapshot": {
"useCalculatedVersion": true,

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Warn in the footer while a typed `/goal` objective exceeds the 4000-character limit, and restore the input instead of losing it when an over-limit objective is rejected. The error message now suggests putting long content in a file and referencing the file path.

View file

@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/agent-core-v2": patch
---
Include the file-reference workaround in the `GOAL_OBJECTIVE_TOO_LONG` error message so clients surface how to submit long objectives.

10
.gitattributes vendored Normal file
View file

@ -0,0 +1,10 @@
# Enforce LF line endings in the working tree on every platform so that
# raw-imported text (e.g. `*.md?raw` templates) is byte-identical on Windows
# and POSIX. Without this, Git for Windows' default `core.autocrlf=true`
# checks text files out as CRLF, which shifts token-count snapshots.
* text=auto eol=lf
# Binary assets — never normalize line endings.
*.gif binary
*.ico binary
*.png binary

View file

@ -86,11 +86,9 @@ jobs:
echo "KIMI_CODE_BUILT_IN_CATALOG_FILE=$CATALOG_FILE" >> "$GITHUB_ENV"
- name: Build Kimi web assets
# The SEA blob step embeds apps/kimi-code/dist-web; build the web app
# and stage its assets before producing the native executable.
run: |
pnpm --filter @moonshot-ai/kimi-web run build
node apps/kimi-code/scripts/copy-web-assets.mjs
# The SEA blob step embeds apps/kimi-code/dist-web. The bundle is
# committed (synced from the code-app repo) — just verify it is in place.
run: node apps/kimi-code/scripts/check-web-assets.mjs
- name: Build native executable (release profile, macOS signed)
if: runner.os == 'macOS' && inputs.sign-macos

View file

@ -30,6 +30,10 @@ jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
@ -42,7 +46,67 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run test
- run: pnpm run test --shard=${{ matrix.shard }}/5
# pi-tui's suite runs on node:test (not vitest), so the root `pnpm run test`
# does not execute it; it needs its own job.
test-pi-tui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @moonshot-ai/pi-tui test
# The VS Code extension suite runs on the default (v2) engine as part of the
# sharded root run above; this job reruns it on the legacy v1 engine, which
# the extension selects through the rollback env var.
test-vscode-legacy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm --filter kimi-code test
env:
KIMI_CODE_LEGACY_FLAG: "1"
test-windows:
runs-on: windows-latest
# Temporarily disabled while Windows tests are being stabilized.
if: false
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
# Windows runners are slower and run the whole suite (including
# in-process e2e tests) under more contention, so the default 5s test
# timeout causes flaky failures. Give it more headroom.
- run: pnpm run test -- --testTimeout=30000
lint:
runs-on: ubuntu-latest
@ -82,8 +146,8 @@ jobs:
echo "Typechecking ${config}"
pnpm dlx --package @typescript/native-preview@beta tsgo -p "${config}" --noEmit
done
- name: Typecheck kimi-web (vue-tsc)
run: pnpm --filter @moonshot-ai/kimi-web run typecheck
- name: Typecheck VS Code extension
run: pnpm --filter kimi-code run typecheck
- name: Typecheck vis-server
run: pnpm --filter @moonshot-ai/vis-server run typecheck
- name: Typecheck vis-web

View file

@ -36,9 +36,6 @@ jobs:
- name: Build package dependencies
run: pnpm run build:packages
- name: Build Kimi web assets
run: pnpm --filter @moonshot-ai/kimi-web run build
- name: Generate Kimi Code built-in catalog
shell: bash
run: |

View file

@ -37,7 +37,7 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for Trusted Publishing
run: npm install -g npm@latest
run: npm install -g npm@11
- name: Install dependencies
run: pnpm install --frozen-lockfile

22
.gitignore vendored
View file

@ -1,12 +1,14 @@
node_modules/
dist/
dist-web/
dist-single/
dist-native/
.tmp-api-extractor/
.contract-types-tmp/
.local/
coverage/
*.tsbuildinfo
.vitest-results/
.vite/
.DS_Store
.playwright-mcp/
.claude
@ -15,6 +17,11 @@ coverage/
plugins/cdn/
.worktrees/
.kimi-code/local.toml
.kimi-sandbox/
.vscode/
!apps/vscode/.vscode/
!apps/vscode/.vscode/*.json
apps/vscode/artifacts/
Dockerfile
docker-compose.yml
@ -23,3 +30,16 @@ docker-compose.yml
docs/superpowers/
reports/
.superpowers/
/plan/
# Agent scratch / throwaway files - do not commit
.tmp/
HANDOVER*.md
HANDOFF*.md
handoff.md
handover.md
*-designs.html
*-design.html
*-mockup.html
*-demo.html
*-demos.html

View file

@ -90,6 +90,30 @@
"eslint/no-console": "off"
}
},
{
// The worker closures: these modules (and everything
// packages/minidb/src/worker/ and
// packages/kap-server/src/search/worker/ pull in) are loaded by a bare
// node:worker_threads Worker under Node's native type stripping with
// `execArgv: ['--experimental-transform-types']`, which requires
// explicit `.ts` import specifiers (the strip loader does not remap
// `.js` -> `.ts`). Keep the exception scoped to exactly those closures.
"files": [
"packages/minidb/src/worker/**/*.ts",
"packages/minidb/src/codec.ts",
"packages/minidb/src/crc32.ts",
"packages/minidb/src/trigram.ts",
"packages/minidb/src/text-postings.ts",
"packages/minidb/src/text-index/tokenize.ts",
"packages/minidb/src/gen-codec.ts",
"packages/kap-server/src/search/worker/**/*.ts",
"packages/kap-server/src/search/indexCore.ts",
"packages/kap-server/src/search/match.ts"
],
"rules": {
"import/extensions": "off"
}
},
{
"files": ["packages/kosong/src/providers/**/*.ts"],
"rules": {
@ -146,10 +170,12 @@
],
"ignorePatterns": [
"dist/",
"dist-web/",
"coverage/",
"node_modules/",
"apps/*/scripts/",
"docs/smoke-archive/",
"packages/pi-tui/",
"*.generated.ts"
]
}

View file

@ -15,16 +15,21 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
## Project Map
- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. When writing or modifying its terminal UI, use the `write-tui` skill (`.agents/skills/write-tui/SKILL.md`).
- `apps/kimi-web`: the browser web UI, a peer to the TUI. Vue 3 + Vite + vue-i18n; talks to the server over REST + WebSocket under `/api/v1`. It must not depend on `@moonshot-ai/agent-core` (wire types are re-implemented locally). See `apps/kimi-web/AGENTS.md`.
- the browser web UI: **its source no longer lives in this repo.** It is developed in the code-app repo (`apps/web`) and shipped as the committed, prebuilt bundle `apps/kimi-code/dist-web` (gitignored, force-added), synced from code-app with `KIMI_CODE_REPO=<this checkout> pnpm run sync:web` — sync and commit the bundle in the same change whenever the web UI should ship differently. `apps/kimi-code/scripts/check-web-assets.mjs` guards packaging against a missing bundle. To hack on the web UI against this repo's server, run `pnpm dev:server` here and point code-app's `pnpm dev:web` at it via `KIMI_SERVER_URL`.
- `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays.
- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities.
- `apps/kimi-inspect`: web inspector for the kap-server `/api/v1/debug` RPC surface — workspace/session browser, per-session transcript chat, per-scope Service panels, and the DI unit inspection view. See `apps/kimi-inspect/AGENTS.md`.
- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, the in-process DI service layer (`src/services/`), and other core capabilities. See `packages/agent-core/AGENTS.md`.
- `packages/agent-core-v2`: the DI × Scope agent engine (the v2 port behind kap-server). Four `LifecycleScope` tiers — `App` / `Workspace` / `Session` / `Agent` (`app/scopes.ts`) — plus the L3 unit layer (`Service`/`Fiber` units, collection contribution points, the Feature seam in `src/features/`); there is no App-level session lifecycle facade — callers compose `ISessionIndex``IWorkspaceLifecycleService.handlerFor` → the handler. See `packages/agent-core-v2/AGENTS.md` and use the `agent-core-dev` skill (`.agents/skills/agent-core-dev/SKILL.md`) when developing here.
- `packages/node-sdk`: the public TypeScript SDK and harness.
- `packages/kosong`: the LLM / provider abstraction layer.
- `packages/kaos`: the execution environment and file/process abstractions.
- `packages/oauth`: Kimi OAuth and managed auth utilities.
- `packages/telemetry`: shared client-side telemetry infrastructure.
- `packages/server`: the Kimi Code server. Hosts `agent-core` sessions and exposes them over REST + WebSocket (`/api/v1`); bootstrapped from `src/start.ts` and consumed by `apps/kimi-code`. See `packages/server/AGENTS.md`.
- `packages/server-e2e`: live e2e tests and scenarios against a running server (`KIMI_SERVER_URL`, default `http://127.0.0.1:58627`). See `packages/server-e2e/AGENTS.md`.
- `packages/transcript`: the isomorphic transcript rendering data layer — L1 agent-granular store, L2 idempotent operations, L3 `off/turn/block/delta` subscription granularity, L4 framework-free view registry, plus turn-cursor pagination. Pure TypeScript (browser-safe, no engine imports); the sole owner of the transcript contract types (`src/contract/`) and the op-batch sequencing contract. See `packages/transcript/AGENTS.md`.
- `packages/kap-server`: the Kimi Code server, backed by `@moonshot-ai/agent-core-v2`; exposes sessions over REST + WebSocket (`/api/v1` + `/api/v1/ws`), plus the `/api/v1/debug/*` reflection RPC surface (`--debug-endpoints`, loopback bind + bearer auth). See `packages/kap-server/AGENTS.md`.
- `packages/klient`: the client SDK — a contract-driven facade over agent-core-v2 (`global.*` / `session(id).*` / `agent(id).*`, zod-validated); transport via subpath entry (`@moonshot-ai/klient/ipc|memory`, both return the same `Klient`); also hosts the e2e suites. See `packages/klient/AGENTS.md`.
- `packages/tree-sitter-bash`: a pure-TypeScript bash parser (no runtime deps, no wasm); `parse(source, { timeoutMs, maxNodes })` runs under a deterministic budget and returns a discriminated `ParseResult` — callers must treat aborted/hasError trees as "cannot analyze" and degrade. Parser only, no safety judgments; see the package README's "Known differences" section.
- `packages/minidb`: the embedded JSON document store (`MiniDb`) behind kap-server's search index — snapshot + WAL persistence with an exclusive write lock, a larger-than-RAM full-text layer, and persistent index generations. See `packages/minidb/AGENTS.md`.
## Environment Requirements
@ -51,19 +56,21 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- NO: `interface Options { user?: User | undefined }`
- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity.
- Except for a package's `index.ts`, other `index.ts` files should prefer `export * from './module';`.
- The `Agent` class in `packages/agent-core/src/agent` must be usable on its own. The constructor must not force the caller to create a `Session` instance, nor require an `agentId` or `session`. It may accept an optional `sessionId` as a request-config hint — for example mapped to the provider's `prompt_cache_key` — but the instance must not hold `sessionId`, and must not depend on the Session lifecycle, metadata, or parent/child relationship logic.
- Do not add too many new test files. Prefer adding tests to the existing test file of the corresponding component or module.
- When a test fails because of a user modification, default to fixing the test first; do not change the implementation to satisfy an old test unless the implementation truly has a bug.
- Do not sacrifice code quality for external compatibility unless the user explicitly asks for it. Breaking changes go through changesets and a `major` bump, gated by the rule below.
## Experimental Features
- Gate a not-yet-public feature behind an experimental flag. Add the flag to the registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`.
- Gate a not-yet-public feature behind an experimental flag. Flags are env-driven and default off: `KIMI_CODE_EXPERIMENTAL_<NAME>` toggles one, `KIMI_CODE_EXPERIMENTAL_FLAG` enables all. Release by flipping the entry's `default` to `true`.
- `packages/agent-core` (v1): add the flag to the central registry at `packages/agent-core/src/flags/registry.ts`, then check it with `flags.enabled('my-feature')`.
- `packages/agent-core-v2` and kap-server modules: there is no central catalog — declare the flag in the owning domain via `registerFlagDefinition` at import time (see `packages/agent-core-v2/docs/flag.md`), then check it with `IFlagService.enabled(id)`. Current search-index-separation flags: `persistence_minidb_readmodel` (session read model, default on) and `search_worker` (global search worker host, default on).
## Where to Update Instructions
- Hard rules that affect almost every task: update the root `AGENTS.md`.
- Rules that only affect a specific directory: update the nearest sub-directory `AGENTS.md`.
- Project-map entries stay at 12 sentences; deep package docs live in the package's own `AGENTS.md`.
- Keep instruction updates focused and supported by code facts.
## Workflow Requirements
@ -75,5 +82,9 @@ This is a TypeScript monorepo built for agent-assisted development. Keep the roo
- When an AI agent opens or updates a PR, fill in `.github/pull_request_template.md` — link the related issue or explain the problem, then describe what changed. Do not leave placeholder text or submit a generic summary of the diff.
- Do not submit vague AI-generated PR text. The human author must understand the change well enough to explain the code, edge cases, and why the approach fits this repository.
- After finishing a task and before submitting a PR, you must run the `gen-changesets` skill (see `.agents/skills/gen-changesets/SKILL.md`) and generate a changeset under `.changeset/` according to its rules.
- When generating a changeset, **never** decide on a `major` bump on your own. When you judge a change to meet the major criteria (breaking changes, incompatible user configuration, renamed or removed commands/arguments, changed behavior semantics, etc.), you must stop and explain it to the user and ask for confirmation. **Only write `major` after the user has explicitly agreed.** Otherwise default to `minor` (and fall back to `patch` if `minor` is unclear). See the "Hard rule: confirm with the user before writing `major`" section in `.agents/skills/gen-changesets/SKILL.md` for details.
- When generating a changeset, **never** decide on a `major` bump on your own — stop, explain, and get explicit user confirmation first; default to `minor`, fall back to `patch`. See `.agents/skills/gen-changesets/SKILL.md`.
- Prefer importing via `import ... from '#/...'`, which serves the same purpose as `import ... from '@/...'`.
- Do not commit throwaway scratch or exploratory files. Never stage:
- Agent working notes or handoff/summary documents (e.g. `HANDOVER-*.md`, `HANDOFF-*.md`, `handoff.md`).
- Throwaway UI/UX prototypes or design mockups (e.g. `*-designs.html`, `*-mockup.html`, `*-demo(s).html`) at the repo root or under a `design/` folder. The only tracked `.html` files should be Vite `index.html` entrypoints.
Before committing or opening a PR, run `git status` and `git diff --staged --stat` and remove anything matching these patterns. Put scratch work under `.tmp/` (gitignored) instead of the repo root or the source tree.

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

231
GOAL.md Normal file
View file

@ -0,0 +1,231 @@
# Goal 功能拆分
本文把 agent-core 中 goal mode 的能力拆成三部分:
1. 核心工作流:没有它就不能运行 goal。
2. 统计 / token 数限制:让 goal 可度量、可限额、可审计。
3. 用户交互相关:让用户可以安全启动、理解、控制和恢复 goal。
## 1. 核心工作流
核心工作流是 goal mode 的运行骨架。它负责创建结构化目标、维护状态机、把普通 turn 串成自治多轮执行,并让模型用机器可读状态结束或停放目标。
### 目标状态
同一个 main agent 同时最多只有一个当前 goal。goal 不是普通聊天文本,而是 runtime 持有的结构化状态,至少包含目标、可选完成标准、当前状态、停止原因和运行统计。
状态分为四类:
- `active`:正在被 goal driver 推进。只有这个状态会自动运行下一轮。
- `paused`暂停但保留目标。通常来自用户暂停、中断、进程恢复后降级、provider 或 runtime 错误。可以恢复。
- `blocked`目标遇到真实阻塞但保留目标。通常来自模型判断需要外部输入、目标无法按当前表述完成、预算达到、prompt hook 阻止。可以恢复。
- `complete`瞬时完成状态。runtime 发出完成事件后立即清除 goal不长期持久化。
没有 `cancelled` 状态。取消就是清除 goal并提醒模型忽略之前关于该目标的 active reminder。
### 创建和替换
创建 goal 时runtime 需要校验目标不能为空、不能过长。已有 active、paused 或 blocked goal 时,默认拒绝创建新 goal防止静默覆盖。只有用户或调用方明确要求替换时才先清除旧 goal再创建新 goal。
新 goal 创建后进入 `active`,写入持久记录,并发出 goal 更新事件。
### 多轮驱动
goal driver 的职责是把一个 active goal 推进成连续的普通 turn
- turn 开始时如果 goal 已经是 `active`,进入 goal driver。
- 普通 turn 中如果模型创建了 goal或把 paused/blocked goal 恢复成 active当前 turn 结束后 goal driver 接管继续执行。
- driver 每次只运行一个普通 turn。
- 每个 turn 结束后读取 goal 状态。
- goal 仍是 `active`runtime 自动追加 continuation prompt 并启动下一轮。
- goal 变成 `paused``blocked` 或被清除时driver 停止。
模型如果不调用状态更新工具,且 goal 仍是 activeruntime 会继续下一轮。模型不能只靠自然语言说“完成了”来结束 goal必须给出结构化状态信号。
### Goal 注入
每个 goal turn 的边界runtime 会把当前 goal 状态注入上下文。注入内容包括:
- 当前正在 goal mode。
- 目标和完成标准是什么。
- 目标文本是用户提供的数据,不能覆盖 system/developer 指令、工具 schema、权限规则或 host 控制。
- 当前状态和进度。
- 模型应该做简短自审,然后推进一个连贯工作切片。
- 简单、已完成、不可能、不安全、矛盾的目标,应在同一轮内直接标记 complete 或 blocked。
- 只有全部要求完成、验证通过、没有下一步有用动作时,才能标记 complete。
- 外部条件或用户输入阻塞时,应标记 blocked。
- 不要只做了计划、总结、第一版或部分结果就标记 complete。
goal 注入只在 turn / continuation 边界做,不在每个 model step 都做,避免上下文重复膨胀,也有利于 prompt cache。
paused 和 blocked goal 的注入更轻:
- paused提醒模型目标存在但当前不应自治推进除非用户明确要求继续。
- blocked提醒模型目标被阻塞且当前不自治推进除非用户要求处理或恢复。
### Continuation prompt
当 goal 仍是 activeruntime 会追加一个系统触发输入,含义相当于“继续朝当前 active goal 工作”。它不只是简单续跑,还要求模型每轮重新判断:
- 是否已经完成。
- 是否遇到真实阻塞。
- 是否应该只推进一个合理切片后继续下一轮。
- 是否应该避免发散或启动无关工作。
- 除非真实阻塞,否则不要向用户要输入。
### 完成、阻塞和暂停
模型通过结构化状态更新控制 goal 生命周期:
- `complete`目标已满足runtime 发出完成事件并清除 goal。
- `blocked`遇到真实阻塞runtime 保留 goal 并停止自治推进。
- `paused`:暂时放下 goalruntime 保留 goal 并停止自治推进。
- `active`:恢复 paused 或 blocked goal。
状态更新工具的输入应保持窄,只表达机器状态。完成总结或阻塞原因由模型随后给用户说明。
当模型标记 complete 后runtime 应再给模型一次收尾机会,生成简短最终回复,说明 goal 已完成、主要做了什么、跑了什么验证。
当模型标记 blocked 后runtime 应再给模型一次收尾机会,说明具体阻塞、需要什么输入或变化才能继续。
如果当前 turn 已经没有 step 预算,不应为了收尾总结强行再跑一步,避免把“没法写总结”变成 turn 失败。
### 错误停车
goal mode 把技术运行失败视为可恢复停车:
- 用户中断当前 turngoal 变 paused。
- provider rate limitgoal 变 paused。
- provider 连接错误、认证错误、API 错误goal 变 paused。
- 模型配置错误goal 变 paused。
- runtime 异常goal 变 paused。
- provider safety filtergoal 变 paused。
业务、规则或外部条件阻塞则变 blocked
- prompt hook 阻止目标。
- 模型判断无法继续。
- 预算达到。
- 需要用户或外部系统提供新条件。
### 持久化和恢复
goal 的创建、更新、完成、阻塞、清除应写入可恢复记录。session 恢复时runtime 用记录重建 goal。
恢复时如果发现 goal 原来是 active不应自动继续跑而是降级为 paused。因为旧进程中的 active turn 不可能还活着,自动继续会造成重启后偷偷消耗资源。
paused 和 blocked 原样保留。complete 理论上不长期存在,因为完成后会清除。
fork session 时不继承源 session 的 goal并提醒模型不要继续源 session 的旧目标。
## 2. 统计 / token 数限制
这一部分让 goal 可度量、可限额、可审计。没有它goal 仍然可以运行,但不可控。
### 运行统计
goal 统计包括:
- continuation turn 数。
- token 数。
- active wall-clock 时间。
统计只在 goal 是 `active` 时增长。paused 和 blocked 期间不继续计数。
turn 统计在每个 goal turn 准备运行时增加,因此模型在某一轮里标记 complete 时,这一轮也计入最终统计。
token 统计在 model step 结束后累计。没有 active goal 时,不记入 goal。token 统计应以静默更新为主,不应每一步都刷 UI。
时间统计只计算 active pursuit 时间。进入 active 时开启计时区间,离开 active 时折算进累计时间pause/resume 会形成新的 active 区间。
### 预算
goal 预算包括:
- turn budget。
- token budget。
- wall-clock budget。
默认没有预算。只有用户明确给出硬限制时才设置,例如“最多 20 轮”“不超过 500k token”“30 分钟内”。模糊表达如“尽快”“别花太久”不能设置预算,模型也不能自行发明预算。
时间预算需要合理范围。过短或过长应拒绝。turn 和 token 预算应规范化为正整数。
### 预算硬停
预算检查应发生在 goal turn 开始前和结束后。token budget 还应在 model step 后触发停止,避免超额后继续下一步。
一旦达到预算runtime 应直接把 goal 标记为 blocked原因是配置预算已达到。这个 blocked 仍可恢复,但如果预算不变,恢复后可能立刻再次 blocked。
### 预算引导和最终统计
当预算未接近时,模型提示应鼓励稳定推进。当任一预算达到 75% 以上时,提示应转为收敛,避免启动新的可选工作。
complete 和 blocked 的最终回复提示应包含 worked turns、elapsed time、tokens used 等统计信息。UI 事件也应带当前 snapshot 和变化类型。
telemetry 可以记录 goal 创建、预算设置、continuation、状态变化、清除等事件但不应包含目标文本、停止原因等敏感内容。
## 3. 用户交互相关
这一部分让用户可以安全启动、理解、控制和恢复 goal。没有它runtime 仍可能运行,但交互体验和安全边界不足。
### 生命周期控制
用户可以直接控制 goal
- 创建。
- 查看。
- 暂停。
- 恢复。
- 取消。
这些操作可以不经过模型 turn。pause 把 active goal 变 pausedresume 把 paused 或 blocked goal 变 activecancel 直接清除当前 goal。
resume 会清除旧停止原因表示开始新的尝试。paused/blocked goal 不会因为用户发普通消息就自动继续。
### 模型发起 goal 的确认
模型可以代表用户创建 goal但只有在用户明确要求启动 goal、自治工作或宿主 goal-intake 提示要求时才应该这样做。普通请求不能被模型擅自升级成 goal。
模型发起 CreateGoal 时,非 auto 权限模式下应触发用户确认。确认菜单允许用户选择本次 goal 的运行权限模式。用户拒绝则 goal 不创建。
`GetGoal``SetGoalBudget``UpdateGoal` 只改 goal runtime 状态,默认可以更容易批准。真正写文件、跑 shell、访问敏感路径等仍走普通权限系统。
### 暂停、阻塞和取消后的提示
paused goal 的上下文提示应说明目标存在但当前不应继续做,除非用户明确要求继续。
blocked goal 的上下文提示应说明目标被阻塞且当前不自治推进,可以在用户要求时帮助解阻,否则正常处理当前请求。
cancel 后应追加提醒,让模型忽略旧 goal 的 active reminder避免旧上下文诱导模型继续已经取消的目标。
### 完成和阻塞的用户回复
complete 后goal 被清除,模型应给用户一条简短完成总结,说明完成了什么、做了什么验证。
blocked 后goal 保留,模型应给用户一条简短阻塞说明,说明具体阻塞和继续所需输入、权限、外部条件或变更。
### Tool 暴露和隔离
goal 工具只给 main agent。subagent 不应直接创建、恢复、结束主 goal。
没有 goal 时,模型不应看到 `UpdateGoal``SetGoalBudget`。有 goal 时才暴露这些控制工具。
goal ID 不应暴露给模型,因为它只是 runtime/UI 内部标识,没有用户语义。
### 辅助写 goal
`write-goal` 类能力用于帮助用户把粗糙意图整理成适合 goal mode 的完成契约。好的 goal 应明确:
- end state什么条件必须变成真。
- proof用什么可观察证据证明完成。
- boundaries工作范围和禁止触碰的内容。
- loop如何迭代推进。
- stop rule什么情况下停止并报告而不是强行继续。
预算是 opt-in不应默认加入也不应把 turn cap 写进目标文本。
### UI 和会话语义
goal 创建、暂停、恢复、阻塞、完成、清除都应发出 goal updated 事件。lifecycle 变化和 completion 变化应区分。completion 是一次终局事件,然后 snapshot 变 null。blocked/paused 保留 snapshotUI 可以继续展示可恢复 goal。
session 恢复时active goal 会变 paused避免重启后自动继续。fork session 时不继承 goal并提醒模型不要继续源 session 的目标。

View file

@ -19,12 +19,6 @@ Install with the official script. No Node.js required.
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
```
- **Homebrew (macOS/Linux)**:
```sh
brew install kimi-code
```
- **Windows (PowerShell)**:
```powershell

View file

@ -22,12 +22,6 @@ Kimi Code CLI 是一个运行在终端里的 AI 编程 agent可以帮你读
curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash
```
- **HomebrewmacOS / Linux**
```sh
brew install kimi-code
```
- **WindowsPowerShell**
```powershell

View file

@ -6,3 +6,6 @@ agents/
# next to it keeps `#/generated/vis-web-asset` type-resolvable on a fresh
# clone (before any build has produced the `.ts`).
src/generated/vis-web-asset.ts
# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
/native/

View file

@ -65,6 +65,7 @@ The theme apply/switch mechanics live in the `write-tui` skill. The following ru
## General Coding Requirements
- The startup path before the workspace trust gate (`KimiTUI.start()` -> `maybeRunWorkspaceTrustPrompt()`) must not spawn child processes by bare command name — on Windows, cmd.exe / CreateProcess resolve them from the current directory first, so a binary planted in an untrusted workspace would run before the user confirms trust. When an external command is unavoidable, resolve it with `resolveCommandPath` from `src/utils/process/resolve-command.ts`, which returns an absolute PATH hit and refuses matches inside the cwd.
- For optional object properties, pass `undefined` directly — do not use conditional spread.
- Optional object properties do not need to additionally allow `undefined` in the type.
- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity.

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more