* refactor(agent-core-v2): extract swarm into a scope-organized feature
- move src/agent/swarm, src/session/swarm, and src/agent/tools/agent-swarm
into src/features/swarm/{agent,session,tools/agent-swarm}; swarmOps.ts
stays a static import=register wire channel at the feature root
- add SwarmFeature carrying the three runtime registrations
(IAgentSwarmService, ISessionSwarmService, IAgentSwarmTool) with
ScopeActivation.OnScopeCreated preserved
- switch src/index.ts to precise leaf exports and update import sites,
including the kap-server and kimi-inspect deep-path imports
- move tests to test/features/swarm and re-assert service overrides in
the test harness so stubs keep winning over feature contributions
* fix(agent-core-v2): keep feature-contributed tools in Agent tool descriptions
SubagentTool.knownToolReferences() now reads the full AgentToolContribution
collection (static registrations and feature contributions alike) instead
of the static contribution table. A caller profile that does not activate
a feature-contributed tool (e.g. AgentSwarm) no longer drops it from the
per-profile tool listings the description advertises for spawned profiles
when a workspace/session restriction forces explicit enumeration.
Add a regression test with a caller profile lacking AgentSwarm under a
global tool restriction.
* refactor(kap-server): lift session profile updates to the route edge
- add sessionProfile.ts/sessionAgentConfig.ts route helpers that resume
the session and dispatch title/metadata and the agent_config patch to
the native v2 services directly
- drop updateProfile from ISessionLegacyService, leaving only the
status rollup and the goal read in the legacy adapter
- wire shape and client-visible behavior unchanged
The [subagent] default_model / models deprecations added in #2700 guard a
migration path that has no users: the pool keys only existed in #2700's own
intermediate commits and never shipped in any release, so no config written
against a released version can contain them. Remove the two deprecation
entries (the mechanism stays — the released loop_control renames still use
it), the migration notes in the en/zh config docs, the agent-core-dev skill
note, and the obsolete test; regenerate the config manifest.
No changeset: #2700 is still unreleased, so no published version ever
emitted these warnings — the removal is invisible to users.
* feat: replace secondary-model experiment with [subagent.models] pool
Add a declarative subagent model pool to agent-core-v2: [subagent.models]
maps [models] entry ids to selection hints rendered in the Agent/AgentSwarm
tool descriptions, and [subagent].default_model picks the spawn model when
the caller passes none. The tools' model parameter becomes a free-form
alias string (stripped when no pool is configured), description rendering
is caller-aware (primary (alias) [main model]), and a session-start
validation service fails fast with CONFIG_INVALID on a missing/invalid
default_model or an unresolvable pool alias.
Remove the secondary-model experiment from the v2 engine, node-sdk,
kap-server, and the TUI (the /secondary_model command), and drop the
agent-profile modelPreference / model_preference frontmatter field on v2.
The legacy v1 engine keeps the experiment unchanged; v2 ignores leftover
[secondary_model] config silently.
* fix(agent-core-v2): harden subagent model-pool validation and error/picker mapping
Deep-review follow-ups to the [subagent.models] pool:
- validate the pool before session materialization (after config.ready)
and before the fork file copy, so a broken pool no longer leaves
orphaned session dirs or leaked MCP overlay connections; the
Session-scope validation service stays as a backstop
- reject the reserved "primary" pool alias at startup, and again
defensively in resolveSubagentBinding so a pool broken by a runtime
config edit fails loudly at spawn instead of binding the wrong model
- keep the [default] marker when the caller's own model is the pool
default (primary (alias) [main model] [default])
- recompile the cached tool-args validator when a tool advertises a new
schema object (mid-session pool edits no longer hit a stale validator)
- map config.invalid to VALIDATION_FAILED in kap-server's session routes,
the debug transport mapper, and the catch-all error handler
- hide the v1-synthesized __secondary__ entry from the /model and
/provider pickers again
- fold per-export doc blocks into file headers per package comment
conventions; add pre-flight/reserved-key/validator/mapping tests and
document that create/resume/fork all fail on a broken pool
* feat: re-add /secondary_model and accept a lone subagent default_model
- v2 engine: a pool-less [subagent] default_model forms an implicit
single-entry pool — validated at session create/resume/fork like an
explicit pool, and advertised through the Agent/AgentSwarm model
parameter.
- Tool descriptions: the caller's own alias is a normal pool entry
marked [main model]; the primary line stays distinct because only it
inherits the caller's thinking level.
- TUI: /secondary_model returns, persisting [subagent] default_model
(merging into an existing pool with an empty description); the picker
hides the no-op Thinking footer and rejects the reserved primary
alias.
- kap-server: /api/v1/config accepts and echoes subagent; the
snake-to-camel patch conversion preserves user-defined map keys under
providers/models/experimental/raw without leaking preserve mode into
a colliding alias's own fields.
- v1 config schema learns subagent.defaultModel/models so the shared
config.toml round-trips; the v1 engine still ignores them at runtime.
- Docs (en/zh) and changesets updated.
* docs: use public model identifiers in the subagent model pool examples
* refactor: rename /secondary_model to /secondary-model
* test: cover the /secondary-model command name resolution
* Revert "test: cover the /secondary-model command name resolution"
This reverts commit 98a4a6d999.
* feat(agent-core-v2): move the subagent model pool to [secondary_model]
The pool keys (default_model, [secondary_model.models]) now live in their
own [secondary_model] config section instead of [subagent], which keeps
only timeout_ms; legacy [subagent] pool keys are ignored with a
deprecation warning. The SDK config contract carries the pool on the
secondaryModel field, so the TUI /secondary-model command (now also
aliased /subagent-model) and the kap-server /config wire read and write
it directly with no translation layer.
* docs: correct default engine guidance
* feat(agent-core-v2): pin subagents to default_model with [secondary_model] force
force = true removes the main agent's per-spawn model choice: the Agent
and AgentSwarm tools stop advertising the model parameter and every spawn
binds default_model; an explicit choice, "primary" included, is rejected.
The setting requires default_model, rejects a [secondary_model.models]
table, and is validated loudly at session create/resume/fork (lifecycle
preflight plus the Session-scope backstop). The v1 engine declares the
key for write round-trips and excludes it from the recipe patch.
Also documents pool entries as per-alias thinking-level variants via
default_effort overrides.
* docs: use real managed model aliases in the secondary_model examples
The pool examples invented aliases (kimi-hs, fable, codex) and referenced
non-existent model IDs (model = "codex"); they now reference only the
managed aliases provisioned by /login (kimi-code/k3,
kimi-code/kimi-for-coding, kimi-code/kimi-for-coding-highspeed), with the
effort variant derived as kimi-for-coding-highspeed-deep. Also replaces the
versioned kimi-k2.5 alias with kimi-for-coding per the docs model-ID rule.
* chore: simplify the subagent model pool changeset
* feat(agent-core-v2): honor the legacy [secondary_model] model key as a fallback default
* refactor(node-sdk): export the reserved model-alias constants from the SDK
Restore the SECONDARY_DERIVED_MODEL_ALIAS re-export and add
PRIMARY_SUBAGENT_MODEL_CHOICE so the TUI imports both from
@moonshot-ai/kimi-code-sdk instead of vendoring local copies.
* feat(agent-core-v2): keep the subagent model pool behind the secondary-model experiment
Restore the secondary-model flag gating so this change only adds the pool:
with the experiment off the [secondary_model] pool keys stay inert — the
Agent/AgentSwarm tools strip the model parameter, spawns inherit the
caller's model, and startup pool validation is skipped. The /secondary-model
slash command is gated behind the experiment again, and the docs and
changeset describe the flag.
* fix(node-sdk): cascade provider removal into the subagent model pool
Deleting a provider left [secondary_model] entries pointing at the removed
model aliases; with the secondary-model experiment on, every subsequent
session create/resume/fork then failed pool validation. planProviderRemoval
now filters dangling pool entries, and drops the whole section when its
effective default (defaultModel, or the legacy recipe's model fallback)
dangles — folded into the same atomic multi-section replace.
* fix(agent-core-v2): close two subagent model pool validation gaps
Spawn-time resolveSubagentBinding now rejects force combined with a
[secondary_model.models] table, matching the startup pre-flight — a live
session could otherwise reach that invalid state through a deep-merged
config patch and only fail on the next create/resume. The session
lifecycle pre-flight also awaits the kosong model/provider registries'
ready alongside config.ready, so a cold bootstrap no longer fails a valid
pool with CONFIG_INVALID against an empty registry.
* test(agent-core-v2): stub the model/provider registries in handler-chain tests
SessionLifecycleService now awaits IModelService/IProviderService readiness
in its pool pre-flight, so the tests that assemble the real service through
a hand-built container must register the two tokens.
* fix(kap-server): cascade REST provider deletion into the subagent model pool
The DELETE /providers route rewrote only the providers and models
sections, so a pool referencing one of the deleted provider's aliases was
left dangling and the engine's create/resume/fork pool validation failed
every subsequent session until the user repaired the TOML by hand. Filter
dangling pool entries and drop the section when its effective default
dangles, mirroring the SDK's planProviderRemoval semantics.
* fix: keep the subagent pool consistent on provider replace and preserve legacy recipe fields
PUT /providers rebuilds the provider's alias set and can drop or rename
aliases referenced by [secondary_model]; the pool now cascades there too —
renamed aliases are repointed (mirroring the global default-pointer
migration), dropped aliases are filtered, and the section is cleared when
its effective default dangles.
The v2 [secondary_model] schema also declares the legacy recipe patch
fields (default_effort, max_output_size, ...) so validation no longer
strips them: pool resolution keeps ignoring them, but config reads/writes
now round-trip losslessly instead of silently deleting them from
config.toml on any pool write.
* fix(agent-core-v2): cascade the subagent pool on catalog refresh writes
A background provider/model refresh rewrites the [models] table without
touching [secondary_model], so a dropped alias left the pool dangling and
every subsequent session create/resume/fork failed validation until the
user repaired the TOML by hand — the same gap the SDK and REST write
paths already had, but triggered unattended.
The cascade helper now lives in agent-core-v2 next to the section it
protects (cascadeSubagentModelPool): the discovery service folds the pool
into the same atomic replaceSections transition, and kap-server's
provider write routes reuse the shared helper instead of a local copy.
* fix: cover the last two model-table write paths for the subagent pool
ModelsDevImportService's catalog and custom-registry imports rebuild the
[models] table without the pool cascade, so an import that drops a pooled
alias left a dangling pool behind; both final write passes now fold the
pool through cascadeSubagentModelPool (the drop passes deliberately skip
it). The StubConfigService test double now treats a null section value as
a delete, matching the real ConfigService.
The TUI's provider overwrite flow removes an existing provider before
re-adding it, which ran the removal cascade against a model table where
every alias of that provider was absent and silently dropped the pool;
the flow now snapshots secondaryModel up front and restores the entries
that survive the re-add, via the cascade helper re-exported from the SDK.
* refactor(agent-core-v2): remove the agent RPC aggregation layer
- delete src/agent/rpc/ (AgentRPCService, IAgentRPCService, core-api,
prompt-metadata, types) and sink each method's orchestration into its
owning domain service
- prompt: new submit/submitSteer composing disabledTools gating,
MAIN-only session metadata, and engine-side {turn_id} settlement
- skill: activate now returns PromptLaunchResult and writes session
metadata internally (MAIN-only, unified across prompt/steer/skill/
pluginCommand); node-sdk and kap-server drop their edge-side writes
- pluginCommand: new agent-scope domain owning command activation and
the plugin_command.activated domain event
- permissionMode/loop/fullCompaction: new setModeAndBroadcast /
cancelFromUser / cancel; setMode and loop.cancel stay pure for
internal callers
- klient: agentRpcContract split into per-domain contracts; facade
re-routes to domain channels with its public API unchanged
- node-sdk, kap-server, kimi-inspect and the v2 test harness now call
domain services directly; ctx.rpc keeps its name as a composed
adapter
- externally visible: the agentRPCService debug channel is gone and
session metadata writes are now MAIN-agent-only (see changeset)
* refactor(agent-core-v2): move disabledTools gating out of the prompt domain
Prompt should not own session tool policy: submit no longer accepts or
applies disabledTools. The klient facade keeps its prompt({ disabledTools })
API and composes it edge-side — applying agentToolPolicyService
setSessionDisabledTools before calling agentPromptService.submit, the same
way kap-server's prompt route already does. Over klient, a profile-less
engine now surfaces the raw profile error instead of request.invalid.
Also restores the RPC-removal changeset, which did not make it into the
previous commit.
* chore(agent-core-v2): drop the RPC-removal changeset
* refactor(klient): drop disabledTools from the prompt entry entirely
The prompt path no longer carries session tool gating on any surface:
the klient facade prompt() loses the disabledTools field and calls
agentPromptService.submit directly, and the node-sdk
SessionPromptRpcInput stops accepting or forwarding it (v1 always
ignored the field). Session tool gating remains available through
IAgentToolPolicyService.setSessionDisabledTools, composed at the edge
the way kap-server's prompt route does; the klient toolPolicy contract
added for facade-side composition is removed as unused.
* fix(agent-core-v2): drop interrupted thinking-only assistant messages at settle
A turn interrupted while the model is still streaming thinking leaves the
open assistant holding only an unsigned thinking fragment. The fold used
to seal it into history because a non-empty thinking block is not vacuous;
on OpenAI-compatible providers the serialized message then carries neither
content nor tool_calls, and strict gateways reject every later request
with a 400 (#1404). Treat unsigned-thinking-only content as unsendable at
settle so the fold drops the message instead — replaying the records of
an already bricked session repairs it.
* fix(agent-core-v2): preserve reasoning-only assistant history
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix(agent-core-v2): disable SDK-internal retries that blocked cancellation
The OpenAI and Anthropic SDK clients default to maxRetries=2 with a
backoff sleep that never observes the request AbortSignal, so Ctrl+C
during a 429/5xx/connection-error retry only took effect after the
sleep elapsed, and the hidden attempts were invisible to the engine
(no turn.step.retrying) while double-counting its retry budget.
Build those clients with maxRetries: 0 so retryable failures surface
to the engine's step-retry layer immediately (observable countdown,
abortable sleep, single retry budget). The Google GenAI main request
path only retries when httpOptions.retryOptions is explicitly set, so
there is nothing to disable; instead its error converter now recovers
the server-directed delay from the wire body's google.rpc.RetryInfo
detail, since the SDK's ApiError drops the Retry-After header.
* chore: simplify the retry-cancellation changeset entry
* fix(agent-core-v2): recover GenAI retry delay from prefixed mid-stream error chunks
Mid-stream error chunks throw ApiError with the message wrapped as
"got status: <STATUS>. {json}", so a strict JSON.parse of the whole
message missed the google.rpc.RetryInfo detail. Locate the JSON object
start before parsing; the non-stream path (pure JSON body) is
unaffected.
* feat(kimi-code): re-baseline pi-tui on upstream v0.84.1 and add fullscreen tui_mode
Re-baseline the vendored pi-tui fork on upstream @earendil-works/pi-tui
v0.84.1, keeping all local patches: narrow-terminal hardening,
processed-line render caching (re-implemented into TuiMainScreen), editor
history hooks, the paste-burst fallback, and multi-root @ completion.
Upstream highlights absorbed: the renderer splits into TuiMainScreen and
TuiAltScreen behind a TUI interface, the Markdown component gains opt-out
LaTeX rendering (disabled on the kimi-code side), paste-registry repair
on delete/undo, Windows input-latency and Shift+Enter fixes, and Kitty
image layout fixes. Editor.setText gains a preservePasteRegistry option
so paste-marker expansion survives wholesale text replacement.
New tui_mode = "fullscreen" preference mounts TuiAltScreen: the
transcript lives in a primary ScrollView with follow-end, the chrome
docks at the bottom, mouse selection and scrollbar come from the
renderer, and full-screen viewers (tasks browser, output viewer, approval
preview) swap the layout root via screen-takeover. Viewport navigation
keys fall through to the focused component when the primary scroll view
cannot scroll.
* feat(pi-tui): merge upstream main through 40a3d85 (post-0.84.1)
Bring in upstream's merged-but-unreleased changes on top of the v0.84.1
re-baseline:
- Fullscreen transcript search (ctrl+shift+f, next/previous navigation)
- Alternate-screen render-churn reduction (9-18x less per-frame
allocation by painting full-width rows as direct line references)
- Unbound single-line scroll actions (tui.altScreen.lineUp/lineDown),
wired into the fork's canScroll gating like the other viewport keys
- SSH-aware escape-timeout default and PI_TUI_ESC_TIMEOUT override
- Search snapping and SGR-mouse fragmentation fixes; LaTeX newline
argument fix
Conflicts resolved by union: upstream's search/line scroll bindings stay
ungated, fork's primaryScrollable guard applies to all scroll actions.
* fix(kimi-code): keep fullscreen dock from crushing the editor box
The fullscreen layout gave the transcript ScrollView its intrinsic
content height as basis and let the dock participate in shrink
distribution with no minSize. Once the transcript exceeded the screen,
the VStack shrink pass crushed the dock to a couple of rows, and the
editor (3 rows: top border / input / bottom border) lost its bottom
border row to clipping.
Adopt pi's sizing contract: the ScrollView starts from basis 0 and
grows, the dock keeps its intrinsic height, the editor never shrinks
below 3 rows, and the footer below 1. Adds a VirtualTerminal-level
regression test that replays a full streaming cycle in fullscreen.
* docs(kimi-code): document the tui_mode preference in tui.toml
* fix(pi-tui): let terminal focus reports fan out in fullscreen
TuiAltScreen's viewport input listener consumed FOCUS_IN/FOCUS_OUT
reports. Since the renderer installs that listener at construction —
before any app-level listeners — terminal focus tracking and
clipboard-image hints never saw focus transitions in fullscreen mode
(notification_condition = "unfocused" went blind, refocus clipboard
hints stopped). Keep the selection cleanup but stop consuming, matching
the main-screen fan-out. Addresses Codex review on PR #2830.
* fix(kimi-code): wire openUrl and right-click paste in fullscreen
Mouse capture in the alternate screen intercepts the terminal's native
link activation, leaving OSC 8 hyperlinks (like the footer's PR link)
unclickable in fullscreen. Route renderer link clicks to the app's
openUrl, and on Windows feed right-clicks to the focused component as a
bracketed paste read from the clipboard.
* feat(kimi-code): fullscreen prompt navigation, exit replay, progress resync
- Mark user/assistant transcript messages with OSC 133 zones (start /
end / final) so the fullscreen renderer's Ctrl-Shift-Up/Down prompt
jumps work; GutterContainer keeps the markers at byte 0 when prefixing
its gutter, and message render caches store already-marked lines.
- On exit from fullscreen, preserve the frame and replay the transcript
through a fresh main-screen renderer so native scrollback gets the
regular inline layout (pi's "transcript" exit form).
- Re-sync the OSC 9;4 progress indicator after a stop/start cycle:
terminal.stop() clears it, and the cached progressActive flag used to
suppress the re-send when returning from the external editor mid-turn.
* feat(kimi-code): enable Markdown LaTeX rendering with a render_latex opt-out
Align with the upstream pi-tui default: LaTeX math in Markdown messages
renders as Unicode text. The explicit renderLatex:false we set during
the re-baseline becomes a shared Markdown options helper fed by a new
tui.toml preference (render_latex, default true), wired at startup and
refreshed on /reload.
* refactor(kimi-code): gate fullscreen behind KIMI_CODE_TUI_FULL_SCREEN
Drop the public tui_mode preference from tui.toml before release; the
fullscreen UI is experimental, so enable it with the
KIMI_CODE_TUI_FULL_SCREEN=1 env var instead. Docs move from the
config-file reference to the env-vars page.
* chore(changesets): clarify fullscreen mode and LaTeX formula entries
* chore(changesets): trim fullscreen mode entry
* chore(changesets): trim LaTeX formula entry
* chore(changesets): drop redundant kimi-code entries
* test(kimi-code): add stepRetry to fullscreen layout fixture after main merge
* fix(kimi-code): apply render_latex before theme-driven Markdown rebuilds
Codex review on PR #2830: applyReloadedTuiConfig set the shared LaTeX
toggle after applyTheme(), but theme application invalidates transcript
components and their rebuilt Markdown children copy the options at
construction — so a /reload that only flipped render_latex kept the old
value until some later invalidation. Move the setter before applyTheme
and pin the ordering with a test.
* fix(kimi-code): carry renderLatex through TUI config saves
Codex review on PR #2830: currentTuiConfig omitted renderLatex, so
saving an unrelated preference (theme/editor/upgrade/cache-hint)
serialized render_latex as the default true and silently reset a user's
opt-out. Carry the appState value through the shared save payload.
* feat(kimi-code): report tui_mode in lifecycle telemetry
Tag startup_perf and exit events with the active renderer mode
(regular/fullscreen) so fullscreen adoption is measurable while it is
gated behind KIMI_CODE_TUI_FULL_SCREEN.
When a plugin manifest omits `skills` and the plugin root contains a
SKILL.md, the fallback treated the whole plugin root as a generic skill
scan directory, so sibling Markdown files such as CHANGELOG.md were
misidentified as skills and inflated the plugin skill count.
Mark the fallback root as root-skill-only so discovery parses only the
root SKILL.md; explicit `skills` entries (including "./") keep the
directory scan semantics. Applied to both agent-core and agent-core-v2.
* refactor(agent-core-v2): unify model-facing reminder scheduling
Route every model-facing reminder through the contextInjector boundary
scheduler. Past-tense events go through a persisted once-reminder queue
(reminderQueue) that delivers exactly once at turn, step, compaction,
and restore boundaries; present-tense state renders through
context-injection providers reconciled against live history.
- interruption, goal (cancel/budget/fork-cleared), image-compression
captions, btw, and init reminders enqueue into reminderQueue instead
of writing the context directly; the interruptionReminder wire model
is removed and its recorded type is retired silently on replay
- swarm mode announcements render through a provider seeded from the
replayed history on restore, replacing live side effects and the
ContextModel pop reducer on swarm_mode.exit
- loadable-tools announcements become an isNewTurn-gated provider,
dropping the compaction boundary flag
- plugin session-start guidance re-renders as a supersedes reminder at
the next boundary via a dirty flag instead of appending immediately
- legacy system_trigger origins of migrated reminders still fold on
replay
* fix(agent-core-v2): make system reminders undo-aware
* test(agent-core-v2): migrate plugin session-start harness
* fix(agent-core-v2): preserve reminder boundary ordering
* refactor(agent-core-v2): narrow reminder and swarm helper exposure
- drop the swarmInjection re-export from the package index; SwarmInjection
stays a domain-internal collaborator like permissionMode/plan injections
- move INTERRUPTION_REMINDER text back to a private constant in the service;
only the variant stays in the Ops module
- make reminderQueue.enqueue return void; no caller consumed the entry id
* chore(agent-core-v2): keep comments in module headers
* refactor(agent-core-v2): track reminder state via injection disclosure
- derive swarm active/inactive state from ctx.lastDisclosure instead of
byte-matching rendered markdown, with variant-only fallback for legacy
swarm_mode/swarm_mode_exit journal entries
- record once_reminder disclosure (entry id) on queue-appended messages
and dedupe the crash window by the contiguous tail id set, covering
multi-entry drains
- move reminderQueue draining behind a sync onWillInject event so the
injector no longer depends on the queue domain
- centralize the system-reminder wrap format behind wrapSystemReminder /
systemReminderContent and use injector-provided positions in the plugin
session-start provider
- spell out the step-boundary fallback and sync-only contract of
registerAtTurnStart via shouldRunAtBoundary
* fix(agent-core-v2): isolate failing turn-start providers and warn once per missing sessionStart skill
* refactor(agent-core-v2): compute injection positions on read
Drop the per-provider positions cache from the context injector: the
registration scan, the context.spliced index arithmetic, and the
post-restore resync all existed only to mirror what the history already
records. Each provider call now derives its injected positions by
scanning context memory for its surviving injection messages, so silent
history edits (such as vacuous-step folds) can no longer desync a
cached index.
* refactor(agent-core-v2): formalize injector once-channels and raw message results
* refactor(agent-core-v2): declare dynamic tool schemas at injection boundaries
Move the dynamic-tool schema declaration out of toolSelect.load(): the
loaded names are recorded as pending and drained by a dedicated
toolSelectSchemas provider through the contextInjector boundary
scheduler, so the declaration message lands at a quiescent boundary
instead of mid-step inside a streaming tool exchange. The folded
history remains the loaded-tool ledger, so undo, compaction, and
resume still self-heal by re-folding.
* refactor(agent-core-v2): deliver AGENTS.md reminders through the reminder queue
The tool hook now only observes and enqueues a once-per-agent reminder
through the reminderQueue once-channel instead of prepending text to
the tool result: results stay verbatim for the truncation pipeline and
the reminder can never be truncated away with an oversized output. The
reminderQueue is resolved lazily through the instantiation service at
enqueue time, breaking the contextInjector -> loop -> llmRequester ->
profile -> agentsMdReminder constructor cycle.
* refactor(agent-core-v2): make injection disclosures opaque and domain-owned
contextMemory no longer declares the ContextInjectionDisclosure union:
InjectionOrigin.disclosure becomes an opaque unknown, and providers
bind their own payload type through register<D>, so lastDisclosure
arrives at the provider already typed by its own variant. The date,
swarm_mode, and once_reminder payload shapes move into the dateChange,
swarm, and reminderQueue domains respectively; reminderQueue keeps a
runtime guard for its cross-message tail scan, the only place that
reads disclosures it did not write. Persisted origin shapes are
byte-identical, so existing journals replay unchanged.
* fix(agent-core-v2): isolate failing step context providers
A step or compaction boundary provider that threw or rejected made the
injector's inject() promise reject, which propagated through the
onWillBeginStep hook chain and failed the whole turn, and starved every
provider registered after it. Log and skip the bad provider instead,
matching the turn-start path's existing isolation.
* refactor(agent-core-v2): derive injector isNewTurn per injection boundary
Replace the shared read-and-clear isNewTurn flag (set by turn.started and
injectAfterCompaction, consumed by the first inject()) with values each
trigger supplies from an authoritative source: the loop marks a turn's
first step via BeforeStepContext.firstStepOfTurn (standalone runs never
count), and the compaction follow-up passes true explicitly, so
interleaved triggers can no longer consume or steal the marker.
A compaction follow-up that lands inside a step hook chain (the
auto-compaction path) doubles as that step's new-turn delivery: the
enclosing step then injects with isNewTurn false, so the upcoming request
receives one new-turn injection, not two.
* refactor(agent-core-v2): unify disclosure placement and injector param naming
* fix(agent-core-v2): keep pending tool schemas across compaction splices
A load announced by select_tools sits in pendingLoaded until the next
injection boundary declares it. A compaction fold in that window
publishes a replacement splice, and the splice-time reconciliation
dropped the pending entries before the post-compaction inject could
declare them — the model was told "Loaded: X" yet X never became
available. Drop pending entries only on removal splices (undo/clear,
which carry no replacement messages); compaction's replacement splice
keeps them so the declaration lands at the post-compaction boundary.
* fix(agent-core-v2): consume the plugin session-start refresh after a successful render
reconcileSessionStartReminder cleared the refresh-pending flag before
awaiting the render, so a throwing render (skipped by the injector's
provider isolation) lost the forced refresh until the next catalog
change. Consume the flag only after the render resolves, and move the
warn-once rationale into the module header per the comment convention.
* refactor(agent-core-v2): remove the generic reminder queue
* chore(agent-core-v2): drop the stale reminder-queue mention in systemReminder
* test(node-sdk): align side-question fork parity with event-point reminders
* chore(agent-core-v2): address reminder review standards
* docs(agent-core-v2): condense the model-facing reminders section
* refactor(agent-core-v2): write all system reminders through wrapSystemReminder
* fix(agent-core-v2): preserve reminder lifecycle invariants
* refactor(agent-core-v2): reconcile context injections at the step head
Unify the injector's delivery timings into one point on the
onWillBeginStep chain, before the step's request is built:
- providers run before every request instead of after every step, so
reminders are visible from the first response of a turn
- a compaction splice re-arms the new-turn flag via context.spliced;
when compaction runs inside the hook chain (full-compaction's
beforeStep), a follow-up inject at the chain tail keeps the first
post-compaction request covered
- registerAtTurnStart and injectAfterCompaction are removed;
reconcileWhenIdle stays as the v1-parity surface for SDK-driven
triggers (swarm toggle, plugin reload)
* refactor(agent-core-v2): clarify the injector's step-hook handler
Name the handler reconcileAroundStep, rename the rearm flag to
compactionRearmPending with a single takeCompactionRearm() consumer,
and extract isCompactionSplice. Consuming the flag into a local before
computing isNewTurn also avoids hiding the side effect inside a ||
short-circuit.
* feat(agent-core-v2): keep session updatedAt stable across meta management writes
Rename, archive/restore, and fork no longer bump a session's updatedAt,
so recency-sorted session lists stop reshuffling on management actions:
- setTitle/setArchived pass touchUpdatedAt: false; an explicit
patch.updatedAt always wins (fork inherits the source's recency, so a
fork lands next to the source instead of floating to the top)
- new SessionMeta.archivedAt records the archive moment (cleared on
restore) and is surfaced through the session index, the v1/v2 session
routes (archived_at), and the klient contract, so the archived list
keeps an accurate archive time without relying on the updatedAt bump
* fix(agent-core-v2): normalize a legacy ISO-string updatedAt when forking a cold session
A cold legacy/v1 state.json read from disk can still carry an ISO-string
updatedAt; passing it through as the fork's explicit patch.updatedAt
would persist a string into the v2 metadata. Normalize with toEpochMs
(falling back to now when absent/unparseable).
* fix(agent-core-v2): write fork metadata after agent recreation
Registering each copied agent during fork is an ordinary metadata write
that bumps updatedAt, which overwrote the inherited source recency and
still floated normal forks (sessions with agents) to the top. Move the
fork's metadata update after the agent recreation loop so the inherited
updatedAt is the final write.
* fix(agent-core-v2): preserve persisted recency when restoring a cold session
Resume creates the main agent for a cold session that has no persisted
agents.main entry (e.g. an empty session), and that registration bumps
updatedAt — so unarchiving an empty session still floated it to the
top. Capture the index summary's updatedAt before resume and re-apply
it in the restore write (archived:false, archivedAt cleared, explicit
updatedAt wins over the bump).
* fix(agent-core-v2): make agent registration non-touching for recency
Registering an agent is a structural write, not content activity — but
it went through an ordinary metadata update that bumped updatedAt. That
reordered recency-sorted listings whenever materialization created an
agent: resume of a cold session without a persisted agents.main (so
archive-via-resume and restore of empty sessions still floated), and
runtime subagent registration mid-turn. registerAgent now passes
touchUpdatedAt: false; restore goes back to the plain unarchive write
and no longer needs the capture/reapply workaround.
* fix(agent-core-v2): duplicate cron tasks only after the fork metadata is durable
With the metadata write moved after agent recreation, cron duplication
ran before it — a rejected metadata update left cloned cron records
pointing at a fork whose directory the catch block just removed. Keep
cron duplication after the durable metadata write.
* style(agent-core-v2): fold new invariants into module headers
The package convention keeps comments in the top-of-file block only —
move the touchUpdatedAt precedence, non-touching registration, and fork
ordering notes out of statement-level positions into the respective
module headers.
* chore: scope the changeset to agent-core-v2
* fix(kimi-code): show MCP launch targets in the workspace trust prompt
Render each gated project MCP server's launch target (transport, command,
args, cwd, or url) in the workspace trust prompt without leaking env or
header secrets, stripping terminal control characters from the
workspace-supplied text, default the prompt to "Don't trust", and
resolve fd binaries to absolute paths so untrusted workspaces cannot
plant a bare-name fd executable that runs before trust confirmation.
* fix(kimi-code): resolve stty to an absolute path before the trust gate
The decorator-registry fallback resolved every decorator name, including
kernel tokens like instantiationService — a call to
instantiationService/dispose would tear down the root container. Record
Feature.contributeService tokens in a contributed-service table and fall
back to that table only, so runtime-contributed services stay callable
while unregistered kernel tokens remain unreachable.
* feat(agent-core-v2): remove Agent and AgentSwarm from builtin profile tool lists
The builtin agent and coder profiles no longer expose the Agent and
AgentSwarm tools, so sessions on the v2 engine do not offer subagent
delegation by default. The tools themselves remain registered; profiles
that list them explicitly can still opt in.
* feat(agent-core): remove Agent and AgentSwarm from builtin profile tool lists
Align the v1 builtin agent/coder profiles with the v2 change: the
default profiles no longer offer subagent delegation, while the tools
stay registered for profiles that list them explicitly.
The parity projection drops v1's inactive Agent/AgentSwarm roster
entries: v1 reports registered-but-inactive builtin tools where v2 only
registers the tools a profile lists, so an inactive entry has no v2
counterpart. Active entries still compare in full.
* fix: keep Agent and AgentSwarm in the builtin agent profile
Scope the removal to the coder subagent profile on both engines: the
main agent keeps Agent/AgentSwarm so default sessions can still
delegate, while coder subagents no longer spawn nested subagents by
default. Snapshots and token counts shift only for the embedded coder
tool list; the v1 parity projection needs no change since the main
agent rosters match again.
* feat(kimi-code): paginate the session picker list
The /sessions picker and kimi -r used to materialize the full session
list before showing anything, which gets slow with hundreds of sessions.
- node-sdk: add listSessionsPage (limit/before -> items + nextCursor);
the v2 engine pages through the session index (draining past entries
whose workDir is unrecoverable), the v1 engine answers one full page
- TUI: open the picker on the first page, fetch the next page when the
cursor reaches the fetched end, and drain remaining pages in the
background once a search query is typed so search still covers all
sessions
- kimi -r now fetches a one-item page for the latest session
* chore: simplify session picker changeset
* fix(kimi-code): join in-flight page fetch in session search drain
A query typed while a scroll-triggered page fetch was still running
stopped the background drain at the loadingMore early return, leaving
the search covering only the pages fetched so far. fetchMoreSessions
now optionally joins the in-flight fetch and continues with the next
page; scroll triggers still drop when busy.
The v1 WS connection had no keepalive: by design it stayed open until the
client disconnected, which only holds for direct connections. Behind a
reverse proxy or gateway with an idle timeout (30s defaults are common),
any quiet stretch — e.g. waiting on a slow model response — got the
connection killed, surfacing as a recurring 'Realtime connection error'
in the web UI.
Send an application-level ping every 10s and advertise heartbeat_ms in
server_hello (the schema and all shipped clients already answer pong).
Application-level rather than protocol-level ping because browser JS
cannot observe the latter, and the client's stale-socket detector keys
on incoming message frames. Any inbound frame refreshes liveness; after
two silent cycles the connection is presumed half-open and closed with
1001 so dead peers get reaped instead of leaking.
* feat(kimi-code): show live background agent activity in the /tasks panel
Background agents (run_in_background or Ctrl+B) showed no run details:
the /tasks panel only had static metadata, and its output view stays
"[no output captured]" until completion because agent tasks capture
output only once at the end.
Tee child-agent events into a bounded in-memory per-agent activity
store segmented by the engine's own turn.step.started events (recent
10 steps, bounded text/output tails). The /tasks preview pane now
shows a live activity preview for agent tasks, and Enter/O opens a
full-screen detail view rendering step-grouped Markdown text and
per-tool results through the main transcript's renderers, with Ctrl+O
to expand. Agent tasks without an in-memory record (e.g. lost after
resume) fall back to the captured-output view.
* feat(kimi-code): retain 20 recent steps in the background agent activity view
* fix(kimi-code): cap the streaming-args buffer in the subagent activity store
* chore(kimi-code): simplify the background agent activity changeset
* fix(kimi-code): drop activity records of foreground-only subagents at terminal state
* fix(kimi-code): cap retained tool argument strings in the subagent activity store
* test(acp-server): retry temp-dir cleanup to deflake ENOTEMPTY on CI
* fix(kimi-code): tighten subagent activity store lifecycle edges
- drop delta-only arg buffers when their step is evicted
- keep records of spawn-time background agents even when the task sync lags
- mark records terminal on background.task.terminated for stopped agents
that never emit subagent.failed
* fix(kimi-code): release leftover arg buffers when an activity record turns terminal
* fix(kimi-code): prune foreground-only activity records when the main turn ends
* fix: surface a readable error when Git Bash is missing on Windows
* fix(agent-core-v2): translate probe rejection into HostProcessError for ready awaiters
- HostEnvironmentService.ready now rejects with the translated
HostProcessError(shell.git_bash_not_found) instead of the raw
ProbeShellNotFoundError, matching what sync field reads throw and what
SDKRpcClientV2.ensureConfigFile() surfaces, while an internal no-op
handler keeps the rejection from becoming an unhandledRejection.
- Replace the Windows-gated probe-failure tests with vi.mock-stubbed
deterministic suites that run identically on any platform.
- Move the ProbeShellNotFoundError explanation into the environmentProbe
file header per the package comment convention.
* fix(agent-core-v2): narrow probe error to Error to satisfy only-throw-error lint
* fix(agent-core-v2): preserve probe error as cause when translating to HostProcessError
* fix(agent-core-v2): keep checked paths out of the public probe error message
* fix(node-sdk): gate the host-environment wait in ensureConfigFile to Windows
The missing-Git-Bash failure is Windows-only, and IHostEnvironment.ready
also covers the login-shell PATH enrichment, which spawns the user's login
shell with a 5s timeout. Awaiting it on POSIX coupled config-only commands
(kimi provider list/remove, export, ...) to the user's shell profile for no
benefit.
---------
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
- name Emitters and surface their subscriptions as on:<name> ledger
labels through a named EventSubscription class and IDisposableDebugLabel
- add IDebugEventsService.subscriptions(), merging unit-book entries
with per-bus listener counts, contributed at App scope by the new
debugEvents feature
- kap-server debug dispatcher falls back to the global decorator
registry so runtime-contributed services stay callable
- kimi-inspect: add an Events panel to the DI view
- return the enqueue-launched turn instead of rejecting with
prompt.not_found when no prompt is pending at steer time
- report steer as queued when a manual compaction holds the context
- sync title/lastPrompt metadata on main-agent steer, matching v1
- update the v1-v2 parity test to assert converged behavior
- add onWillCreateSession to ISessionLifecycleService: a synchronous
participation event fired before a session's services activate, exposing
a session-domain facade (readSeed / contributeSeed / onSessionDispose)
- workspaceMcp subscribes and activates ephemeral-server overlays itself:
the configs travel as the new ISessionEphemeralMcpServers session seed,
the stdio cwd is read from ISessionContext, the merged ISessionMcpHandle
is contributed over the seed adapter's workspace projection, and the
overlay shutdown is attached to the session's teardown
- sessionLifecycle drops its IWorkspaceMcpService dependency, the overlay
tracking map, handle-dispose wrapping, and the dispose backstop
- rename ScopeOptions.extra to seeds and ScopeOptions.assemble to
configureContainer
- move session/btw to features/btw, mirroring the plan feature layout
- contribute ISessionBtwService at Session scope through BtwFeature
(contributeService) instead of a static registerScopedService call
- keep the package root exports unchanged; move the test to
test/features/btw
* feat(minidb): instrument open lifecycle with phase timings and status
Add MiniDb.lifecycleStatus() exposing the no-generation/generation-load/
wal-catch-up/full-rebuild/ready/degraded state machine plus per-phase
timings (generation candidate load, store/non-text/text image load,
postings integrity check, WAL scan/apply, full recovery, text rebuild
hosting), so snapshot load, WAL catch-up and full rebuild can be told
apart in diagnostics.
Also add a repeatable open-lifecycle bench (small data, large WAL delta,
large full-text generation, corrupt generation) and fixtures proving a
healthy generation open performs no full-corpus tokenization while a
corrupt or missing generation falls back. Log search-index and
query-store open diagnostics in kap-server and agent-core-v2 so a
listSessions call can be attributed to the database it touches.
No persistence format or product behavior change.
* feat(agent-core-v2): isolate the session index from the global search index
Harden the separation between the session read model and the full-text
search index so session operations never depend on search availability:
- Reject text index definitions in MiniDbQueryStore at definition level,
keeping the session query-store a structural-only read model with no
postings/tokenizer artifacts, and assert its generation carries no
full-text files.
- Share one authoritative scan between the first list and the initial
projection (single-flight) instead of scanning twice; reads may only
join an in-flight scan, and every fallback read folds the mirror's
pending queue so read-your-writes holds while preparing.
- Keep withReadModel() fallback semantics pinned by tests:
uninitialized/preparing reads hit authoritative metadata immediately,
ready reads use the read model, degraded keeps falling back with a
diagnosable status reason.
- Guard session metadata writes so a mirror failure degrades only the
read model and never fails the session lifecycle.
- Prove via tests that listSessions/--resume/--continue never open the
global search DB (including when search-index is unopenable), and that
only real full-text search requests report building/stale/degraded.
* perf(minidb): slice open-time work so it never blocks the main thread
Make the whole generation-open path cooperative:
- Replace the synchronous postings/store CRC verification with chunked
async variants (readGenerationFileCheckedAsync, verifyFileIntegrityAsync)
that keep the exact bytes/crc-mismatch error semantics.
- Give the WAL-delta apply a primitive-op + wall-clock budget
(walApplySlicer), so a batch frame unrolling into thousands of ops can
no longer run as one uninterruptible slice; torn-tail, corrupt-batch
and read-only behaviors are unchanged.
- Slice the big attach loops: Store.bulkLoadRefsAsync +
SkipList.bulkLoadAsync for the store image, async parsers and
loadImageAsync for secondary/compound images, and
TextIndex.attachImageAsync for the docs/dictionary map construction.
- Queue text builds on worker-slot pressure (WorkerSlots.acquireBounded,
bounded by MiniDb.textBuildSlotWaitMs, abort-aware) instead of falling
back to an unbounded inline build; a persisted drought hosts the
bounded inline core as the explicit last resort with stats accounting.
Bench (bench/open-lifecycle, seed 42): event-loop delay max across the
four open scenarios drops from 45/734/331/492 ms to ~12-28 ms with wall
time flat or better.
* feat(kap-server): run the global search index in a dedicated worker
Move the whole search-index MiniDb lifecycle (open, generation load,
WAL replay, sync, rebuild, compaction) off the main thread into a
long-lived worker_threads host, so it never shares the event loop with
TUI input:
- Add a versioned request/response protocol and worker entry hosting a
host-agnostic SearchIndexCore; the same core also backs an inline
backend kept as the explicit rollback
(KIMI_CODE_EXPERIMENTAL_SEARCH_WORKER=false, flag default ON).
- The worker exclusively owns the search-index handle. The lock token
is reported at acquire time (new MiniDb OpenOptions.onLockAcquired
hook) and reaped on dirty exit; an orphan-lock detector (same-pid
lock row whose token no live holder owns) recovers the window where
the token report is lost, so a mid-open crash can never freeze the
index into a silent permanent read-only.
- Crash handling: in-flight requests are rejected with typed errors,
respawn uses capped exponential backoff, per-request watchdogs
terminate wedged workers, and beginClose propagates into the worker
so dispose stays bounded during a long sync. Page tokens pin a
boot-salted generation, so tokens issued before a transparent worker
restart fail closed with invalid_page_token.
- The main process keeps the sync coordinator (debounce/coalescing/
single-flight), live transcript routing, query normalization and
page-token codec; searches keep reading the published generation and
report building/stale/degraded instead of waiting for sync/rebuild.
- Wire the worker into the CLI packaging: self-contained worker bundles
for npm dist and the SEA asset manifest/installer/smoke check, plus a
dev runtime (type-stripping + .ts resolve hook) scoped to worker
execArgv.
* feat(kap-server): model search and session-index lifecycles explicitly
Consolidate the two-index separation into explicit, diagnosable
lifecycles:
- Surface the global search state machine (stopped / opening / building
/ ready / degraded / closing) end to end: SearchIndexCore.lifecycleState,
SearchWorkerHost lifecycle snapshots cached from RPC responses (and
invalidated across worker generations), a never-throwing status()
carrying the lifecycle, and a synchronous lifecycleReport() that
neither kicks the open nor spawns the worker. Corrupt search-index
rebuilds are announced with a dedicated warn log so building, stale,
degraded, corrupt and worker-unavailable stay distinguishable.
- Turn MiniDb read-only replica catch-up fully cooperative:
catchUpWalAsync scans frames with the windowed async scanner and
yields per primitive op on the shared walApplySlicer budget, while a
per-instance catchUpChain serializes concurrent catch-ups so each
caller keeps its atomic watermark advance. The stale synchronous
implementations are removed.
- Pin the dependency direction and availability timing with tests:
session list/create/resume survive a corrupt or unopenable search
index (also end-to-end with a dead query-store), search generation
reuse and stale-serving keep working across restarts, concurrent cold
callers open the index / spawn the worker exactly once, resume-then-
fetchSessions performs no duplicate authoritative scan, and a clean
dispose releases the lock and settles at stopped.
- Document the experimental flag surface (persistence_minidb_readmodel,
search_worker) in the root guide.
* feat(agent-core-v2): default the session read model on and roll out the separation
Rollout and validation for the index separation plan:
- Flip persistence_minidb_readmodel to default ON (rollback via
KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL=false or the
experimental config section); session list/--resume/--continue now
always go through the isolated session read model with the
authoritative fallback. Test harnesses pin the flag off where shared
fixtures require hermetic homes, while the dedicated suites keep
explicit on/off coverage.
- Add a probe proving the main thread stays responsive while the
search worker rebuilds and swaps a generation (reindex), completing
the TUI responsiveness matrix.
- Record the rollout state in the agent-core-v2 guide (session index
section) and the root flag line.
- Add changesets for the CLI (worker isolation, session index
independence) and minidb (cooperative open lifecycle).
Validation: full suites green across minidb (551), agent-core-v2
(4760), kap-server (1005), node-sdk (343), klient (91) and the CLI app
(2567); open-lifecycle bench event-loop delay max is down from
45/734/331/492 ms to ~16-22 ms across the four scenarios with wall
time flat or better.
* fix(agent-core-v2): evict deleted sessions from the mirror queue and drain the index on close
Two issues surfaced by the read-model default in the acp-server suite:
- ISessionIndex.remove only deleted from the query store, but a summary
still queued in the mirror was folded back into reads (and re-written
by the next flush), resurrecting a deleted session in listings. The
mirror now exposes evict(id): drop the queued summary and wait out an
in-flight flush before the store delete.
- RunningAcpServer.close and SDKRpcClientV2.close disposed the engine
without awaiting the asynchronous mirror flush / query-store close,
so a host removing homeDir right after close() raced in-flight shard
closes (ENOTEMPTY). Both now follow the kap-server shutdown order:
drain the mirror while the store is open, dispose, then await the
drains.
* fix(minidb): pause active expiry during the sliced bulk load
The store's active-expire timer is armed at construction, so during a
sliced bulkLoadRefsAsync a tick can fire mid-load: it reaps a TTL key
from the map while the order skiplist is still the old empty one, and
the final bulkLoadAsync then rebuilds order from the stale orderEntries
snapshot — resurrecting the expired key in the ordered index (and
duplicating it if the key is later set again). The sync bulkLoadRefs had
no yield windows, so guard the async path with a bulkLoading flag that
defers expiry ticks until the load settles (finally-safe).
* chore: consolidate changesets into the TUI startup freeze fix
- tokensBefore/tokensAfter now include the system prompt and non-deferred
tool schemas, matching the measured-anchor basis the context gauge uses
between exchanges
- the post-compaction ledger rebase carries the same full-request size, so
the reported context size no longer dips to a messages-only estimate and
jumps back on the next exchange
- the PreCompact hook tokenCount uses the same basis
* fix(agent-core-v2): gate plugin changes behind session baselines and reminders
- capture a per-session MCP server baseline (ISessionMcpHandle.isBaselineServer)
so servers added mid-session (plugin install, mcp.json edit) never register
tools in live sessions; they take effect on /new, /reload, or resume, while
removed servers stay tombstoned and fail calls with a removal notice
- stop rebuilding the system prompt on plugin-source catalog changes: the
frozen skill listing and plugin sections cannot move anyway, and the rebuild
only churned the ${now} timestamp, invalidating the provider prompt cache
- freeze the Agent tool description's catalog profile list once the session
catalog has loaded, keeping the tools payload byte-stable across mutations
- append a plugin_change system reminder to live sessions on plugin mutations
(new IPluginService.onDidMutate; explicit reloadPlugins does not raise it)
- revert the TUI hint to "Run /new or /reload to apply plugin changes." and
update the plugin/MCP docs and changesets to the corrected contract
* fix(agent-core-v2): import LifecycleScope from app/scopes in sessionOutcomeMirror
#2666 imported LifecycleScope from #/_base/di/scope, which does not export
it (it lives in #/app/scopes), breaking the package build and typecheck on
main.
* fix(agent-core-v2): close the mutation-driven session-start refresh and overlay baseline leaks
Codex review on the PR found two contract leaks:
- a plugin mutation re-pulls the plugin skill source, and the existing
catalog listener answered with a fresh plugin_session_start reminder —
injecting the newly installed plugin's instructions into the live session
alongside (and contradicting) the plugin_change notice. The session-start
refresh now skips mutation-driven catalog changes (one per mutation,
counted; explicit reloads keep the old refresh behavior).
- a session created with ephemeral mcpServers kept its MCP baseline open
until the overlay connect finished; a workspace server added in that
window (plugin install, config edit) leaked into the live session through
the merged view. The overlay handle's baseline now freezes on the
workspace manager's initial load, with the ephemeral names baseline by
construction.
* fix(agent-core-v2): drop duplicate LifecycleScope import in sessionOutcomeMirror test
---------
Signed-off-by: Haozhe <yanghaozhe@moonshot.ai>
The L3 unit layer refactor moved LifecycleScope out of the _base DI kernel
into the app tier, leaving the session outcome mirror with a stale import
that broke typecheck and import-time evaluation on main.
* feat(agent-core-v2): persist the last turn outcome into session metadata for cold listings
A cold session (no live handle) reported no lastTurnReason, so after a
server restart the session list could not mark a session whose last turn
failed until it was opened and resumed.
A new Session-scope SessionOutcomeRecorder subscribes to the activity
aggregate's turn_ended changes and persists the outcome
(completed/failed) into the session metadata document; the summary
pipeline (mirror + cold reader) carries it as SessionSummary
.lastTurnReason, and toWireSession falls back to it when no live fact
exists. 'cancelled' is deliberately not persisted: it is also what an
in-flight turn ends with during scope disposal, and writing there races
the host's home-dir teardown.
Verified end to end with an isolated home and a dead provider: a turn
fails, the server restarts, and GET /sessions reports
last_turn_reason=failed without opening the session.
* fix(klient): carry lastTurnReason/lastTurnOutcome in the validated contracts
Review follow-up: zod strips unknown keys on parse, so the new outcome
fields never reached klient callers; add them to the session summary and
metadata/patch/key schemas (contract parity test covers the engine
mirror).
* fix(agent-core-v2): persist user-cancelled outcomes, never teardown aborts
Review follow-up: skipping every 'cancelled' left a stale earlier outcome
in the metadata (e.g. a prior failed reported for a session whose latest
turn was stopped by the user). The recorder now subscribes to the main
agent's turn.ended facts directly and keys on interruptReason:
user_cancelled is persisted like any other terminal state, while
programmatic aborts — including the cancel every in-flight turn suffers
during scope disposal — are never written, so no metadata write races the
host's home-dir teardown.
* fix(kap-server): only fall back to the persisted outcome for cold sessions
Review follow-up: a warm session that just started a new turn clears its
live lastTurn, and the unconditional ?? fallback would then report the
previous turn's persisted outcome for a turn that is still running.
SessionFacts now reports whether a live handle exists, and the wire
projection only reads the persisted value when the session is cold.
* docs(agent-core-v2): keep the outcome-recorder header at role level
* fix(agent-core-v2): settle turn outcomes on turn start and drain metadata writes on close
Review follow-ups:
- a new main turn now clears the persisted outcome (turn.started), so a
process that dies mid-retry no longer reports the previous turn's
terminal state for a turn that never ended
- the dedupe marker only advances after a successful write, so a failed
persist no longer suppresses the next identical outcome
- session metadata writes are tracked in a module-level pending set with
drainSessionMetadataWrites(), awaited by kap-server close alongside the
mirror/query-store drains — an event-driven write (e.g. the outcome
recorder) can no longer land in a session dir while the host removes it
* fix(agent-core-v2): track the metadata dispose flag locally
Disposable exposes no public isDisposed accessor; keep a class-local flag
set in the dispose override.
* fix(kap-server): drain session metadata writes before the mirror and disposal
A write still in flight when close() begins must settle before the mirror
flushes its summary into the read model and before scope disposal marks
the service disposed — not after.
* fix(agent-core-v2): reattach the recorder when the main agent is recreated
Review follow-up: a failed bootstrap still fires onDidCreate before the
handle is dropped; the subscription then pointed at a dead bus and the
guard blocked any later reattach. Track onDidDispose and reset so the
next main creation attaches cleanly.
* test(agent-core-v2): resolve the recorder through the scoped DI harness
Review follow-up: construct SessionOutcomeRecorder via registerScopedService
+ a Session-scope test host (stubbed lifecycle/metadata), so the test covers
the production registration path; add the durable-value adoption case.
* fix(agent-core-v2): unbreak CI — iterable Promise.all and the debug channel surface
- Promise.all takes the pending-writes set directly (oxlint error)
- the disposed flag moves into a _register'd marker instead of a public
dispose() override, which the debug channels listing (and its test)
correctly rejects as framework plumbing
* fix(kap-server): surface persisted failures on the v2 session status
The v2 list folds the outcome into activity.status, which previously
read only live facts — a cold session always looked idle. Cold sessions
now map a persisted failed outcome to status 'failed' (completed and
cancelled stay idle, matching the live fold); warm sessions are
unchanged, and the statuses filter inherits the mapping.
* refactor(agent-core-v2): name the persisted field lastTurnReason
Aligns with the established name for the same concept end to end
(activity view's lastTurnReason, the v1 wire's last_turn_reason, and the
SessionSummary mirror), instead of introducing a third variant.
* fix(agent-core-v2): drain pending metadata writes before session teardown
Review follow-up: closing/archiving a session right after a turn ended
could dispose the scope while the outcome write was still queued, and
delete() removes the session dir immediately after close. Await the
pending metadata writes before the handle goes away.
* fix(node-sdk): carry lastTurnReason through the SDK session summary
Review follow-up: the in-process SDK path maps the engine summary through
v2SummaryToSessionSummary, which dropped the new outcome field. Add it to
the public SessionSummary type and the mapper; the parity gate projects
it away (the v1 engine never records an outcome).
* fix(node-sdk): populate lastTurnReason on live SDK summaries
Review follow-up: resumeSession/reloadSession build their summary from the
live session's metadata document, which now carries the outcome — surface
it there too so the SDK reports it consistently for live and listed
sessions.
* fix(agent-core-v2): carry the last turn outcome across session forks
Review follow-up: fork skips state.json when copying the session dir, so
the fork's fresh metadata never had the outcome and a restart dropped a
marker the warm fork was still reporting. The fork's metadata patch now
inherits the source's lastTurnReason.
* fix(agent-core-v2): settle pending outcome writes before reading a fork source
Review follow-up: a fork requested right after the source's turn ended
could read the metadata before the recorder's queued write landed,
inheriting a stale or absent outcome. Drain pending metadata writes
first.
* fix(agent-core-v2): backfill restored outcomes into the session metadata
Review follow-up: for sessions whose last turn ended before this field
existed, the cold-resume seed restores the outcome into the activity view
without a turn.ended fact, so the recorder never persisted it and cold
listings stayed blank. The recorder now also watches the main agent's
activity updates and backfills the restored outcome when nothing is
persisted yet.
* fix(agent-core-v2): never backfill restored cancellations
Review follow-up: a restored 'cancelled' cannot be told apart from a
programmatic abort (the activity event carries no interruptReason), and
those are never persisted. Backfill now covers only completed/failed;
user stops are still persisted from the live turn.ended fact.
* refactor(agent-core-v2): rename the outcome recorder to outcome mirror
Mirror is the codebase's established term for a write side that reflects
live state into a store (SessionIndexMirror); Recorder has no precedent.
* fix(agent-core-v2): backfill without bumping recency; header-only comments
Review follow-ups:
- a mere resume must not float an old session to the top of the list:
metadata updates accept touchUpdatedAt:false and the outcome mirror's
backfill uses it (live outcome writes keep bumping — turn end is a
recency moment)
- the mirror service's inline notes move into the file header per the
package comment convention
- drop the redundant |undefined from the SDK's optional outcome field
* fix(node-sdk): read the live outcome for resumed session summaries
Review follow-up: on a fresh resume the restored outcome can still be
queued as a metadata backfill, so the document may lag a tick; the live
activity aggregate already holds it. Resume/reload summaries now prefer
the live value and fall back to the metadata field.
* fix(agent-core-v2): confine the outcome backfill to pure resumes
Review follow-up: the view publishes its turn.ended fold before this
mirror's own turn.ended handler runs, so a live ending reached the
backfill branch first and got persisted without the recency bump. The
backfill now only applies when no turn ever started in this process —
live endings always take the bumped write.
* fix(agent-core-v2): drain the session-index mirror before session teardown
Review follow-up: settling the metadata write alone left the fresh
summary in the mirror's pending queue, so a list right after close could
read a stale outcome from the read model. close/archive now also drain
ISessionIndexMirror. Test harnesses register a mirror stub for the new
dependency.
* docs(agent-core-v2): fold the metadata drain contract into the file header
* chore: include the SDK package in the changeset; fold the drain note into the header
* fix(agent-core-v2): backfill restored cancellations too, quietly
Review follow-ups: dropping every restored cancel loses legitimate user
stops whose live write never landed (or was rejected) before a restart —
cold surfaces never mark cancelled anyway, so healing them is harmless
and strictly more accurate. The metadata disposal note moves into the
file header per the comment convention.
* fix(node-sdk): prefer the live outcome over the index in SDK listings
Review follow-up: a live session that just started a new turn after a
failure can briefly keep the stale outcome in the index while the
mirror's clear is queued. listSessions now reads the live activity
aggregate for warm sessions, matching the kap-server cold-only fallback.
* fix(node-sdk): never read the metadata outcome for a live session
Review follow-up: with a retry in flight the live aggregate has no
outcome while the document may still hold the previous failure — the
fallback showed the stale one. Live summaries now take the live
aggregate's answer alone; the restored outcome is already seeded there
on resume.
* feat(mcp): tombstone removed MCP servers and apply plugin changes immediately (20 files)
- add 'removed' MCP server status: workspace config removals call markRemoved
instead of remove, keeping tool registrations alive while short-circuiting
calls with a removal notice
- fire onDidReload after every plugin mutation (install/enable/disable/remove)
so workspace consumers refresh contributions immediately
- TUI renders the removed status in the MCP panel/startup summary and shows an
apply-immediately hint on the v2 engine
* feat(agent-core-v2): freeze plugin prompt inputs for live agents (2 files)
- snapshot the model skill listing and plugin system-prompt sections on the
first successful prompt build and reuse the frozen values for the agent's
lifetime, so plugin install / enable / disable / remove / reload never
rewrites a live agent's prompt (same keep-live-sessions-stable philosophy
as the MCP tombstone)
- freeze only on success: a not-yet-ready skill catalog or a failed
enabledSystemPrompts() read must not pin empty values for the agent's
lifetime
- refreshSystemPrompt still rebuilds on catalog change events but reuses
the frozen values, so the prompt only moves when non-plugin inputs change
(AGENTS.md, [tools] section, session tool policy, compaction); new agents
snapshot the then-current state
* chore(changeset): add changesets for MCP tombstone and frozen plugin prompt inputs
* docs: describe immediate plugin changes and the removed MCP status on the v2 engine
* fix(klient): mirror the removed MCP server status in the wire contract
* docs: drop the legacy-engine behavior notes from the plugin and MCP pages
* fix(agent-core-v2): freeze plugin sections only on a loaded snapshot
- enabledSystemPrompts() resolves to its consumption fallback (never
rejects) while the initial plugin load has failed; freezing that empty
read locked plugin sections out of the live agent even after a later
successful reload
- expose hasLoadedSnapshot() on IPluginService so resolvePluginSections
can tell a real empty snapshot from the fallback before freezing
* feat(kap-server): accept attachments on skill activation
The :activate endpoint only took {args?}, so REST clients (web/desktop
composers) could not attach uploads to a /skill invocation — attachments
were silently dropped at the edge.
- activateSkillRequestSchema gains an optional attachments field carrying
the image/video/file subset of the prompt content wire shape.
- The skills route resolves them through the same edge pipeline as prompt
submissions (validate file refs → materialize/compress → convert),
extracted from routes/prompts.ts into lib/promptMedia.ts.
- AgentSkillService.activate appends the resolved parts after the rendered
skill prompt in the activation's user message; SkillActivationInput
gains an optional content field. The native RPC/TUI path is unchanged.
- Attachment failures map to 40407 file.not_found / 40001
validation.failed, mirroring the prompts route.
* fix(kap-server): drop the unused parseKimiFileUrl import in promptMedia
* refactor: address review — header-only comments in the skill domain, provider id on protocol URL sources
- agent-core-v2 keeps comments solely in the top-of-file block (scoped
guide): SkillActivationInput.content documented in the skill.ts header,
the activate() note folded into the skillService.ts header.
- packages/protocol's image/video URL source gains the optional
provider-issued id, matching the kap-server wire schema so parsing the
public contract no longer strips it.
* fix(kap-server): validate the skill before materializing activation attachments
An unknown or non-user-activatable skill name with attachments ran the
media pipeline first, streaming bytes into the session/cache dirs and
compressing images for a request that activate() would reject with
40415/40912. The route now checks the session catalog up front (the
service still re-validates) so invalid activations leave no disk or CPU
side effects.
- align the REST status rollup with the WS push: a bound alias that no
longer resolves omits max_context_tokens instead of reporting 0 (0 is the
engine's UNKNOWN_CAPABILITY marker, not a real limit)
- fall back to the default model's limit only when no model is bound,
resolved through IModelService like the WS side
- mark max_context_tokens optional in the shared session status schema
* feat(agent-core-v2): add the L3 unit layer and the Feature seam
- introduce the L3 Service/Fiber unit layer: the Service base class with this.provide/effect/on/get/ref capabilities, the fiber runtime with thenable FiberHandles, collection contribution points, and the per-scope-kind ScopeUnits materialization fold
- provide each scope's static registration batch as one atomic provideAll cascade transaction (waiting-area activation, sticky Failed on construction error)
- add the DI unit inspection surface: App-scope debug ledger / dependency graph / cascade history services and the kimi-inspect DI view
- add the Feature unit seam (IFeatureManager + feature assembly), port plan mode onto it, and add the contributed-command seam (agent-command domain + node-sdk RPC types)
- remove the legacy dep-graph tooling
- apply the header-only comment convention across src and test: strip non-header narration, keep the file header, tooling pragmas, and NOTE comments
* feat(kap-server): gate the event.di.* debug feed to kimi-inspect connections
- add an opt-in target set in SessionEventBroadcaster; the global fan-out
now skips event.di.* frames for connections that never opted in, so
kimi-web and other clients no longer receive the high-churn DI feed
- WsConnectionV1 opts a connection in when client_hello carries
client_id 'kimi-inspect'; removeGlobalTarget drops the opt-in on close
- temporary gate until a client-declared event-type whitelist lands
* chore(agent-core-v2): fix oxlint errors in the DI unit layer
- build the live-ref container chain without aliasing this (no-this-alias)
- snapshot the materialized map with Array.from and document why the copy
is required (no-useless-spread)
* test(klient): use string scope kinds in the lifecycle handle fakes
The engine's LifecycleScope is a string enum now; the facade test doubles
still returned the old numeric kinds and failed the handleWireSchema output
validation.
* build(nix): update the pnpmDeps fetch hash
* fix(kimi-code): select compatible PowerShell for Computer Use
* fix(kimi-code): handle locked Computer Use plugin files
* fix(kimi-code): align Windows Computer Use name
* fix(agent-core-v2): reuse PowerShell fallback for detection
* fix(agent-core-v2): refresh ready Computer Use plugin
* feat: surface the bound model on subagent UIs
The subagent.spawned event now carries the display-normalized model alias
(the derived __secondary__ entry resolves to its base alias), so clients can
show which model a subagent is bound to. The TUI subagent card, swarm panel
header, and background-agent entry show it at spawn; the WS snapshot roster
and REST /tasks (background/detached subagents) carry it too, keeping the
model visible across client reconnects.
* feat: carry the subagent thinking effort alongside the model
The spawned event, snapshot roster, and REST /tasks now also carry the
child's effective thinking effort (read from the child profile at spawn, the
same vocabulary as agent.status.updated). UIs show it only when it diverges
from the main session's current effort — an inherited level adds no
information, and 'off' is never shown.
* feat(tui): show the bound model and effort in the /tasks browser
The task browser's Detail pane renders Model and Effort rows for agent
tasks (raw alias and level — it is the inspector surface, so no diff
filtering), and its minimum height grows to fit the new rows. The values
were already persisted on SubagentTaskInfo; the TaskInfo union, its zod
schemas (protocol, kap-server, klient contract), and the v1 type
declaration now carry them so nothing strips them in transit.
* feat(tui): show concrete subagent effort levels unconditionally
Display rule simplified: any concrete effort tier (low/high/max/…) is
shown next to the model — including when it matches the main session's
level. Only the boolean states stay hidden: 'off' (no thinking) and 'on'
(generic thinking) carry no level information.
* docs: trim the changeset entry
* fix(tui): keep the model and effort on background-agent entries across resume
replayBackgroundProjection only copied agentId/parentToolCallId/
description, so a background subagent that outlived a resume lost its
model/effort on the later terminal transcript entry. The projection now
threads the persisted values (catalog-mapped model; boolean effort states
dropped), and session replay passes the loaded model catalog through.
* fix(agent-core-v2): normalize the derived secondary alias regardless of the flag
A child bound while the secondary-model experiment was on keeps
__secondary__ in its persisted binding; if the flag is later switched off
with the recipe still configured, resolveSecondaryModel() gated the
normalization and the sentinel leaked back onto resumed subagents.
subagentDisplayModel now reads the recipe straight from config (the flag
gates new bindings, not the interpretation of existing ones), which also
drops SessionSwarmService's now-unused IFlagService dependency. Also adds
the SDK package to the release: the new SubagentSpawnedEvent/AgentTaskInfo
fields are SDK-visible types.
* fix(agent-core-v2): normalize the status-frame model at the source
A derived-bound child republishes agent.status.updated right after spawn
with its raw modelAlias, which overwrote the spawned event's normalized
display model on single-subagent cards (swarm headers were first-wins and
escaped). emitStatusUpdated now maps through subagentDisplayModel, a no-op
for the never-derived main agent. Also moves the inline comments added by
this branch into top-of-file headers per the v2 comment convention.
* fix(tui): clamp the /tasks detail frame to the available body
At terminals near the minimum height the forced 10-row detail frame
overflowed the body and truncated the preview frame's border. The detail
height now caps out at whatever leaves the preview its borders plus one
content row, with a regression test at exactly MIN_HEIGHT.
* fix: normalize inherited derived aliases and keep model/effort on replayed terminal entries
- resolveSubagentBinding's caller-fallback branch also maps through
subagentDisplayModel: a caller itself bound to the derived entry (a
resumed subagent making a nested Agent call) no longer publishes
__secondary__.
- The replayed background-task terminal notification builds its metadata
with the persisted model (catalog-mapped) and concrete effort, matching
the live completion path.
- Drops the inline comments this branch added inside v2 test bodies; the
scenario context lives in the source file headers.
The 10-year default print wait ceiling (315360000s) overflowed Node's
setTimeout limit (2^31-1 ms) into a 1ms fire, so the steer/drain wait
returned instantly and kimi -p exited right after the main turn, killing
pending background tasks and subagents.
- add setClampedTimeout in agent-core-v2 _base, clamping delays to
MAX_TIMER_DELAY_MS, and route every config-driven timer through it
(timeoutOutcome, task wait/manager timeout, swarm attempt timeout)
- chunk the print turn-endings wait against the real deadline instead of
returning null on the first clamped timer fire
- restore v1 semantics: a non-positive swarm subagent timeout is unbounded
- default print_wait_ceiling_s to 2147483s (~24.8 days, the timer maximum)
- detect UTF-16 LE/BE from a BOM or a zero-byte parity heuristic
(tolerant of CJK content), derived from VS Code's encoding detection
- Read tool and workspace fs.read transcode UTF-16 text to UTF-8
instead of refusing it as binary; larger than 10 MiB still refused
- refuse other non-UTF encodings (e.g. GBK) with a clearer message
* fix(agent-core-v2): seed the activity view's lastTurn from the persisted turn.ended record
A cold-resumed agent seeded its activity view only from live loop/task
state, so the last turn's outcome was lost on a server restart: sessions
came back with no lastTurnReason, and clients could not surface a
previously failed turn (e.g. a provider 429 that killed the turn before
the restart).
The loop already persists the terminal turn.ended record (reason, error,
durationMs); fold the latest one into the TurnModel as lastEnded and have
AgentActivityView.seedFromLoop adopt it when no turn is active, so the
session work aggregate (and everything built on it) reflects the last
turn's outcome again after a cold start.
* fix(agent-core-v2): seed lastTurn on wire restore and add a changeset
Review follow-up: the agent scope (and with it this view) is constructed
before wire.restore() replays the journal, so a constructor-time read of
TurnModel.lastEnded always saw the initial state on a cold resume. Move
the wire-backed seed behind the onDidRestore hook (constructor seed kept
for views built after a restore), and drop the inline comments in favor
of the file header per the package comment convention.
* docs(agent-core-v2): trim the activityView header to role and collaborators
Review follow-up: the previous revision narrated the restore-hook
mechanics in the header; the package convention keeps headers at the
module's external role plus collaborators, so drop the implementation
narrative.
* fix(agent-core-v2): keep TurnModel.lastEnded across clock advances
Review follow-up: advanceTurnClock built a fresh state object without
spreading, so a new prompt or a queued cancel silently dropped the stored
last-ended outcome even though no new turn had ended — after a restart the
activity view would again find nothing to seed. Spread the prior state and
cover the prompt/queued-cancel/replace cycle with a model-level test.
* fix(agent-core-v2): clear the stored turn outcome once a newer turn starts
Review follow-up: with the clock advances preserving lastEnded, a prompt
persisted without its turn ever starting would leave the previous turn's
outcome to be seeded after a restart, reporting a stale result for a turn
that never ended. The loop-event fold now drops lastEnded as soon as a
newer turn's events land, while prompts and queued cancels keep it.
* docs(agent-core-v2): keep the turnOps header at the domain role
Review follow-up: the lastEnded keep/clear mechanics read as
implementation narrative in the header; the convention there is role and
collaborators only.
explorer.exe parses its raw command line rather than argv, so Node's
default spawn quoting breaks the `/select,` argument whenever the path
contains spaces: the command line becomes `"/select,\"C:\...\""`, which
explorer rejects, silently opening the Documents folder instead of
selecting the file. Quote only the path portion and launch with
windowsVerbatimArguments so the command line keeps the documented
`/select,"C:\some dir\f.txt"` form.
- return the domain-grouped page payload inside { code, msg, data,
request_id } and carry business outcomes in code (40001 invalid
params with details, 40922 page_token mismatch) instead of raw HTTP
statuses plus an { error: { code, message } } body
- add ErrorCode.PAGE_TOKEN_MISMATCH (40922)
- register the route via defineRoute (shared runtime validation and
envelope-wrapped OpenAPI docs); fold include-domain validation into
the query schema and replace the preprocess/doc-twin pair with
scalar-or-array union params
- update the kimi-inspect client to unwrap the envelope and sync the
two AGENTS.md guides
- kap-server: add GET /api/v2/sessions with a domain-grouped response
(workspace / meta / activity, opt-in git), status / archived /
updated_after filters, three sort orders, and fingerprint-bound opaque
cursor pagination
- kimi-inspect: rebuild the chat sidebar as a spreadsheet-like session
table on the v2 endpoint — preset views (All / Opened / Archived /
By workspace / Git), column visibility config, header sort toggles,
cursor-paged Load more, and localStorage-persisted panel prefs
- live activity frames from the WS hub override the REST status badge;
session created / meta-updated events invalidate the v2-sessions query
* fix(cli): show built-in capabilities before the first session exists
The lazy-session refactor left capability calls going through
requireSession(), so on a session-less v2 startup /plugins reported the
capabilities unavailable and hid the built-in rows behind the promo.
Like plugin management, capability readiness and installs are app-global
on the v2 engine: the node-sdk harness gains a capability facade over
the global channel, and the TUI resolves session-or-harness for every
capability call.
* fix(cli): count the dev marketplace server as the default catalog
dev.mjs always points KIMI_CODE_PLUGIN_MARKETPLACE_URL at its own
repo-serving server, which the override gate mistook for a user-configured
marketplace and suppressed the built-in capability rows in every dev run.
The dev server now marks itself, and the gate treats that marked URL as
the default catalog while still honoring real overrides (slash-command
source, user-set env, KIMI_CODE_DEV_MARKETPLACE_URL).
* fix(cli): align built-in capability updates