Commit graph

210 commits

Author SHA1 Message Date
7Sageer
26dd6ce6ae
fix(telemetry): deduplicate session_started and model switch events (#3499)
* fix(telemetry): deduplicate session_started and model switch events

* fix(telemetry): keep engine session_started for direct v2 SDK clients and avoid model_switch race

* fix(telemetry): preserve model_switch when activation rebinds the same alias

* fix(telemetry): route TUI reload through harness

* test: update /reload message-flow test for harness reload route
2026-09-03 16:49:27 +08:00
7Sageer
b6f78f9fc8
Revert "fix(agent-core-v2): assemble flag-gated features after config ready (…" (#3486)
This reverts commit eb4cd4b38e.
2026-09-02 21:43:09 +08:00
tpoisonooo
8057d30afb
fix(tower): support config.toml configuration (#3461)
* fix(agent-core-v2): rework tower mode enter failures and config-flag assembly

- IAgentTowerService.enter() returns a typed TowerEnterResult: the 4
  former silent failure points now carry their reason (not-main-agent /
  experiment-off / feature-not-assembled / owned-by-live-session with
  the owner session id + title); kap-server and node-sdk map each reason
  to a distinct SESSION_TOWER_MODE_INVALID message via
  towerEnterFailureMessage() instead of one misleading catch-all.
- Fix config-sourced [experimental] tower flag never assembling the
  tower feature: ConfigService seeds its state synchronously at
  construction with a best-effort readFileSync of the config document it
  owns, so the flag is already visible when TowerFeature's constructor
  runs during App-scope creation. No event machinery: a runtime config
  flip updates flag reads but does not re-run feature constructors — a
  restart is required, pinned by test. docs/flag.md + docs/features.md
  document the contract.
- Consolidated changeset for @moonshot-ai/kimi-code.

* fix(tower): release handler when teardown

---------

Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
2026-09-02 21:16:14 +08:00
7Sageer
eb4cd4b38e
fix(agent-core-v2): assemble flag-gated features after config ready (#3471)
FeatureAssemblyService ran during the eager bootstrap batch, while
ConfigService was still loading config.toml asynchronously, so
flags.enabled() always read false for config-sourced flags and gated
features (e.g. tower via [experimental] tower = true) never assembled.

registerFeature now accepts a { flag } option; gated registrations are
assembled in a deferred pass once IConfigService.ready resolves, and
IFeatureAssemblyService.ready exposes that pass. WorkspaceInstanceManager
.materialize also awaits config.ready so session creation cannot race
the deferred assembly.
2026-09-02 21:09:35 +08:00
7Sageer
9f866cb7f4
refactor(agent-core-v2): rebuild telemetry context as a scope-bound layered registry (#3430)
* refactor(agent-core-v2): rebuild telemetry context as a scope-bound layered registry

* fix(agent-core-v2): prefer ambient model in telemetry envelope and repair print-runner stub

* fix(agent-core-v2): delete only the fragment registered by this telemetry binding

* fix(agent-core-v2): bind telemetry context writes to the binding's own fragment

* fix(agent-core-v2): keep stale telemetry emissions on the binding-owned fragment

* refactor(agent-core-v2): mirror turn-request trace_id into ambient telemetry context

* refactor(agent-core-v2): telemetry key-resolution cleanups

Drop the dead defineAgentTelemetryEvent branch in declaredKeysFor: agent_id is already copied unconditionally as identity plumbing before the declared-key loop, and the loop itself skips agent_id, so the branch had no runtime effect. Name the wire session id property key (WIRE_SESSION_ID_PROPERTY) so the session_id-to-sessionId mapping is explicit.

* refactor(agent-core-v2): flow ambient telemetry context to every event unconditionally

Remove the registry-key filtering and the FILTERED_CONTEXT_KEYS hard
filter from composeTelemetryProperties: the merged ambient now reaches
every event's properties, with session_id still remapped to the
camelCase sessionId key and explicit defined values winning on
collision. Explicit undefined values no longer clobber ambient fields.

With filtering gone, correctness for time-varying fields rests on
lifecycle discipline, so the loop now clears the ambient trace_id when
a step starts (mirroring the activeRequestTrace reset) instead of
relying on an explicit undefined payload value to mask the stale
mirror.

* refactor(agent-core-v2): move thinking_effort into the turn telemetry context

* refactor(agent-core-v2): replace the telemetry fragment registry with parent references
2026-09-02 20:50:23 +08:00
github-actions[bot]
0d45dddc57
ci: release packages (#3456)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-02 17:18:07 +08:00
Grapedge
411572e166
feat(vscode): engine-backed @ file suggestions (#3453) 2026-09-02 14:16:20 +08:00
Haozhe
ae7a6dc6fb
refactor(acp): remove the legacy acp-adapter package (#3434) 2026-09-01 23:59:20 +08:00
Haozhe
ceb51535ef
feat(cli): add the kimi session list subcommand (#3425)
* feat(cli): add the kimi session list subcommand

- add the session command group with list: cwd scope by default, --all/--cwd/--archived/--limit/--json
- pass includeArchived through the SDK listSessions options (v2 klient query, v1 includeArchive mapping)

* docs: condense the session list changeset to one sentence

* fix(cli): sanitize session list rows and tidy option types

- strip control characters and line breaks from user-controlled row fields
- declare cwd/limit as plain optional properties without undefined unions
2026-09-01 21:41:52 +08:00
Haozhe
9c37feb473
perf(agent-core-v2): fork sessions without materializing the target (#3421)
* fix(agent-core-v2): persist plan.revision as agent-relative key

- replace the session-scoped `path` field in durable plan.revision records
  with an agent-relative `key`, resolved to a display path at read and
  projection time against the current agent/session scope
- migrate legacy `path` records during wire restore and rewrite the journal
  once migration succeeds, emitting low-cardinality
  `wire_plan_revision_migrated` telemetry
- tolerate journal truncation during the migration rebuild and keep the
  legacy `path` display in cold transcript folds for unmigrated records
- keep external transcript marker `path` and meta `reviewPath` projections
  unchanged

* perf(agent-core-v2): fork sessions without materializing the target

- copy session files byte-for-byte (wire.jsonl included, parallel file
  writes) and append the forked marker per agent instead of parsing and
  rewriting every wire journal
- write the forked session's state.json, agent registry, and session
  index entry directly without creating a live session or agent scopes;
  fork returns SessionMeta and SessionForkedEvent drops the scope handle
- keep turnIndex truncation on the per-record path
- kap-server fork/createChild routes, the klient facade and wire
  contract, and the node-sdk forkSession resume the forked session
  through the normal resume path when a live handle is needed
- stop healing truncated source wires during fork; the forked session
  repairs its own copy lazily on resume
- add the `kimi fork [--cwd] [-y]` CLI command
2026-09-01 19:46:35 +08:00
tpoisonooo
c3bf6f9d2d
feat(agent-core-v2): rework tower mode entry and surface worker deaths (#3399)
Tower mode is now entered only manually — /tower on, or /tower
<base-branch> to also pin the local branch missions merge back into. The
agent can no longer enter it on its own: TowerInit refuses while the mode
is off and points at the slash command.

/tower <base-branch> is deterministic end to end: a missing base branch is
created from the current checkout (uncommitted changes committed onto it
as a labeled WIP snapshot), the checkout switches to it, and the tower
workspace is initialized immediately instead of relying on the model to
apply the base. With an existing workspace the tower rebases onto the
requested base when no missions are open and refuses naming the blocking
missions otherwise. The base rides the tower_mode.enter event into the new
tower.base state key and TowerInit falls back to it.

Tower agent deaths (failed/timed_out/killed/lost) are recorded in the
tower protocol: TowerStatus marks dead roster entries and warns about
missions whose owner died, with a resume hint. The tower console prompt
now requires a per-worker deliverables summary before TowerTeardown.

The session profile REST surface accepts tower_base so web clients can
turn tower mode on with /tower <base-branch> instead of sending the
argument as a prompt.

Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
2026-09-01 18:03:13 +08:00
7Sageer
379241e008
feat(agent-core-v2): report enabled experimental flags in session_started telemetry (#3419)
* feat(agent-core-v2): report enabled experimental flags in session_started telemetry

Add a sorted, comma-separated experimental_flags property to the v2
session_started event so feature-gated metrics gain a session-granular
exposure denominator, and carry the post-apply enabled set on the TUI
experimental_features_apply event to bound mid-session flag flips.

* feat(node-sdk): carry experimental flags on the harness session_started row

The v2 in-process path emits session_started twice (engine-side from
SessionLifecycleService, harness-side from KimiHarness), and only the
engine row carried experimental_flags, so consumers could double-count
or miss the flag dimension. Merge a per-emission dynamic getter into the
harness row, wired to the in-process engine's flag service, so both
rows report the same enabled flag set.

* fix(node-sdk): keep experimental_flags canonical on session_started

A caller-supplied experimental_flags in per-call sessionStartedProperties
could overwrite the engine-owned dynamic value, breaking row-to-row flag
consistency. Merge the dynamic properties after the session-scoped ones
so engine-owned keys always win.

* fix(agent-core-v2): count tower as exposure only once assembled

The tower flag reacts live to mid-process flips, but TowerFeature's
tools and profiles are assembled at App scope construction, so a session
started after an in-process flip cannot actually use tower. Filter it
out of experimental_flags on both session_started producers unless
isTowerFeatureAssembled says the feature is assembled in this process,
keeping the exposure denominator honest.

* test: recalibrate fixtures for the added constructor param and drop timing-sensitive tower assertion

The WorkspaceInstanceManager test builds the service with positional
args; the new flags param shifted unitHostFactory one slot. The tower
inclusion assertion in the SDK integration test depended on feature
assembly winning the race against async config load, which slow CI
runners lose; tower assembled-state coverage stays with the tower
service tests.

* refactor(agent-core-v2): move flag exposure gating into the flag definition

Telemetry consumers hardcoded tower's assemble-at-startup semantics to
keep it out of experimental_flags until assembled. Push the gate into
FlagDefinitionInput as an optional isExposed predicate owned by the
flag's own domain, expose it as IFlagService.exposedIds(), and let both
session_started producers call it — no feature-specific knowledge leaks
into the SDK or the session lifecycle.
2026-09-01 17:14:35 +08:00
Haozhe
15bf58555b
refactor(agent-core-v2): replace agent runtime with agent-scoped DI services (#3402)
* refactor(agent-core-v2): migrate todo domain to agent-scoped DI service

* refactor(agent-core-v2): migrate skill domain to agent-scoped DI service

* refactor(agent-core-v2): migrate reminder domain to agent-scoped DI service

* refactor(agent-core-v2): migrate interaction domain to agent-scoped DI service

* refactor(agent-core-v2): migrate cron domain to agent-scoped DI service

* refactor(agent-core-v2): migrate dateChange domain to agent-scoped DI service

* refactor(agent-core-v2): migrate goal domain to agent-scoped DI service

* refactor(agent-core-v2): remove agent runtime infrastructure

* docs(agent-core-v2): rewrite agent domain standard as agent-scoped DI service

* fix(agent-core-v2): restore actor services on late feature re-provide and order agent-created events after metadata registration
2026-08-31 22:53:29 +08:00
Haozhe
4b9888b73d
feat(agent-core-v2): require approval for dangerous bash commands in all permission modes (#3290)
* feat(agent-core-v2): require approval for dangerous bash commands in all permission modes

* feat(agent-core-v2): deny dangerous commands in auto mode, unwrap command launchers, and skip the guard for non-interactive hosts

* feat(agent-core-v2): support disabling the dangerous-command guard via config

* test(node-sdk): project v2's env-materialized empty permission section in config parity
2026-08-31 20:24:53 +08:00
7Sageer
971a8b24c1
feat(secondary-model): graduate the subagent model pool out of experimental (#3334)
* feat(secondary-model): enable the subagent model pool by default

* feat(secondary-model): graduate the subagent model pool out of experimental

* fix(secondary-model): honor the v2 default_model key on the legacy engine

* fix(secondary-model): enforce forced subagent pools on the legacy engine

* fix(tower): keep reviewers on primary model

* fix(secondary-model): live-apply picker saves and replace the legacy model key

* fix(secondary-model): accept default_model in the live-apply setter and clear on removal

* fix(secondary-model): revert the legacy v1 engine, keep the v2 pool opt-out

* feat(secondary-model): graduate pool with model source telemetry

* fix(secondary-model): keep legacy engine opt-in

* test(sdk): pin secondary-model engine divergence

* fix(tower): honor forced model for reviewers
2026-08-31 15:33:26 +08:00
liruifengv
2bf7ed22d7
feat(auth): split model readiness from sign-in state in /api/v1/auth (#3293)
* feat(auth): split model readiness from sign-in state in /api/v1/auth

GET /api/v1/auth now reports models_ready (the default model resolves
against the configured catalog, providerless and env-injected models
included) instead of the compound ready flag, and no longer carries
default_model — config values are served by /config alone. The v1
summary schema follows.

OAuth managed-model refreshes now heal a lost default model: the
refresh snapshot includes defaultModel, so an unchanged catalog with
a missing default still lands the write-back branch and re-selects
one. The refresh also rebases onto a fresh config read after the
remote fetch, so a model or thinking change made during the fetch is
no longer overwritten. The shared discovery refresh path (scheduler,
POST /providers/{id}:refresh) heals the default the same way.

Config changes are now published to WS clients on every write path:
a debounced+trailing publisher bridges IConfigService section changes
to ConfigChanged with camelCase changedFields and a full config
projection, and the broadcaster forwards event.config.changed and
event.model_catalog.changed (both previously published but never
delivered). All three event types are registered in the event unions,
so session_event parsing and AsyncAPI describe them.

BREAKING CHANGE: GET /api/v1/auth drops the ready and default_model
fields in favor of models_ready; event.config.changed's changedFields
is now camelCase domain names instead of the raw snake_case request
keys (v1 summary schema follows).

* fix(kap-server): expose the session model in session list projections

GET /api/v1/sessions hardcoded agent_config.model to '' and the v2
projection had no model field at all, so clients could only learn a
session's model via the post-select /status read — which races the WS
replay and often never lands. SessionFacts now carries the live
session's model (same source as the snapshot route), toWireSession
emits it, and the v2 activity domain gains a nullable model field.

* fix(kap-server): gate prompt submission on the effective session model

The submit gate called ensureReady() with no override, so it only ever
validated config.default_model: a session with a bound model (or a
prompt carrying one) was rejected with 40113 whenever default_model was
missing or dangling. Pass the effective model (request model, then the
agent profile's bound model, falling back to default_model inside
ensureReady) on both the prompt submit and btw routes.

* fix(agent-core-v2): honor defaultProvider in model readiness resolution

resolveModelForReady stopped at the flat baseUrl fallback, so a model
that omits provider/providerId and relies on the configured
defaultProvider resolved at runtime (ModelCatalog.resolveProviderContext
falls back to it) while /api/v1/auth reported models_ready:false and the
send gate rejected the prompt. Mirror the runtime order (providerId ->
provider -> defaultProvider -> flat baseUrl) and pass the configured
default provider from both readiness callers.

* fix(kap-server): redact inline model credentials from config responses

toConfigResponse only redacted the providers section, so a model's
inline apiKey/oauth rode GET /config verbatim and, via the new
event.config.changed publisher, every WS connection plus the persistent
event journal. Project the models section the same way: strip
credential fields and report has_api_key.

* fix(agent-core-v2): honor defaultProvider in ensureReady credential checks

The readiness phase learned the defaultProvider fallback, but the
credential phase right after still derived the provider only from the
model's explicit fields: a model omitting provider/providerId passed
readiness yet missed the default provider's apiKey/OAuth material and
prompts failed with auth.token_missing. Mirror the same provider chain
(providerId -> provider -> defaultProvider) when resolving credentials.

* fix(kap-server): validate the model a profile bind will select at the prompt gate

The gate validated the session's current model even for a prompt that
switches profile without a model — but bind falls back to defaultModel
in that case, so a stale session model drew a misleading 40113 before
bind could run. Gate on bind's selection order instead: the request's
explicit model, then the default on a profile switch, then the session's
bound model.

* fix(kap-server): redact inline service credentials from config responses

The earlier redaction covered providers and models, but toConfigResponse
still passed the services section through verbatim: inline or
env-injected apiKey, oauth references, and credential-bearing
customHeaders rode GET /config and, via the event.config.changed
publisher, every WS connection plus the persistent event journal.
Project services the same way: strip apiKey/oauth into has_api_key and
report only the header names as custom_header_keys (the MCP
envKeys/headerKeys convention).

* fix(kap-server): keep unlisted config domains through event validation

The config.changed broadcaster returned the zod-parsed config, which
strips domains absent from configResponseSchema (mcp, identity,
model_catalog, image, tools, token_counting): changedFields named them
while the advertised full snapshot no longer matched GET /api/v1/config.
Make the response projection passthrough (defineRoute validates only
requests, so REST responses are unaffected).

* fix(agent-core-v2): use the exact configured key for model readiness lookups

resolveModelForReady trimmed the model id before the models-table lookup
while ModelCatalog and ensureReady use the configured string as an exact
record key: a whitespace-padded default_model was reported ready and then
crashed the submit gate with an internal error instead of 40113, and a
legitimate key containing spaces was reported dangling. Trim only rejects
blank values now; the lookup always uses the raw key.

* fix(protocol): keep unlisted config domains in the shared event projection

The shared configResponseSchema stripped domains it does not enumerate
(mcp, identity, model_catalog, image, tools, token_counting, subagent,
secondary_model), so event.config.changed parsed through agentEventSchema
named them in changedFields while omitting their values. Make the shared
projection passthrough like the kap-server-local one.

* fix(agent-core-v2): use the exact default_provider key in readiness checks

The defaultProvider fallback trimmed the configured value before the
providers-table lookup while ProviderService and ModelCatalog use the
configured string verbatim: a whitespace-padded default_provider could
build successfully yet report not-ready (40113), or report ready for a
provider runtime resolution cannot find. Trim only rejects blank values;
the lookup uses the raw key.

* chore: sync web dist from code-app

Rebuild the bundled web UI against this branch's /auth contract (models_ready, no ready/default_model): the previous bundle still read the old fields and stayed in the not-ready flow against this server.

code-app: 000d2594ff3e95b553be326126bab3f939b62944

* Revert "chore: sync web dist from code-app"

This reverts commit 9400a24a03863b3e8b780dda251540f824f02f3a.

* fix(oauth): rebase the default selection after the refresh fetch

A provider refresh snapshots the config before the remote catalog fetch;
when the user selects a default model while the fetch is in flight, the
stale snapshot's empty default made an otherwise unchanged catalog enter
the write path and the self-heal persisted the generated default over the
user's newer selection. Each branch now re-reads and rebases the
default/thinking selection after its fetch, before cloning, comparing,
or writing.

* style(kap-server): pass optional custom_header_keys without conditional spread
2026-08-28 15:53:56 +08:00
github-actions[bot]
52e8d19dbd
ci: release packages (#3140)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 19:33:51 +08:00
7Sageer
bd5e32f683
fix(secondary-model): stop rewriting the section when providers refresh or are removed (#3284)
* fix(secondary-model): stop rewriting the section when providers refresh or are removed

Provider refresh, provider deletion/rename, catalog/registry import,
OAuth logout, and SDK removeProvider used to cascade into the user's
[secondary_model] block: pool entries were silently pruned, and the
whole section was deleted when its effective default dangled. The
cascade ran from a cache-refresh path (including an unattended 6h
scheduler), so upstream model-list changes could irreversibly destroy
hand-written configuration without any notice.

Config is user intent; the catalog is an availability snapshot. Stop
rewriting the section on every provider/models writer. An entry whose
model no longer resolves fails pool validation on the next session
create with a message naming the offending alias, which is the same
fail-fast contract hand-written typos already had.

* chore(sdk): add changeset for the removed secondary-model cascade export

* Delete .changeset/sdk-remove-secondary-model-cascade.md

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

* Delete .changeset/secondary-model-no-silent-rewrite.md

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

---------

Signed-off-by: 7Sageer <sag77r@hotmail.com>
2026-08-27 16:50:42 +08:00
liruifengv
f143130c07
fix(transcript): carry the orchestrator's prompt on subagent turns (#3289)
* fix(transcript): carry the orchestrator's prompt on subagent turns

* chore: add changeset for subagent turn prompts
2026-08-27 16:03:06 +08:00
liruifengv
692bb0a409
feat(kap-server): add task detach action to move foreground tasks to background (#3273)
* feat(kap-server): add task detach action to move foreground tasks to background

* test(kimi-code-sdk): normalize v2-only parentToolCallId in task parity projections

* fix(agent-core-v2): distinguish user-initiated detach in tool result text

* feat(agent-core-v2): mention user-initiated backgrounding in the bash tool description

* feat(agent-core-v2): use a client-agnostic background-task panel hint in the bash tool description

* ci: retrigger checks

* feat(agent-core-v2): client-agnostic human_shell_hint and a detached_by_user marker in tool results

* fix(protocol,docs): declare parent_tool_call_id in the shared task schema and document the detach action

* style: remove added comments
2026-08-27 15:38:21 +08:00
LCZcn96
75550c5686
fix(vscode): restore live context usage on v2 (#3098)
* fix(vscode): restore live context usage on v2

* chore: point changeset at vscode extension and node-sdk packages
2026-08-26 19:57:50 +08:00
Haozhe
f0a609487f
feat(kimi-code): add remote control web tunnel (#3034)
* feat(kimi-code): add remote control web tunnel

Add CLI and TUI entry points for exposing the local web UI remotely.
Bridge HTTP and WebSocket traffic with local authentication and reconnect handling.

* fix(kimi-code): prevent remote control websocket crash

* fix(kimi-code): align websocket dependency versions

* fix(kimi-code): harden remote control connection setup

Reconnect when management closes during the HTTP tunnel handshake.
Reject non-loopback Remote Control binds whose CSP blocks path bootstrap.

* fix(kimi-code): fix remote control rewriting, caching, and WS frame loss

* feat(kimi-code): add remote control QR output

* build: update pnpm dependencies hash

* refactor(kimi-code): remove the --allow-remote-terminals flag

* feat(kimi-code): add remote control lock, rc command, and QR fixes

* fix(kap-server): broadcast user prompts to all session clients on submit

- agent-core-v2: emit prompt.submitted (status running|queued) at enqueue and prompt.started when the turn launches
- kap-server: project prompt.submitted/prompt.started into transcript prompt entities and the live transcript REST response
- update flake.nix pnpmDeps hash for the PR lockfile

* fix(node-sdk): drop v2-only prompt.started from SDK event stream

- event-mapper: add prompt.started to the dropped v2-only prompt lifecycle types (parity with submitted/completed/aborted/steered)
- cli test: assert only visible sub-commands and stub the experimental flag env for determinism

* ci(pkg-pr-new): post custom install comment for npm 12 compatibility

* feat(kimi-code): render remote control QR as inline image on capable terminals

* feat(kimi-code): improve remote control terminal output

- add onboarding, security, device management, and help guidance
- show compact clickable links and QR image fallback details
- report relay and remote device connection lifecycle

* test(agent-core-v2): update tool event snapshot

* revert(ci): keep preview workflow unchanged in rc pr

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-08-25 20:22:06 +08:00
Haozhe
41f1eaed9a
refactor(agent-core-v2): merge contextInjector and systemReminder into reminder agent runtime domain (#3223) 2026-08-25 10:49:45 +08:00
Haozhe
f4dc9ba1ae
refactor(agent-core-v2): migrate skill domain to agent runtime (#3202)
- define the AgentSkill contract and SkillRuntime (activate /
  promptWithSkills / recordModelToolActivation) with a lazy, non-durable
  provider; recordActivation dispatches SkillActivated directly and the
  skillKey / SkillActivate relay is gone
- allow agent runtime descriptors to omit logic (durable definitions
  still require it), making skill the first pure-API domain
- featurize the skill domain: app/skillCatalog,
  workspace/workspaceSkillCatalog, session/sessionSkillCatalog,
  agent/skill, and agent/tools/skill move under src/features/skill;
  the static Skill tool registration and the AgentSkillService are gone
- resolve the runtime at the edge: kap-server routes and node-sdk
  activateSkill go through IAgentLifecycleService.resolve, and the
  klient dispatcher keeps the agentSkillService wire name over a
  per-agent view
2026-08-24 18:53:04 +08:00
qer
2f12469301
fix(transcript): fold mid-turn task notifications into the current turn on cold rebuild (#3102)
* fix(transcript): fold mid-turn task notifications into the current turn on cold rebuild

* chore: changeset for the notification fold fix

* fix(transcript): key the notification fold on persisted task-turn boundaries, not the previous message role

* fix(transcript): collect background_task turn origins too, and fall back when the wire has no turn.started records

* fix(transcript): fold consecutive task notifications into the same turn

* fix(transcript): normalize folded notification text to the live title/body form

* fix(transcript): stop folded notification text before child blocks, preserve legacy background_task turns absent from the boundary set

* fix(transcript): truncate folded notification text at the first child-block tag, not just the tag lines

* fix(transcript): drop folded notifications without a persisted step, truncate only at output blocks

* fix(transcript): attach mid-turn task notifications to the following step, cold and live

* fix(transcript): keep other tasks' notifications in task-origin turns

* fix(transcript): derive task-turn boundaries from durable turn.prompt records

* feat(transcript): carry subagent model and thinking effort on task entities

* fix(transcript): mirror turn liveness into meta.activity, live and cold

* fix(transcript): populate the prompts entity from prompt.accepted/queued engine events

* fix(transcript): reconcile liveness and the prompt queue at backfill from the live loop state

* fix(transcript): include the prompts entity in the REST transcript response

* fix(agent-core-v2): publish prompt.accepted on the event bus

* test(transcript): add the contract-level e2e covering every entity the client renders from

* fix(transcript): guard the prompt backfill against missing services; align stream expectations with prompt.accepted on the bus

* test(agent-core-v2): re-record event stream snapshots with prompt.accepted published

* fix(transcript): settle the spawned agent row when its lifecycle redirects to the task row

* test(transcript): move the contract e2e timeout to the describe arg (jest lint)

* fix(transcript): declare task model fields in the wire schema, carry prompt content on accepted, normalize queued content

- transcriptTaskSchema declares model/thinkingEffort: Zod strips
  undeclared keys, so schema-driven REST/WS consumers lost both fields
  the projector now populates.
- PromptAccepted carries the admitted content (it is the only event a
  first-turn prompt ever emits, and the bare id left the prompts entity
  permanently partial) — projected with userMessageId and the public
  content shape via projectPromptContentParts, same as queued and the
  live backfill.
- Regenerate the wire manifest and re-record the affected event-stream
  snapshots.

* fix(transcript): preserve task model fields across termination and accepted prompt details across queueing

- onTaskLifecycle carried resultSummary/usage/error/stateReason but
  dropped model/thinkingEffort: a completed detached-Agent row lost the
  metadata spawned set while running.
- prompt.queued rebuilt the entity from scratch, discarding the
  userMessageId and createdAt that prompt.accepted had just stamped —
  build the queued update from prev.

* fix(transcript): keep prompt.accepted out of the public v1 event stream

The observable marker made the broadcaster forward every accepted
prompt to v1 WS clients, but events-zod has no accepted variant (v1
surfaces submission through the service-synthesized prompt.submitted)
and the SDK mapper didn't drop it — schema-driven clients could reject
the frame. Drop it at the WS edge and in the SDK's dropped set; the
transcript projection keeps consuming it internally.

* fix(transcript): derive cold task-turn boundaries through undo anchor replays

* fix(transcript): flush trailing folded notifications into the open turn

* fix(transcript): drop trailing buffered notifications to match the live projector

* chore: split transcript changesets per logical change

* fix(transcript): parse only the generated Title/Severity header lines in folded notifications
2026-08-24 14:33:36 +08:00
Haozhe
9a715820c4
refactor(agent-core-v2): featurize todo/cron/interaction and migrate goal to agent runtime (#3184)
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 / test-vscode-legacy (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): featurize todo/cron/interaction and migrate goal to agent runtime

* refactor(agent-core-v2): move cron scheduling helpers into features/cron/internal
2026-08-23 23:03:51 +08:00
Haozhe
368b4b7400
refactor(agent-core-v2): migrate agent domains to agent runtime architecture (#3175) 2026-08-23 17:36:59 +08:00
7Sageer
d723cc47ee
feat(agent-core-v2): add the unified MCP management plane (#3002)
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* feat(agent-core-v2): add the unified MCP management plane

Port the v1 MCP management plane (#2858) onto the v2 DI x Scope engine:

- App-scope IMcpOAuthService shared by every workspace handler and
  session overlay: credential events, single-flight refresh, proactive
  refresh timers, OAuthTokenTransaction-serialized writes, offline
  tokenState, shutdown. Providers read tokens through the store so
  grants written or revoked by another process are honored immediately;
  http/sse transports ride the transaction fetch.
- IMcpConfigStore: the single write point for the user-level mcp.json
  over the filesystem byte store, byte-identical to v1's format, with
  per-entry validation, name normalization, __proto__-safe parsing, a
  mutation tail, and an onDidWrite event.
- IMcpRegistryService: the unified read view over the layered config
  files (with per-entry origins) and plugin manifests (full descriptors
  incl. disabled, with provenance); collisions stay visible and runtime
  resolution ranks an enabled plugin above the file layers.
- IMcpManagementService: guarded CRUD, connection-test probes, the
  locator-addressed inspection/auth-status surface, and
  locator-addressed OAuth begin/complete/cancel/reset with ambiguity
  rejection. Engine services stay ungated; the mcp_management flag
  gates the edge exposure.
- Workspace runtime aligns with v1 precedence (an enabled plugin entry
  wins over the file layers, shadows revive), and management writes
  reload immediately via onDidWrite instead of the watch debounce.
- node-sdk v2 facade delegates to the engine service (deleting its
  in-process duplication); kap-server exposes /api/v2/mcp/* and klient
  gains global.mcp.*, both flag-gated.

* fix(agent-core-v2): settle early and cancelled MCP OAuth callbacks

* refactor(node-sdk): write session MCP persists through the engine config store

* refactor(agent-core-v2): strip comments from the MCP management plane files

* fix(agent-core-v2): harden MCP management readiness

* test(node-sdk): cover offline MCP auth statuses

* fix(agent-core-v2): isolate stdio MCP probes

* fix(klient): normalize MCP OAuth errors

* fix(mcp): honor workspace CRUD context and refresh timing

* fix(mcp): drain OAuth refreshes during shutdown

* fix(mcp): guard CRUD across registry collisions

* fix(mcp): canonicalize trust and refresh scheduling

* fix(mcp): close callback listener on setup failure

* fix(mcp): preserve trust and oauth behavior

* fix(oauth): retain refresh tokens after SDK saves

* fix(oauth): stop proactive sweep during shutdown

* fix: await MCP workspace reconciliation

* fix: serialize MCP OAuth and trust cleanup

* fix: reject persisted MCP plugin collisions

* fix: reconcile MCP workspaces concurrently

* fix(mcp): check project-layer trust at the queried cwd

* fix(mcp): expire abandoned OAuth flows after an idle timeout

* fix(mcp): keep mutable user entries writable past read-only collisions

* fix(mcp): abort the auth::complete long poll on client disconnect

* fix(mcp): map OAuth flow failures to wire code 40929

* docs(mcp): note probe credential effects and plane semantics

* chore: add the SDK changeset for MCP management cwd params

* feat(mcp): expose the management plane without the experimental flag

* fix(mcp): preserve auth management semantics

* fix(agent-core-v2): bound MCP OAuth auth-server requests and the shutdown drain

* fix(node-sdk): restate engine MCP management errors as KimiError

* fix(agent-core-v2): preserve shared OAuth flow lifetime

* fix(agent-core-v2): close MCP OAuth cancellation and shutdown gaps

- bound the authorization-code exchange with the request timeout and the
  flow/caller abort signals, and make shutdown abort hung begins and close
  their callback listeners immediately
- keep token-transaction effect coalescing intact when durable tokens carry
  local stamps, and serialize the meta sidecar and tokens-saved event with
  the token write inside the lock
- drain transport-driven grants, their trailing SDK save continuations, and
  interactive completions during shutdown, with a cancellable deadline

* fix(agent-core-v2): harden MCP probe runtime resolution and path handling

- resolve stdio probes against the containing workspace's runtimes and
  reject out-of-workspace probes for non-local runtime_id instead of
  silently falling back to a local-only transient registry
- share one Windows-aware path canonicalization across the config loader,
  registry trust lookup, trust records, and workspace matching
- keep a UTF-8 BOM fatal for the user-level mcp.json store, matching the
  workspace loader and v1
- validate completeServerAuth timeoutMs bounds at the engine boundary

* fix(agent-core-v2): await workspace MCP reconciliation on plugin mutations

Plugin install/enable/disable/remove now resolve only after reload
listeners settle their waitUntil work, so a disabled plugin's MCP server
cannot linger connected and an enabled one is visible to the next
session, matching v1. The workspace MCP consumer joins the barrier while
keeping its log-only failure tolerance; delivery is awaited outside the
mutation queue to avoid self-deadlock through consumption reads.

* fix(mcp): close the SDK, klient, and server edge gaps

- register mcp.oauth_failed in the v1 error registry and restate unknown
  engine codes as internal instead of minting undeclared KimiError codes
- route persisted session MCP adds through the same KimiError restating
  as the global management methods
- give the klient IPC transport a per-call timeout so completeAuth's long
  poll outlives the 30s default, clamped to the Node timer ceiling, and
  align the contract timeoutMs upper bound with REST
- await the MCP OAuth service shutdown directly in SDK and server close
  before scope disposal

* fix(agent-core-v2): keep file-over-plugin MCP precedence and harden the plane

- Revert the v1-style precedence flip: the workspace merge and
  resolveRuntimeTarget keep the file entry above plugins (v2's historical
  order; the divergence from v1 is deliberate and documented in AGENTS.md).
- Guards follow each engine's winner: project-layer entries stay read-only,
  while plugin entries never block user-level writes, so a file entry may
  shadow a plugin and removing it revives the plugin. The parity suite pins
  the engine split for a persisted session add over a plugin-owned name.
- inspectServers tolerates a wire-encoded null targets array: klient's ipc
  transport sends null for an omitted leading optional argument.
- Fire the config store's onDidWrite after the mutation tail settles, so a
  write listener can re-enter the store without deadlocking the queue;
  concurrent-mutation and re-entrant-listener tests pin both contracts.

* chore: condense the sdk MCP changeset to one sentence

* test(node-sdk): pin verify:false auth-status parity and fix the sdk changeset
2026-08-22 18:22:37 +08:00
qer
0f44537c13
feat(agent-core-v2): turn /tower into a mode parallel to plan mode (#3099)
* feat(agent-core-v2): turn /tower into a mode parallel to plan mode

* feat(agent-core-v2): align /tower command semantics with the original skill behavior

* fix(tui): harden the /tower command against mid-turn objectives, legacy engines, and stale status

* fix(agent-core-v2): keep restored tower state inert while the feature flag is off

* fix(agent-core-v2): include TowerInit in the mode tool overlay and report flag-gated tower state

* feat(agent-core-v2): expose tower control tools statically and make the mode injection history-derived

* fix(tui): warn that tower mode needs a restart after a live flag flip; drop redundant undefined from SessionStatus mode fields

* fix(agent-core-v2): let tower mode exit clear persisted state while the flag is off

* fix(agent-core-v2): emit the tower exit reminder through a disabled flag and drop the redundant REST state comparison

* fix(tui): show the Tower mode status row only when the experiment is available

* fix(agent-core-v2): confine tower mode to the main agent and always re-assert it for objectives

* feat(kap-server): project tower mode into transcript modes and reassert explicit toggles

* fix(agent-core-v2): fold the main-agent invariant into the effective tower state

* fix(kap-server): gate the cold tower mode badge behind the experiment flag

* fix(agent-core-v2): reapply the tower tool overlay after a profile bind; fix(kap-server): clear cold tower badges on non-main agents

* fix(protocol): mirror towerMode and tower_mode in the shared zod schemas

* test(agent-core-v2): adapt tower tests to the agent lifecycle context architecture

* chore(agent-core-v2): regenerate the wire manifest; test(node-sdk): look up the main agent via findAgentHandle

* fix(agent-core-v2): exit a replayed tower mode when the workspace belongs to another session

* fix(agent-core-v2): carry tower ownership in the enter record so forks clear inherited mode

* fix(agent-core-v2): apply the tower overlay before dispatching enter and repair it on status updates

* fix(agent-core-v2): validate store ownership before entering tower mode

* fix(agent-core-v2): claim the repository tower owner at enter; fix(tui): confirm mode activation before reporting success

* fix(agent-core-v2): make the first tower claim exclusive and refuse adoption over a live owner

* feat(agent-core-v2): guard tower adoption and teardown with a cross-process ownership lease

* revert(agent-core-v2): drop the cross-process lease and enter-time claim, keep ownership checks process-local

* fix(agent-core): hide the v2-only tower flag from the legacy experiments list

* fix(agent-core-v2): keep tower mode inert until the tower feature is assembled

A live /experiments flip refreshes the flag but cannot re-run App-scope
feature assembly, so the tower tools/profile stay unregistered until a
restart. Gate enter()/isActive on the assembly fact and say so in the
TUI error. Also resolve the AGENTS.md conflict block committed by the
merge, and widen the TUI experimentalFlag type to string now that flags
live in two registries.

* fix(kap-server): gate the cold tower badge on tower feature assembly

Same live-flip gap as the mode machinery: a persisted tower_mode.enter
plus a flag enabled without a restart would still show the badge while
the feature is inert. Require isTowerFeatureAssembled() alongside the
flag, re-exported from agent-core-v2.

* fix(agent-core-v2): liveness-aware tower entry and per-App assembly state

enter() now mirrors TowerInit's adoption rule: a stored owner blocks
entry only while that session is live in this process, so a new session
can enter the mode and reach TowerInit to adopt a stale tower. The
assembly marker is keyed by each App's flag service (WeakSet) instead of
process-global module state, so coexisting Apps no longer leak assembly
into one another.

* fix(agent-core-v2): keep stale-owner adoption across resume; reject tower updates that do not take

exitForeignTower now treats a stored owner as foreign only while that
session is live, so a mode entered by adopting a dead owner's tower
survives close/restart. The TUI validates the model prerequisite before
enabling tower for an objective, and the REST agent_config path throws
session.tower_mode_invalid when enter() did not take effect instead of
acknowledging a no-op.

* fix(agent-core-v2): clear the tower assembly marker when the feature unloads

Register the WeakSet cleanup via the feature's onDispose so the
capability follows the managed unit's lifecycle — after
unprovideUnit('tower'), isActive/enter() no longer treat the retracted
tool set as assembled.

* fix(agent-core-v2): publish tower deactivation on gate loss; reject refused setTowerMode in the SDK

isActive now reconciles its projection: restore and feature-manager unit
changes publish AgentStatusUpdated({ towerMode: false }) when the
persisted mode lost a gate (flag off at runtime, feature unloaded), so
live transcript badges and TUI state stop showing an inert mode. The
SDK's setTowerMode(true) verifies the effective state and throws
session.tower_mode_invalid, matching the REST path.

* fix(agent-core-v2): reconcile tower projection on config changes and cold ownership moves

The last unreconciled gate inputs: a live setConfig writing the
experimental section (no session reload, no units event) now republishes
towerMode:false through the config-change subscription, and the cold
transcript badge mirrors the same ownership/liveness rule as enter() —
retained while the store owner is this session or no live session, cleared
once a live session elsewhere owns the tower.

* fix(agent-core-v2): reconcile the tower projection in both directions

The OFF-only reconcile left a hole: re-enabling the flag in the same
process made isActive true again with no towerMode:true publish, and
enter() could not heal it (it early-returns when already effective).
The projection now tracks the last published state and emits both
false→true and true→false transitions from the same restore/units/config
triggers; direct publishes (enter/exit/restoreTowerTools) keep the
tracker in sync.

* fix(agent-core-v2): validate forked tower ownership even while the flag is off

exitForeignTower's flag short-circuit let a fork restored while the
experiment was disabled keep its inherited enter record; re-enabling the
flag later revived both source and fork over the same store. Ownership
validation is flag-independent state hygiene, so restore now runs it
regardless of the effective flag — a live foreign owner clears the
fork's persisted mode before any gate can rise again.

* fix(agent-core-v2): veto tower tools while the tower experiment is off

With the feature assembled and the tool overlay active, disabling the
flag live left TowerInit/TowerTeardown callable — they have no flag
check — so prompts could still mutate or dismantle .tower/ while the
experiment reported disabled. A dedicated onBeforeExecuteTool hook now
denies every tower tool whenever the flag is off, mirroring the TodoList
veto.

* fix(agent-core-v2): keep tower worker write isolation when the flag turns off

The worker Write/Edit guard is identity-scoped (tower-worker profile),
not feature-activity-scoped: disabling the experiment live must not let
already-spawned detached workers write into the main checkout or other
worktrees. The guard no longer checks the flag; the tower-tool veto
added earlier covers protocol access instead.

* test(agent-core-v2): make tower tests hermetic for CI

Three CI-only failures: towerService git fixtures committed without a
repo-local identity (CI has no global gitconfig), the node-sdk tower
positive tests relied on the developer shell's
KIMI_CODE_EXPERIMENTAL_FLAG=1 master switch instead of enabling the
tower flag explicitly, and the legacy harness experimental-features
expectation still listed the tower entry removed from the v1 registry.
2026-08-21 14:47:37 +08:00
github-actions[bot]
0999454bdc
ci: release packages (#3074)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-20 21:11:45 +08:00
Haozhe
3fdce983f8
fix: drain in-flight persistence and log writes on session close and shutdown (#3122) 2026-08-20 17:19:20 +08:00
Haozhe
a09d904140
refactor(agent-core-v2): migrate agent domains to model-as-container architecture (#3103) 2026-08-20 16:06:59 +08:00
liruifengv
3d7762003a
feat(kimi-code): support two OAuth login endpoints (#2862)
* feat(kimi-code): add China/International region selection for OAuth login

- Add region profiles (cn/overseas) and resolver in @moonshot-ai/kimi-code-oauth:
  env override → persisted login host → install-channel marker → default cn
- /login now offers Kimi Code (China) / Kimi Code (International); the CLI
  login entries (kimi login, kimi acp --login) accept --region cn|overseas
- Update/plugin/site/telemetry endpoints derive from the selected region;
  plugin trust list covers both .com and .ai hosts
- kap-server: POST /oauth/login accepts an optional region; new GET /oauth/region

* fix(oauth): keep an explicit default-slot login ahead of the install marker

A China login persists no oauthHost (the default credential slot carries
no host trace), so after switching back from International the resolver
fell through to a stale overseas install marker. Treat a persisted
default-slot oauth ref (key === oauth/kimi-code) as an explicit-cn signal
that outranks the marker; getRegion() on the v2 side mirrors it.

* fix(agent-core-v2): thread the default-slot key through capability region resolution

Capability installs resolved the region from the persisted oauthHost only,
so an explicit China login (which persists no host) lost to a stale
overseas install marker. Pass the oauth ref key through as well, matching
getRegion(). Also move the region contract notes into the auth.ts file
header per the package comment convention.

* fix(agent-core-v2): honor the region-marker opt-out for the telemetry endpoint

Hosts that set KIMI_CODE_REGION_MARKER=off (the desktop embedded server)
skip the install marker in getRegion(), but the default telemetry endpoint
still consulted it, so a stale overseas marker could split the reported
region from the telemetry destination.

* feat(cli): show region site domains in login platform selector

* chore: reword oauth login changesets

* fix: honor the region marker opt-out in the CLI and capability resolvers

* refactor: rename login region values to mainland-cn and global

* fix: keep the --region help text in English

* fix: simplify the --region help text to site domains

* feat: drop the suggested login platform order

* feat: split a browser-safe region profile table out of the region resolver

* Revert "feat: split a browser-safe region profile table out of the region resolver"

This reverts commit a037b1143eeb8adcbb349264b5bd940f3ef9c0b3.

* fix: read the install marker from the bootstrapped home directory

* fix: resolve the server plugin marketplace from the active login region

* feat: expose the login region option through the klient auth facade

* fix: drop a comment from the v2 auth region test

* fix: keep scoped base-only logins on their environment for a bare login

* fix: invalidate the region cache on the provider-manager logout path

* fix: route client-config fetches through the active region profile

* fix: resolve the telemetry endpoint per flush so a login region switch applies in-process

* test: expect the telemetry endpoint resolver in the CLI init assertions

* fix: resolve the default telemetry endpoint from the bootstrapped home

* chore: reword the oauth login changeset around the two login methods

* chore: trim the oauth login changeset to the headline

* feat: let hosts override the region marker env through the server bootstrap env bag
2026-08-20 11:24:02 +08:00
Luyu Cheng
8440801de4
feat(agent-core-v2): add the WaitFor tool for waiting on background tasks (#3060)
* feat(agent-core-v2): add the WaitFor tool for waiting on background tasks

* fix(agent-core-v2): mark WaitFor deliveries only after formatting succeeds

* fix(agent-core-v2): cancel losing waits once the WaitFor race resolves

* test(node-sdk): project WaitFor out of the v1-v2 resume parity roster

* fix(agent-core-v2): gate WaitFor goal guidance behind the wait_for flag

* fix(agent-core-v2): gate WaitFor goal guidance on actual tool availability

* fix(agent-core-v2): enforce the wait_for flag at WaitFor execution time

* fix(agent-core-v2): consult the live tool policy in the WaitFor availability check
2026-08-19 11:26:59 +08:00
github-actions[bot]
04944f380a
ci: release packages (#2932)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 19:19:18 +08:00
Haozhe
98ebda840a
fix(kimi-code): revert the todo panel to its pre-turn state on undo (#3016)
Some checks are pending
CI / build (push) Waiting to run
CI / test (1) (push) Waiting to run
CI / test (2) (push) Waiting to run
CI / test (3) (push) Waiting to run
CI / test (4) (push) Waiting to run
CI / test (5) (push) Waiting to run
CI / test-pi-tui (push) Waiting to run
CI / test-vscode-legacy (push) Waiting to run
CI / test-windows (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
* fix(kimi-code): revert the todo panel to its pre-turn state on undo

* fix(kimi-code): hide all-done todo lists on undo refresh and detach SDK todo state
2026-08-18 12:00:19 +08:00
Haozhe
3ded08084a
fix(protocol): expose turn ended event time (#3011)
* fix(protocol): expose turn ended event time

* fix(protocol): expose turn ended event time

* chore(changeset): remove patch release entry

* test(node-sdk): align background task parity expectations
2026-08-18 10:16:12 +08:00
Haozhe
2265305e81
refactor(agent-core-v2): replace defineOp/Model with Event2 dispatch and replayable states (#2909)
* refactor(agent-core-v2): replace defineOp/Model with Event2 dispatch and replayable states

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

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

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

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

- update snapshots for the durable prompt.accepted event
- normalize prompt.steered media in transcript projections
2026-08-17 17:38:50 +08:00
7Sageer
59dde734f3
feat(agent-core): unify the v1 MCP management plane (#2858)
* feat(agent-core): unify the v1 MCP management plane

- McpServerRegistry: one config view over global (layered mcp.json),
  plugin (manifests, read-only, final effective config), and caller
  (SDK-injected) servers; name collisions keep both entries.
- Write plane: add/update/removeGlobalMcpServer mutate the user-level
  file and push into live sessions; getGlobalMcpServer returns the
  effective config; mutations of read-only entries are rejected.
- testGlobalMcpServer accepts an inline config; addSessionMcpServer
  connects a server in one live session with an optional persist flag;
  reconnect accepts a replacement config and re-resolves via the registry.
- One process-wide McpOAuthService shared with every session: obtained_at
  stamps, offline token state, single-flight and proactive refresh, and
  credential events. Sessions self-subscribe in the constructor, so even
  initializing sessions see every event; token writes serialize through
  the process-local OAuthTokenTransaction per credential identity.
- inspectAppMcpServers + locator-addressed begin/complete/cancel/reset
  cover plugin servers; inspection output redacts env/headers to sorted
  key lists; locator OAuth ops reject ambiguous shared runtime names.
- The legacy auth-status surface reads the registry (offline by default,
  verify=true probes) and never mutates credentials.
- VS Code panel receives source/origin/mutable and hides mutating
  actions on read-only entries.
- v2 client facade in node-sdk mirrors the surface over agent-core-v2
  (plugin inventory stays v1-only for now).

* fix(agent-core): close the v1 MCP live-session reconciliation gaps

Recompute each live session's MCP target from the registry's runtime
resolution (enabled plugin > project layer > user file; caller injection
shadows everything) behind every config mutation, instead of per-path
patching: shadowed file layers recover when a plugin winner is disabled or
removed, removing a user-level entry resurrects its project-layer shadow,
disabled plugin descriptors no longer block removals, persisted session
adds validate against the session's project layer and broadcast to other
live sessions, and per-session sync failures are logged with context.

Session status entries and read-only management entries now report
redacted config views (envKeys/headerKeys instead of literal env/headers
values); core-internal reconciliation compares full configs via the
connection manager's raw-entry accessor.

OAuth: interactive flows are serialized per credential (concurrent
begins join the in-flight flow instead of clobbering its PKCE/state), a
malformed credential meta sidecar no longer aborts core start, grants
inside the refresh-ahead window refresh immediately while far-future
grants re-arm through a max-length timer, and the service shuts its
timers and flows down with KimiCore/SDKRpcClient close.

* fix(agent-core): route the proactive MCP OAuth refresh through the token transaction

 refreshNow ran its /token request with the SDK default fetch, outside the
 credential-serializing OAuthTokenTransaction that every other token write
 uses; a slower response carrying an older rotating refresh token could
 overwrite a newer grant written by a concurrent transport-side refresh.

* fix(agent-core): keep disabled MCP servers out of auth-state classification

The unified mcpServerAuthState dropped the previous enabled short-circuit,
so a disabled oauth-flagged server reported oauth-required — or was even
probed over the network — instead of not-applicable.

* fix(kimi-code-sdk): short-circuit disabled MCP servers in the v2 auth-status classifier

The v2 parity copy of v1's mcpServerAuthState missed the same enabled
guard v1 just regained; a disabled oauth-flagged entry would report
oauth-required (or be probed). The parity suite now pins the disabled
case on both engines.

* fix(kimi-code): refresh the VS Code MCP list with the workspace cwd after mutations

The add/update/remove RPCs return a cwd-less management list, so the
webview broadcast dropped project-layer entries until the next full load;
re-list with the workspace cwd after every mutation instead.

* fix(agent-core): keep SDK token saves matched to the OAuth token transaction

saveTokens stamped obtained_at onto a fresh object before calling
tokenTransaction.save, so it never matched the exact payload the
transaction recorded for a grant fetch; the consume path was dead and
every save re-wrote. Between the fetch and the SDK callback an intervening
clear could then be overwritten — the resurrected grant came back after a
reset. The write callback stamps the durable record instead.

* fix(agent-core): reject ambiguous legacy name-based MCP auth lookups

The legacy begin/reset auth RPCs took the registry's first name match,
silently starting OAuth for one entry of a runtime-name collision while
the locator path refused the same ambiguity; align them on the shared
uniqueness rule and point callers at the locator-addressed variants.

* fix(agent-core): propagate registry errors during live-session MCP sync

resolveMcpRuntimeTarget collapsed every registry failure into "no target":
a project config file that turned malformed mid-session made sync treat a
still-configured server as gone (tearing down the live connection) and
made config-aware reconnects report "no longer configured" instead of the
actionable config error. Absence still resolves to undefined; malformed
config now propagates — per-session sync logs and keeps the entry, and
reconnect surfaces config.invalid.

* fix(agent-core): close the remaining registry-error and ambiguity gaps

The management guard lookup mapped every registry failure to "absent",
so a malformed project config let a persisted session add write a
user-level entry over an unknown state; only not-found is a miss now. And
the name-only connection test now shares the auth paths' uniqueness rule
instead of probing the first match of a runtime-name collision.

* fix(agent-core): probe the enabled MCP entry under a disabled-name collision

The name-only connection test counted enabled matches for its ambiguity
guard but still probed the first registry match, and the file layers list
before plugins. With a disabled file entry shadowing an enabled plugin of
the same runtime name, Test probed the disabled entry instead of the one a
live session would run. Select the sole enabled match, falling back to the
first entry only when every match is disabled so it reports as disabled.

* fix(agent-core): let session-local MCP adds shadow plugin entries

Caller injection shadows every registry source at session start, plugins
included, and reconciliation leaves caller entries untouched; the live
non-persist add path rejected plugin-owned names anyway, so SDK clients
could not apply the same per-session override without a restart. Gate the
plugin-source rejection on persist: session-local adds connect as caller,
while persisted adds stay rejected as user-level writes behind a read-only
owner.

* fix(agent-core): normalize session MCP names before connecting

The persisted store trims server names, but addSessionMcpServer used the
raw name for the live connect and cross-session reconciliation: a padded
name persisted under the trimmed key while the requesting session ran and
reconciled the raw one, and a blank name connected with no identity at
all. Normalize once up front (rejecting blank) so the store write, the
session entry, and reconciliation agree on the same server.

* fix(agent-core,node-sdk): close the collision-selection and probe-freshness gaps

The legacy name-only auth resolver started from the first registry match,
so a disabled file-layer shadow plus an enabled plugin of the same runtime
name was misread as an ambiguity conflict; select the sole enabled match
before judging ambiguity, exactly like the test probe path. On the v2
client, addSessionMcpServer connected the raw name while the store wrote
the trimmed key — normalize once for both, and route the verify-triggered
auth probes through the per-call OAuth service instead of the cached one
whose providers snapshot tokens at construction, so a grant saved after
the first probe is honored.

* fix(agent-core): normalize global MCP mutation names and guard disabled reconnect swaps

The global add/update/remove mutations guarded and reconciled with the raw
server name while the store persisted the trimmed key, so a padded name
left live sessions unreconciled and could slip past the plugin read-only
guard; normalize once before lookup, persistence, and reconciliation. And
a config-carrying reconnect assigned the replacement before the disabled
check fired, leaving a connected entry that reported the disabled config;
reject disabled replacements before mutating, keeping the same error.

* fix(agent-core): skip proactive refresh while an interactive flow owns the credential

refreshNow reset the shared provider's flow state before and after the
token request; when a proactive timer (or a manual refresh) fired while
beginAuthorization was waiting on the browser callback for the same store
key, that wiped the redirect URL, PKCE verifier, and state the in-flight
flow needed — complete() then failed the exchange even though the user
authorized. Refresh now skips when an interactive flow is active for the
credential: the flow delivers fresh tokens on completion, and the 401
transport path is the backstop if it fails.

* fix(agent-core): allow global MCP adds over disabled plugin descriptors

A disabled plugin entry is absent from the runtime target, but the
read-only guard still treated it as the owner, so a user-level fallback
could only exist if it predated the plugin disable. Relax the shared
guard: disabled plugin descriptors never block mutations (disabled
project entries still shadow the user file and keep their rejection).

* fix(node-sdk): close the v2 session-MCP parity gaps

A v2 reconnect with an explicit enabled:false replacement config used
connect()'s upsert semantics — closing the live client and reporting
success where v1's manager reconnect rejects before applying anything;
reject disabled replacements up front with the same error. And a persisted
v2 session add never consulted the workspace config, so a same-named
project-layer entry was silently shadowed: the user-level write never
takes effect while the direct workspace-manager upsert displaces the
project config for every live session. Resolve the workspace layers and
reject like v1's read-only rule.

* fix(agent-core): keep __proto__-named MCP servers through config parsing

A z.record() parse rebuilds its output via property assignment, so a
server literally named __proto__ hit the prototype setter and vanished
before validation; the layer merge then repeated the same trap with plain
object accumulators. Parse the server map entry-by-entry over the JSON own
keys and accumulate into null-prototype maps, so session startup and the
unified registry keep the declared server and its origin.

* fix(node-sdk): begin v2 MCP auth against a fresh OAuth service

The v2 begin path ran through the cached globalMcpOAuth, whose providers
snapshot tokens at construction: a grant another process saved (or reset)
after that cache materialized was invisible, so begin could open a browser
flow over a valid grant, or report already-authorized off a removed one.
Build the service per call — the read path and the verify probes already
do — and route the status list through the same helper. The test fixture
grows a real token endpoint honoring one rotating refresh token; the
regression fails against the cached-service implementation on v2.

* fix(agent-core): broadcast SDK-driven MCP token invalidations to live sessions

* test(agent-core-v2): give the no-op reconnect test runtime plumbing

The branch added the case against a bare McpConnectionManager, but #2961
made stdio connects resolve the runtime through runtimeResolver, matching
every other case in the file.
2026-08-17 13:19:51 +08:00
7Sageer
d833a1a893
feat: engine-native image references via kimi-file:// media resolver (#2593)
* feat: engine-native image references via kimi-file:// media resolver

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: clean staged media lifecycle

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

* fix: keep staged media alive through turn

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Two review fixes around prompt media intake:

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

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

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

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

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

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

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

* fix: decouple media persistence from prompt intake

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

* test: slim redundant media-ref coverage across layers

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

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

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

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

* fix: harden image file reference lifecycle

* fix: close image reference lifecycle gaps

* fix: preserve session media paths on replay

* chore: streamline image-file-ref changesets

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

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

* fix: reconcile image file references with main after rebase

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

feat(packages): update throttle control

feat(agent-core): rename to /tower

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

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

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

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

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

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

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

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

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

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

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

fix(tower): disable todo-list tool

feat(tower): reviewer keep primary model

fix(tower): tower worker call for authroization

update

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

* feat(tower): remove builtin.ts

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

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

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

* fix(tower): allowActivationWhileBusy for all skill

* update

* update

---------

Co-authored-by: konghuanjun <konghuanjun@moonshot.ai>
2026-08-16 15:13:42 +08:00
github-actions[bot]
13d86f8b7b
ci: release packages (#2881)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-14 20:51:17 +08:00
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
Haozhe
1414d46028
refactor(agent-core-v2): fold session lifecycle hooks into sessionLifecycle events (#2896) 2026-08-13 21:03:51 +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
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
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 98a4a6d99925e45352a3d8ac6d0831a1024dbda8.

* 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