* fix(agent-core-v2): guard config persistence against lossy writes
- A failed load no longer clears the in-memory snapshot: the service keeps
the last-known-good config, reports an error diagnostic, and taints.
set/replace/replaceSections on the persisted layer then fail fast with
Error2(config.persist_blocked) instead of erasing the file; memory-layer
overrides stay available, and a successful reload clears the taint.
- persistDomains is now read-modify-write: the file is re-read and only the
domains being written are applied on top of current disk content, so
external edits are merged instead of clobbered, and an external delete is
honored instead of resurrected.
- External changes absorbed at persist time trigger a full reload so change
events fire for domains the writer did not touch.
* fix(agent-core-v2): rebase set() merges onto re-read config state
set(domain, patch) now merges the patch against the freshly re-read file
content and refreshes the in-memory snapshot from the same read, so external
edits to the same section survive a concurrent write instead of being
overwritten by the stale in-memory copy.
* fix(protocol): register config.persist_blocked in KimiErrorCode
Add the new code to the KimiErrorCode union and kimiErrorCodeSchema so the
persist-refusal error payload passes protocol validation across RPC
boundaries.
* fix(agent-core-v2): compute every config write against the re-read file
Move strip/merge/validate for set/replace/replaceSections into the persist
rebase callback so each write is derived from the file content re-read at
persist time. Overlay strip handlers (e.g. the KIMI_MODEL_* mask restoring
default_model) now read the fresh snapshot instead of the stale in-memory
one, and the unconditional snapshot sync makes the separate
absorbed-external reload redundant.
* fix(agent-core-v2): build defaults when the initial config load fails
A failed first load has no last-known-good state worth preserving, so fall
through with an empty document: registered section defaults are still
validated and applied (consumers of defaulted sections keep working), while
the taint keeps blocking persisted writes until a reload succeeds. Only
reload failures preserve the previous in-memory state.
* fix(agent-core-v2): stage re-read config snapshots until the write succeeds
Build the rebased raw/rawSnake snapshots in locals and publish them only
after the rebase and documentStore.set both succeed, so a validation error
or a storage failure cannot leave userValue and effective pointing at
different snapshots. stripEnv now takes the staged snapshots explicitly.
GET /api/v2/sessions gains view=by_workspace: one request returns every
workspace with a matching session, each carrying its first group.page_size
sessions under the requested sort plus the workspace's full matching total,
with group-level page_token pagination (40922 on condition drift). Groups
key on the alias-canonical workspace id, so legacy split buckets of one
physical directory merge into a single group, matching the v1 alias
semantics. meta.has_prompt filters sessions by prompt presence (the v1
exclude_empty equivalent) in both views. The flat view and v1 routes stay
byte-compatible.
The global WS stream now fans out event.session.archived (live and cold
paths; payload carries the session id and workspace_id) and
event.workspace.created/updated/deleted, published by the core
IWorkspaceService on every mutation path including the implicit
createOrTouch on session creation.
kimi-inspect consumes the grouped projection as a single-column
workspace/session tree in the chat view; the session pane merges into the
right dock as the Session tab. The server API reference (en + zh) documents
the new parameters, the grouped response, and the new events.
* feat(agent-core-v2): rework the title generation excerpts
- Rebalance the excerpt budgets toward the user's prompts (400 chars
each) and trim the assistant segments (300) so titles follow the
user's task instead of narrating the assistant's reply.
- Cap each prompt in the default user_prompts excerpt so one long
paste no longer starves the remaining prompts.
- Compose the digest excerpt from the full conversation arc: every
natural-language user prompt in the live window paired with its own
turn's final assistant text, interleaved chronologically, with
per-segment caps and a 3000-char total budget (middle turns elided).
* chore: scope the title changeset to agent-core-v2
* fix(agent-core-v2): dedupe digest prompts and elide whole turns
- Drop the redundant `| undefined` from the optional
TitleDigestTurn.assistant per the monorepo optional-property
convention.
- Deduplicate user messages by id when constructing digest turns, so a
prompt already in the context and still active in the queue does not
produce two turns.
- Elide the over-budget digest at whole-turn granularity, keeping each
assistant line paired with its own user line.
* docs(agent-core-v2): describe the full-arc digest in the SessionTitleSource contract
* fix: fail fast on provider-filtered empty responses
An APIEmptyResponseError carrying finishReason 'filtered' (OpenAI
content_filter, Anthropic refusal) is deterministic: replaying the same
request re-triggers the provider safety filter. Both isRetryableGenerateError
implementations (kosong, agent-core-v2) treated every empty response as
retryable, so step retry replayed the doomed request the full 10 attempts
before the filter notice surfaced. Return non-retryable for filtered empty
responses in both engines; the error already carries the provider.filtered
code, so the turn fails immediately with the existing filter notice.
* fix: skip the compaction shrink-retry for filtered empty responses
Both full-compaction loops routed every APIEmptyResponseError into the
shrink-and-continue branch before isRetryableGenerateError was consulted,
so a filtered response was retried with shrinking input instead of failing
fast. Exclude finishReason 'filtered' from the shrink branch in both
engines; it now falls through to the retryability check and throws
immediately. Add end-to-end tests (real kosong generate over a filtered
think-only stream) asserting a single attempt with the history untouched.
---------
Co-authored-by: kimi-agent-bot <kimi-agent-bot@users.noreply.github.com>
* 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 a037b1143e.
* 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
- give the builtin agent profile an explicit subagents allowlist (coder, explore, plan), restoring v1 semantics
- inherit the default profile's allowlist when a caller profile declares none, instead of leaving delegation unrestricted
- pass a lone "*" subagents field through as an explicit unrestricted marker
* fix(kap-server): serve real session usage in snapshot and persist per-turn context readings
* fix(kap-server): omit unknown session usage fields instead of reporting zero
* refactor(agent-core-v2): persist cron tasks as durable wire records
- write CronAdd/CronDelete/CronCursor as durable wire records and rebuild the cron task table from dispatcher replay
- migrate legacy per-workspace cron JSON files into the wire on first resume, then drop the file-based persistence service, its registrations, and the bootstrap cron scope
- derive the session cron view from the agent replayable cron state and remove the redundant session-level copy
- let session forks inherit cron tasks through the copied wire instead of duplicating task files
* fix(agent-core-v2): keep legacy cron tasks on cold forks and flush before cleanup
- inherit legacy cron task files into a full fork's wire so cold sessions forked before their first post-upgrade resume do not silently lose scheduled tasks
- flush the migrated wire records before deleting legacy files so a crash cannot lose both copies
* refactor(agent-core-v2): drop the legacy cron file migration
- stop reading legacy per-workspace cron JSON files entirely; pre-upgrade tasks simply stop applying instead of being migrated into the wire
- remove the legacy read path, the fork-time legacy inheritance, and the now-unused session context/document store injections
* refactor(kap-server): table-driven dispatch for multi-action routes
- add a shared action-dispatch helper; sessions, prompts, plugins, questions, and modelCatalog collection routes declare action tables with module-level handlers
- add ISessionManager.status returning the session summary; the archive action checks it instead of resuming the session
- archive cold sessions through the persisted-metadata path shared with batch archive
* fix(agent-core-v2): read-your-writes for session index point gets
Overlay pending mirror summaries in getFromReadModel so a freshly recorded
summary (e.g. cold-session archive) is visible to GET immediately, matching
the existing pending overlays in list and cursor resolution.
* feat(agent-core-v2): carry prompt attachments on turn.started so the live transcript projects them
* fix(transcript): clear the transcript goal when the goal is cleared
* fix(agent-core-v2): count a prompt media part as a transcript attachment only when its id matches its daemon file URL
* test(kap-server): expect the session-media file id on converted prompt parts
Move the external hook services out of app/externalHooksRunner,
session/externalHooks, and agent/externalHooks into
features/externalHooks, assembled as the ExternalHooksFeature unit:
- services live under per-scope subdirectories (app/, session/, agent/);
shared pure helpers (types, hook matching/dispatch, process spawn,
prompt result rendering) live under internal/
- the runner and the two observers are contributed through the Feature
seams (ScopeUnits materialization); the hooks config section stays on
the static import=register channel
- update the package entry leaf exports, the plugin domain imports, the
kap-server events-zod import, and the affected tests; regenerate the
state manifest
* fix(agent-core-v2): emit subagent.spawned after task registration
The spawned signal previously fired at launch, before the run's task
registration, so clients learned the agent id with no task id to bind
cancel/status actions to; a failed registration also left a spawned row
behind for a run that never registered. Emit it only after registerTask
succeeds and carry the task id on the event.
* fix(agent-core-v2): keep spawned ahead of started for Agent-tool runs
The TUI drops subagent.started until spawned has established the row,
and a failed registration must not leave a started row behind with no
terminal event. Defer the mirrored started dispatch so the Agent tool
can emit it itself after registration and spawned.
* fix(agent-core-v2): void the deferred started dispatch
* fix(kap-server): key Agent-tool transcript rows by the registered task id
Transcript-protocol clients suppress the raw task.*/subagent.* session
events, so they only saw a subagent row keyed by agent id that cannot
address /tasks/{id}, plus a second row once task.started landed. Key the
spawned row by the task id it now carries, fold task.started and the
subagent lifecycle back into it, and keep the agent-id path for spawns
without a registration (swarm/session-init/tower). Statement-level
ordering notes move to the file headers per package convention.
* test(agent-core-v2): split the spawned/started ordering contract into its own test
* fix(kap-server): keep subagent result details across task termination and drop stale task mappings on taskless respawns
* fix(kap-server): recover the agent-to-task association from a backfilled task.started
* fix(kap-server): seed pre-attach Agent task mappings on the transcript binding
* fix(kap-server): seed the full in-flight task row on transcript bind, not only its id
* docs(agent-core-v2): name the state-domain event dispatcher in the Agent tool header
* style(kap-server): drop comments in transcript services per the no-comments lint rule
* feat(kimi-code): specialize the WaitFor tool's transcript display
* feat(agent-core-v2): emit status progress while WaitFor is pending
* fix(kimi-code): route WaitFor dimming through the TUI theme
* feat(kimi-code): support replaceable status updates in tool progress
* fix(kimi-code): forward status progress to subagent activity surfaces
* fix(agent-core-v2): drop the redundant undefined from ToolUpdate.replace
* fix(kimi-code): honor replace semantics in the subagent live status path
* test(agent-core-v2): drive the WaitFor progress test through a manual tick
* fix(kap-server): mirror ToolUpdate.replace in the ws event schema
* refactor(agent-core-v2): expose the WaitFor progress scheduler as a public seam
* fix(kimi-code): pass child wait statuses without the trailing newline
* feat(agent-core-v2): tick the WaitFor progress status every second
* feat(agent-core-v2): format WaitFor progress durations as 1m 15s
* feat(agent-core-v2): omit zero seconds and minutes in WaitFor durations
* refactor(agent-core-v2): unify the loop-event fold into one core with two materializations
The loop-event stream was reduced by two hand-mirrored state machines:
loopEventFold.ts for the live/replayed context and contextTranscript.ts
for the full transcript behind the messages endpoints, kept in sync by
comments alone and already drifted (transcript dropped tool-result note
metadata and never closed a dangling tool exchange at step.end).
createLoopEventFold now owns the shared state machine once (settle,
pending tool exchanges, deferred appends, vacuous tracking) and both
views plug in as LoopEventFoldSink materializations. New parity tests
pin the foldedLength === live length invariant the endpoints splice on.
* fix(agent-core-v2): drop every removed prompt's injections on multi-turn transcript undo
The transcript undo only walked prompt-owned injections off the oldest
counted anchor, so with count > 1 an injection owned by a newer removed
prompt (e.g. an image-compression caption) survived the display undo
while the live context removed it. Collect every counted anchor's id
during the walk and sweep their owned injections afterwards, keeping
the transcript's 'prompt-owned ones leave with their prompt' contract
for every count and matching the live view.
* refactor(agent-core-v2): drop module headers from the context fold modules
The comment-free zone lint only allows JSDoc on exported symbols.
* fix(agent-core-v2): recover fold state after rehydration
* fix(agent-core-v2): scope undo injections to their prompt
* fix(agent-core-v2): settle open transcript frames when compaction lands mid-fold
An overflow-triggered compaction arrives with the failed attempt's
frame still open. The transcript appended the summary marker and reset
the fold but left the frame, so a vacuous partial stayed in the entries
while the live context dropped it, and a pending tool exchange lost its
interrupted result. Settle through the shared fold core at the marker
instead: close pending tool calls, drop or seal the open frame, then
append the summary. recoverFoldedLength recomputes the absolute count
right after either way.
* fix(agent-core-v2): keep legacy compaction recovery on the pre-settlement count
A legacy context.apply_compaction record (compactedCount without
keptUserMessageCount) recovers foldedLength as 1 + (foldedLength -
compactedCount), and the live legacy tail shape keeps the unsettled
open frame inside history.slice(compactedCount). Settling the fold for
those records shifted foldedLength by the settlement delta before the
recovery read it, leaving the transcript count one off the live
context. Gate the settle to modern records; legacy records keep the
previous freeze-and-reset behavior.
* fix(agent-core-v2): stop advertising unavailable ReadMediaFile to non-multimodal models
* fix(agent-core-v2): honor the effective tool policy before advertising ReadMediaFile
* fix(agent-core-v2): keep media-unavailable guidance reason-neutral and within the active toolset
* fix(agent-core-v2): recommend MCP fallbacks only when an active MCP tool exists
* fix(agent-core-v2): stop naming other tools in Read descriptions and errors
* fix(agent-core-v2): drop tool roster from plan agent prompt
Assistant messages carrying only tool calls were serialized without a
content key (JSON.stringify drops undefined), which strict
chat-completions validators such as LiteLLM reject with a 422,
permanently poisoning the session. Emit content: null for such messages
in both the kosong and agent-core-v2 converters, matching the shape
OpenAI responses use alongside tool_calls. The think-only empty-string
behavior in agent-core-v2 and both Kimi providers' deliberate content
omission are unchanged.
* 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
#2593 replaced AgentVideoResolverService with the image+video
AgentMediaResolverService and reduced videoResolverService.ts to a pure
deprecated alias with no DI registration. #2909's squash merge restored
the pre-#2593 file wholesale, bringing back the legacy class and its
registerScopedService call. Both classes then registered the same token
('agentVideoResolverService') at the Agent scope and the legacy
video-only resolver won on the production import order, so image
kimi-file:// references reached the provider unresolved. Gateways
reject the unknown scheme with a 400 ("unsupported image url"), the
media-strip fallback then hid the image from the model, and pasted
images only worked on undo-resend via the inline base64 fallback.
Delete the legacy alias files and their index exports, drop the stale
alias assertion, and pin the behavior with a klient e2e regression:
a kimi-file image prompt part must reach the provider as a data: URL,
never verbatim.
* test: replace prose-pinning system-prompt tests with a structural sharing check
The two removed tests pinned exact sentences of the default system prompt
('reversibility and blast radius', 'premature abstraction', optional-tool
phrasings that must not appear, ...). They broke on any intentional
wording change while only catching regressions that reused the same words.
The one real contract underneath — shared, ungated sections must render
byte-identically in the root agent and every subagent profile — is now
checked structurally by slicing the section out of the root prompt and
asserting the other profiles contain it, regardless of its wording.
* test: remove wording-pinning tests of model-facing prose across both suites
Sweep of the class identified in #3030: assertions pinning the exact
English wording of product model-facing text (system prompt, reminder
and injection .md files, tool descriptions, shipped profile/skill
bodies). They break on any intentional rewording yet only catch
regressions that reuse the same words.
Across 35 files (~60 test cases, net -1131 lines):
- deleted dedicated wording tests: 'exposes current metadata and
schema' description pins, goal/plan/todo reminder content tests,
tower skill-body prose pins, goal-outcome.test.ts;
- trimmed wording assertions from behavioral tests that otherwise
stand alone; kept identifiers (tool names, XML tags, section
markers), structural properties (wrapping/escaping/gating/cadence),
fixture data, tool outputs and error messages;
- re-anchored a few gating tests on exported constants
(WINDOWS_PATH_HINT, DEFAULT_REPLY_STYLE_GUIDE) instead of prose
literals.
Deferred for a follow-up decision: ~15 tests whose prose pin is the
only discriminator of which reminder/budget-band fired (constants not
exported). Wire baselines and snapshot machinery untouched.
#3028 removed the 'treat ambiguous requests as tasks' rule and its
'locate the method in the code' example from the default system prompt.
The profile test pinned that example verbatim, so it now fails on main.
The removal was intentional; update the test to the new contract.
* feat(kap-server): accept bundled skill activations on the prompt submission route
The bundled-submission capability was only reachable through the
in-process klient transports; the App talks to kap-server over /api/v1.
The submit-prompt route now accepts an optional non-empty skills field
and delegates to IAgentSkillService.promptWithSkills — same validation,
events, and single bundled user message as the TUI path — skipping its
own prompt-metadata update (the engine owns it there) and mapping
skill.not_found / skill.type_unsupported onto the skills route's codes.
To return the submission's queue identity, the engine's promptWithSkills
now resolves with prompt_id / user_message_id / created_at / state (plus
turn_id once launched), mirrored through the klient contract.
* refactor(agent-core-v2): slim the promptWithSkills result contract
Drop the user_message_id field (it is always the same identity as
prompt_id — the route duplicates it) and narrow state to the
running/queued/blocked vocabulary, mapped at the engine edge instead of
exposing the internal seven-state PromptState on the wire.
* fix(kap-server): harden bundled skill submissions against review findings
- Validate bundled skill names and types before any media materialization
or control override, so a rejected bundle leaves session state untouched
(the engine still re-validates authoritatively).
- Declare the 40415/40912 outcomes on the submit route so the generated
API documentation includes them.
- The klient output schema no longer tolerates a missing promptWithSkills
result (a transport-level absence now raises instead of resolving
undefined), and a failed launch surfaces as an error rather than a
successful running result.
- Add the changeset for the new public API field.
* fix(kap-server): preflight bundled skills before agent materialization and stabilize listed content
- Skill preflight now runs on the session's catalog before the main agent
is resolved, so a rejected bundle cannot mutate session metadata by
registering main (regression test on a cold session without an agent).
- The prompts list projection strips the stored skill blocks from a
bundled prompt, so GET /prompts returns the same caller-only content as
the submit response.
* fix(kap-server): reject bundled prompt_id combos at preflight and clean queued staging
- The skills + prompt_id incompatibility rejection now runs at the initial
bundled preflight, before the main agent is materialized or any
override binds (previously a yolo override could bind before the 40001).
- Queued bundles no longer skip staging cleanup forever: the discard is
deferred to the bundle's prompt.completed / prompt.aborted lifecycle
event, mirroring the plain path's launch-raced cleanup.
* fix(kap-server): clean queued bundle staging on the steer path too
A queued bundle steered into the active turn is consumed at steer time,
but the engine publishes prompt.completed/aborted only for the parent —
the deferred cleanup never fired and its subscription leaked. The
prompt.steered event (matching promptIds) now counts as the child's
intake-completion signal.
* fix(agent-core-v2): materialize daemon-ref media on the steer and inject paths
startNext materializes daemon file references into the session media
store before a prompt's turn, but steer() and inject() enqueued the same
references without that intake, leaving the staging upload as the only
copy — any staging cleanup at steer time would delete the media the
turn is about to consume. Both paths now run the same intake before the
SteerStepRequest is created, so prompt.steered is a truthful
intake-complete signal.
* fix(kap-server): defer staging cleanup to turn settlement, never to steer time
Prompt-intake materialization is best-effort: when it degrades, the
daemon upload is the request-time resolver's fallback source. Discarding
staging at prompt.steered could therefore delete the only readable copy
before the parent's request ran. Cleanup is now uniformly event-driven —
the bundle's own prompt.completed/aborted, or the steer parent's — so
the upload always outlives the request it feeds.
* fix(kap-server): install settlement tracking before bundled enqueue
A hook-blocked bundle completes synchronously inside the submission
call, and an exceptionally fast launch can settle just as early — a
post-call subscription misses the only settlement event and leaks both
the staging blob and the listener. The tracker now subscribes before
enqueueing, buffers lifecycle events, and settles against the returned
prompt id (or its steer parent's).
* fix(kap-server): scope settlement tracking to the owning agent and dispose on rejection
- The tracker now subscribes through the agent-scoped IEventBus instead of
the App-scoped IEventService: prompt lifecycle events from other
sessions never reach it, so a colliding client-chosen prompt id cannot
trigger a foreign settlement (and the steer re-target only follows this
agent's parent).
- A bundled submission that rejects after the tracker was installed now
disposes it on the error path instead of leaking a permanent listener.
* fix(agent-core-v2): keep steered prompts queued until their media intake finishes
Materializing a steered prompt's daemon-ref media awaits a file copy
during which the active turn may finish. Records are now spliced out of
the queue only after that copy completes, and when the turn is gone by
enqueue time they are restored to pending so startNext can launch them
as fresh prompts — their handles always launch or settle.
* fix(agent-core-v2): revalidate the queue and active turn after steer media intake
The daemon-ref copy yields, so settle/abort can consume selected records
and the active turn can rotate meanwhile. Only records still pending are
steered, and only into the turn that was active at entry; records that
vanish from the queue are left to their own launch path, and a missing
turn restores them to pending instead of splicing an unrelated tail
prompt. The intake/queue-preservation contract is documented in the
module header.
* fix(agent-core-v2): steer only the surviving records and keep their media truthful
- The steered content is rebuilt from the records that are still pending
after the media intake, so an aborted or concurrently consumed record's
text is never injected (or injected twice) alongside the surviving
handles.
- The enqueue is wrapped so an activeTurnOnly rejection restores the
records to pending (the loop throws instead of resolving a missing
turn, which made the previous rollback unreachable).
- The merged origin now carries the union of every record's bundled
skillActivations, and prompt.steered publishes the caller-only content,
so the skill instructions reach the model with their metadata intact
while the event projection stops leaking internal skill markdown.
* fix(agent-core-v2): harden steer rollback and register bundled prompt ids
* fix(agent-core-v2): strip bundled blocks from prompt.queued and reject partial steers
* fix(kap-server): update session metadata for bundled prompts routed to subagents
* fix(agent-core-v2): restart queue after raced steer rollback and prefix skill blocks in merged steer
* fix(agent-core-v2): block queue advancement during steer admission
* chore: drop the changeset for server-only protocol plumbing
* fix: tone down over-proactiveness in the default system prompt
The default system prompt pushed the agent to act before discussing:
ambiguous requests were explicitly resolved to tasks, the opening framed
the primary goal as taking action, and 'default to making progress, not
to asking' discouraged clarifying questions.
Trim both copies (agent-core and agent-core-v2) by deletion only:
the ambiguous-means-task rule and its example, the action-framed opening
clause, the 'default to taking action with tools' paragraph, the
duplicated must-use-tools sentence (kept once in Ultimate Reminders),
and the 'default to making progress, not to asking' bullet. Operational
guidance and the execution guards stay untouched.
* Delete .changeset/tame-system-prompt-proactiveness.md
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* fix: drop the tool-use and no-placeholder bullets from the default system prompt
---------
Signed-off-by: 7Sageer <sag77r@hotmail.com>
* chore: remove internal-network references from comments and test fixtures
- Reword two comments that named the internal free-tokens model
registration flow; the generic OAuth / managed wording carries the
same meaning
- Replace the qianxun.example placeholder base URL in google-genai and
runtime-provider tests with genai-gateway.example
- Swap realistic-looking LAN fixture IPs in the kimi web banner tests
(192.168.98.66, 10.8.12.216) for RFC 5737 documentation addresses
(192.0.2.66, 198.51.100.216)
* chore: retrigger CI (flaky kap-server searchRoute title-indexing test)
---------
Co-authored-by: bj456736 <bj456736@users.noreply.github.com>
* feat(kap-server): add page-number mode and total to GET /api/v2/sessions
The v2 session list gains a stateless 1-based `page` parameter beside the
opaque page_token cursor for admin-style lists that jump arbitrarily:
each request stays a full independent snapshot, no token is minted, and
`page` + `page_token` together fail 40001. Every response now carries
`total` (the filtered/sorted set size) in both pagination modes.
* feat(kap-server): add meta.updated_before filter to GET /api/v2/sessions
Symmetric with meta.updated_after (inclusive boundary, Unix ms), applied
at the edge over the drained set and bound into the page_token query
fingerprint like every other condition.
* feat(kap-server): add POST /api/v2/sessions:archive and :restore batch endpoints
Batch archive/restore for session-management views: { ids } (non-empty,
≤5000 unique after dedup) answers per-item results in input order with
succeeded/failed counts — only a body validation failure fails the whole
request, and an unknown id folds into its own item as 40401.
The live/cold split keeps the batch cheap: a session with a live handle
goes through the full ISessionLifecycleService chain (agents drain,
scope teardown, mirror drain), while a cold session is never
materialized — the new setColdSessionArchived helper in agent-core-v2
patches the persisted state.json (archived/archivedAt, updatedAt
preserved, mirroring setArchived's touchUpdatedAt: false semantics),
mirrors the flipped summary into the read-model queue, and republishes
the same event.session.archived bus event the live lifecycle emits
(:restore publishes nothing, matching the live restore). Hot items run
with bounded concurrency and the batch ends with one shared
ISessionIndexMirror.drain().
* docs(server-api): document v2 sessions page mode, total, updated_before, and batch archive/restore
* fix(kap-server): deep-import workspace lifecycle symbols in the v2 sessions route
CI's tsgo/rolldown (Linux) fail to bind liveHandlerForSession and
IWorkspaceLifecycleService through the agent-core-v2 package-root
barrel even though it re-exports them; the same files use the
established deep-import pattern already used for the git domain.
* fix(kap-server): inline the live-handler lookup in the batch route
The previous deep imports still fail to resolve on CI's Linux toolchain
(tsgo TS2307, rolldown MISSING_EXPORT) while every other module path
from the same package binds fine. Keep the route self-contained: the
hot-path lookup is a five-line loop over IWorkspaceLifecycleService's
handlers (mirrors agent-core-v2's liveHandlerForSession), and the tests
assert non-materialization behaviorally via the live map instead of
importing the same two symbols for spies.
* fix(kap-server): drive the batch hot path through getLiveSessionById
The phantom only hits the workspaceLifecycle-group symbols in these two
files on CI's Linux toolchain; getLiveSessionById is observed to bind
fine there. It returns the session's live scope directly (no resume),
which is exactly what the batch hot path needs.
* refactor(kap-server): move the batch live/cold split into agent-core-v2
setSessionArchivedBatch owns the split next to the cold patch: live
sessions go through the full lifecycle chain via the workspace handler
accessor (the v1-proven resolution path), cold sessions through the
direct write. The route becomes a thin wire-code adapter, and the batch
tests assert the live chain behaviorally (disposal, events, index)
instead of spying through scope accessors.
* fix(agent-core-v2): import sessionLookup relatively from coldSessionArchive
The '#/app/workspaceLifecycle/*' specifier resolves from src/ and
src/app/* files on CI's Linux toolchain but not from
src/workspace/sessionLifecycle/ (tsgo TS2307, rolldown follows); a
relative import bypasses the package-imports mapping.
* fix(agent-core-v2): migrate the batch hot path to ISessionManager
Main's workspace/session DI refactor removed the workspaceLifecycle
lookup modules; the live branch now goes through the App-level
ISessionManager (the same entry the v1 action route uses post-refactor)
with getLiveSessionById from the new sessionManager lookup.
* feat(kap-server): add the id,archived item projection to GET /api/v2/sessions
fields=id,archived trims each item to { id, archived } for
select-all-matching flows (the session admin page's Gmail-style
select-all). Only that projection gets the relaxed page_size ceiling
(10000); unknown fields, non-pair subsets, and include=git combinations
are 40001, and the projection binds into the page_token fingerprint so
shapes never flip mid-pagination.
* fix(agent-core-v2): serialize the batch cold write against in-flight resumes
Codex review on #2983: while a resume is in flight the live registry
hides the handle, so the batch route could classify the session as cold
and its direct write would race the materializing metadata service (its
stale in-memory document wins the next write, silently un-archiving the
session after the endpoint reported success).
The batch now settles the resume first: SessionManager registers the
whole resume promise synchronously at the App level (controllerForSession
is async, so the controller's own resuming map learns about it a few
microtasks late) and whenResumeSettled awaits it before classification —
a settled resume lands the item on the live chain, a failed one falls
back to the cold path. Also folds the module header down to the
package's external-role comment convention.
* fix(agent-core-v2): publish SessionArchived as an Event2 class in cold archive
* fix(agent-core-v2): serialize batch archive/restore with session lifecycle transitions
* fix(agent-core-v2): serialize session delete with the lifecycle chain
* fix(agent-core-v2): mirror the persisted metadata on cold archive, not the index summary
* docs(agent-core-v2): bring sessionManager comments and new tests to package conventions
* fix(agent-core-v2): normalize legacy session metadata before the cold archive write
* fix(kap-server): serialize the v1 single-session archive with the lifecycle chain
* chore: drop changesets for internal-only protocol work
* fix(agent-core-v2): encode cold-archived metadata for v1 readers
* fix(agent-core-v2): serialize fork and createChild with the source session's chain
* refactor(agent-core-v2): chain every session lifecycle method and hand batch sections unguarded ops
* fix(agent-core-v2): propagate failed resumes to the next settle
* fix(agent-core-v2): roll back the unannounced handle when a resume fails mid-materialization
* fix(agent-core-v2): read and migrate the legacy session-meta location on cold archive
* fix(agent-core-v2): serialize explicit-id session creation with the lifecycle chain
create() with a caller-supplied sessionId bypassed the per-session chain,
so a concurrent batch archive could classify the half-created session as
cold and write archived state that the live metadata service later
overwrites. Creation now queues on the target id's chain whenever an
explicit id is present.
Also type the resume-failure maps as Error and normalize at the catch
site, satisfying only-throw-error.
* style(kap-server): strip comments from the session routes per the no-comments convention
* fix(agent-core-v2): serialize explicit fork and child target ids on the lifecycle chain
fork() and createChild() with a newSessionId locked only the source id, so
a batch archive of the target could slip into the creation window: the
index already knows the half-created session, the batch writes archived
state to its document, and the fork's in-memory metadata later overwrites
it. Both operations now acquire the deduped, sorted key set so multi-key
sections always take locks in one deterministic order.
* 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
* fix(agent-core-v2): degrade media tool registration when the bound model alias is stale
A restored session replays its persisted profile.bind without catalog
validation, so the profile can carry a model alias that no longer
resolves (e.g. the managed kimi-code models were removed from
config.toml on logout). AgentMediaToolsRegistrar.refresh() called
modelCatalog.getRequester() unguarded on that alias; the throw escaped
the agent.status.updated listener and was reported as an [unexpected]
Error2 (config.invalid) on startup.
Catch the resolution failure and degrade to "no model": media tools
stay registered off the profile-reported capabilities, just without a
model-bound video uploader, matching the tryResolveRawModel style used
elsewhere in the profile service.
* test(agent-core-v2): reproduce the stale-alias regression with production-consistent collaborators
A stale alias makes the real AgentProfileService report
UNKNOWN_CAPABILITY, so the regression now binds unknown capabilities,
asserts the tool stays unregistered without surfacing an [unexpected]
error, and covers recovery once the alias resolves again. The rationale
moves into the mediaToolsRegistrar file header per the package comment
conventions.
---------
Co-authored-by: Mira <bj456736@users.noreply.github.com>
* refactor(agent-core-v2): carry the context fold cursor in state and converge fold/projection internals
- ContextModel state is now { messages, fold }: the loop-event fold cursor
(openStepUuid / pending / deferred) lives in the state instead of a
module-level WeakMap keyed by array identity, so wholesale replacements
(undo / clear / compaction / swarm exit) reset it structurally via
EMPTY_FOLD instead of a manual resetFold at five call sites.
- The display transcript and the wire model now share one generic fold
kernel (FoldFrame / FoldEntryAdapter), eliminating the mirrored second
implementation. Events tagged with a non-open step uuid are dropped and
step.end settles only the step it names — defensive in abnormal streams,
identical on well-formed ones (v1 replay unaffected).
- IAgentContextProjectorService converges to project(messages, policy) with
a ProjectionPolicy data object; llmRequester builds the policy from retry
state instead of selecting among four methods.
- Blob rehydrate now also covers messages still deferred in the fold cursor.
- ContextState is deeply frozen at the op boundary to preserve the consumer
immutability the wire's shallow freeze gave the bare array state.
* test(agent-core-v2): move fold parity rationales into the test file header
* docs(agent-core-v2): move fold declaration comments into module headers
* refactor(agent-core-v2): merge FoldFrame into generic ContextState
* refactor(agent-core-v2): compile-enforce part/event handling decisions in context memory
- isVacuousContentPart and dehydrateRecord now switch exhaustively over
ContentPart / LoopRecordedEvent variants, so a new variant fails
compilation until it takes an explicit position
- the transcript/model parity comparator spreads whole messages and masks
only summary content, so new ContextMessage fields join the comparison
automatically
- correct two stale header comments: local message ids persist with
append_message records, and undo's prompt-owned-injection pairing
depends on them after a resume
* refactor(agent-core-v2): converge undo-cut decision in conversationTime
The model Op and the display transcript each walked the undo anchors with
their own loop, and the transcript partially removed the tail when an undo
was blocked (compaction summary / clear floor / too few anchors) while the
model side no-ops at the precheck. Move the walk into conversationTime as
computeUndoCut/computeUndoCutFrom applied destructively by the context.undo
Op and non-destructively by the transcript reducer, so a blocked undo reads
identically on both sides.
Also: make isUndoAnchor exhaustive over origin kinds with a never assertion,
mirrors the compaction result message count via compactionHandoff, and
extend UndoCut with anchorIndex distinguishing the counted anchor from the
injection-extended cut point.
* fix(agent-core-v2): drop removed prompts' injections on multi-turn transcript undo
The transcript's kept-loop retained every injection after the oldest
counted anchor, so with count > 1 a prompt-owned injection of a newer
removed prompt (e.g. an image-compression caption) survived the display
undo while the model Op removed it. Collect the removed anchors' ids on
the same pass and keep only injections not owned by them, so the header's
'prompt-owned ones leave with their prompt' holds for every count.
* refactor(agent-core-v2): accumulate request projection repairs as policy
The llmRequester retry chain kept a RequestProjection union and translated
it into a ProjectionPolicy per attempt; repairs were mutually exclusive,
so a strict resend rejected again for body size or image format either
aborted or silently dropped the strict repair. Retry state is now the
ProjectionPolicy itself: each rejection adds its repair on its own axis
(media: 413 -> degraded -> strip; wire: structure -> strict) without
discarding the other, requestInput's translation layer and the unreachable
snapshot ??= disappear, and the persisted llm.request projection name
derives from the policy (the op enum gains strict-media-degraded /
strict-media-stripped). Also narrows ProjectionPolicy to the variants
actually produced (wire 'strict'; media 'degraded' | { strip }), dropping
the dead 'default'/'keep' literals and their guard.
* refactor(agent-core-v2): derive the visible context window from an append-only log
context.apply_compaction now appends a summary marker carrying the record
fields as CompactionMeta instead of replacing the folded history; the
model-visible window is derived at read time (visibleWindow) with the same
[head, elision?, tail, summary] layout, one deterministic derivation for
live dispatch and replay alike. The record format is unchanged, so v1- and
v2-written sessions keep replaying identically both ways.
- undo maps the visible-window cut back to a log position (the verbatim
legacy-summary edge falls back to the pre-append-only destructive cut)
- replay rehydrate only loads blobs the window derivation can surface
- contextInjector drops position tracking; injection positions become a
read-time scan that splices can never desync
- fullCompaction's safety check moves to the stable-identity log
* fix(agent-core-v2): rehydrate exactly the messages the visible window surfaces
The first append-only rehydrate rule kept pre-marker real user input plus
markers, but a legacyTail derivation keeps window.slice(compactedCount)
visible — assistant/tool media in that range stayed blobref after replay
and could be resent unresolved. Decide survivors by identity membership in
the derived window instead, which covers every derivation branch at once
and skips the unselected pre-marker pool as a bonus.
* fix(agent-core-v2): pop the visible-tail swarm reminder behind a legacy marker
SwarmService.exit decides the pop on the derived window's tail, but the
swarm_mode.exit reducer tested the raw log tail — after a legacyTail
compaction the survivor reminder is visible at the window tail while the
marker is the log tail, so the pop silently skipped and the stale reminder
stayed in the model context. Mirror the visible-tail decision in the
reducer and remove the entry by its stable log identity.
* fix(agent-core-v2): settle open transcript frames when compaction lands mid-fold
An overflow-triggered compaction arrives with the failed attempt's vacuous
partial still open. The transcript appended the summary marker and reset
the fold but left the frame; the retried step's step.begin then settled it
(-1) alongside the new frame (+1), so foldedLength stayed one short of the
model-visible window and kap-server's live-tail merge could duplicate the
retry tail. Settle the frame at the marker through the shared kernel;
recoverFoldedLength recomputes the absolute count right after either way.
* fix(agent-core-v2): settle open frames at the compaction marker
Apply settleModelOpenStep inside context.apply_compaction so a marker only
ever lands on a settled frame: no partial survives a marker and nothing
mutates the log behind one, making the append-only invariant structural
(the identity-prefix check in historySafeToCompact relied on it) instead
of timing-dependent. Mirrors the transcript's settle-at-marker.
Also freeze the derived visible window before caching it (an in-place
consumer mutation now throws instead of silently polluting the shared
cache), and drop the production-unreachable legacy branch of
buildContextCompactionShape so the legacy tail layout lives only in
deriveCompactionWindow.
* refactor(agent-core-v2): tighten naming and comments in context memory internals
- Slim file headers to the package header-only comment convention
- Rename PR-introduced identifiers for clarity: getMessageLog,
ProjectionPolicy.structure, pendingToolCallIds/deferredEntries,
removedEntryCount, deriveVisibleWindowAfterCompaction,
compactedWindowMessageCount
- Extract nextProjectionPolicyForError, removeUndoOwnedEntries and
summarizeProjectionRepairs; name fold intermediates after their
business stage
- Regroup splice-replay tests by topic and unify projection-call
recording in llmRequester tests
* fix(agent-core-v2): preserve bounded context state
* docs(agent-core-v2): restore the domain identity line in the compactionHandoff header
* refactor(agent-core-v2): rebuild context projection as a staged block pipeline
Split the 650-line projector service into three modules by concern:
mediaProjection (read-side media degrade/strip fallbacks), projection
(the structural transform), and the service (DI binding plus repair
reporting). Rebuild the structural projection as a two-stage pipeline:
pairBlocks groups tool exchanges into blocks that own their calls'
results, flattenBlocks serializes them back to wire order and merges
consecutive user prompts. The shared slot sentinel and index
back-patching are gone; the trailing-close and sizing-slice rules are
named and documented in the module header. Behavior is pinned unchanged
by the existing projector and llmRequester suites.
* docs(agent-core-v2): trim the projection helper header to its external role
* docs(agent-core-v2): keep the contextProjector module headers at the external-role level
* feat(cli): add --web-title and expose it via /meta
* refactor(kap-server): pass optional web_title directly in /meta
Per the repo rule for optional object properties, pass undefined directly
instead of a conditional spread; serialization omits the unset value.
* fix(cli): sync web bundle with instance tab title support
The committed dist-web bundle predates the document title feature, so a
released `kimi web --web-title` served a client that never read web_title.
Rebuilt from code-app (feat/web-document-title) via sync:web; the bundle
now titles tabs from web_title or the active workspace directory.
* ci: retrigger checks after flaky harness cleanup failure
---------
Co-authored-by: wbxl2000 <wbxl2000@outlook.com>
Co-authored-by: bj456736 <bj456736@users.noreply.github.com>
* refactor(agent-core-v2): carry the context fold cursor in state and converge fold/projection internals
- ContextModel state is now { messages, fold }: the loop-event fold cursor
(openStepUuid / pending / deferred) lives in the state instead of a
module-level WeakMap keyed by array identity, so wholesale replacements
(undo / clear / compaction / swarm exit) reset it structurally via
EMPTY_FOLD instead of a manual resetFold at five call sites.
- The display transcript and the wire model now share one generic fold
kernel (FoldFrame / FoldEntryAdapter), eliminating the mirrored second
implementation. Events tagged with a non-open step uuid are dropped and
step.end settles only the step it names — defensive in abnormal streams,
identical on well-formed ones (v1 replay unaffected).
- IAgentContextProjectorService converges to project(messages, policy) with
a ProjectionPolicy data object; llmRequester builds the policy from retry
state instead of selecting among four methods.
- Blob rehydrate now also covers messages still deferred in the fold cursor.
- ContextState is deeply frozen at the op boundary to preserve the consumer
immutability the wire's shallow freeze gave the bare array state.
* test(agent-core-v2): move fold parity rationales into the test file header
* docs(agent-core-v2): move fold declaration comments into module headers
* refactor(agent-core-v2): merge FoldFrame into generic ContextState
* refactor(agent-core-v2): compile-enforce part/event handling decisions in context memory
- isVacuousContentPart and dehydrateRecord now switch exhaustively over
ContentPart / LoopRecordedEvent variants, so a new variant fails
compilation until it takes an explicit position
- the transcript/model parity comparator spreads whole messages and masks
only summary content, so new ContextMessage fields join the comparison
automatically
- correct two stale header comments: local message ids persist with
append_message records, and undo's prompt-owned-injection pairing
depends on them after a resume
* refactor(agent-core-v2): converge undo-cut decision in conversationTime
The model Op and the display transcript each walked the undo anchors with
their own loop, and the transcript partially removed the tail when an undo
was blocked (compaction summary / clear floor / too few anchors) while the
model side no-ops at the precheck. Move the walk into conversationTime as
computeUndoCut/computeUndoCutFrom applied destructively by the context.undo
Op and non-destructively by the transcript reducer, so a blocked undo reads
identically on both sides.
Also: make isUndoAnchor exhaustive over origin kinds with a never assertion,
mirrors the compaction result message count via compactionHandoff, and
extend UndoCut with anchorIndex distinguishing the counted anchor from the
injection-extended cut point.
* fix(agent-core-v2): drop removed prompts' injections on multi-turn transcript undo
The transcript's kept-loop retained every injection after the oldest
counted anchor, so with count > 1 a prompt-owned injection of a newer
removed prompt (e.g. an image-compression caption) survived the display
undo while the model Op removed it. Collect the removed anchors' ids on
the same pass and keep only injections not owned by them, so the header's
'prompt-owned ones leave with their prompt' holds for every count.
* refactor(agent-core-v2): accumulate request projection repairs as policy
The llmRequester retry chain kept a RequestProjection union and translated
it into a ProjectionPolicy per attempt; repairs were mutually exclusive,
so a strict resend rejected again for body size or image format either
aborted or silently dropped the strict repair. Retry state is now the
ProjectionPolicy itself: each rejection adds its repair on its own axis
(media: 413 -> degraded -> strip; wire: structure -> strict) without
discarding the other, requestInput's translation layer and the unreachable
snapshot ??= disappear, and the persisted llm.request projection name
derives from the policy (the op enum gains strict-media-degraded /
strict-media-stripped). Also narrows ProjectionPolicy to the variants
actually produced (wire 'strict'; media 'degraded' | { strip }), dropping
the dead 'default'/'keep' literals and their guard.
* refactor(agent-core-v2): derive the visible context window from an append-only log
context.apply_compaction now appends a summary marker carrying the record
fields as CompactionMeta instead of replacing the folded history; the
model-visible window is derived at read time (visibleWindow) with the same
[head, elision?, tail, summary] layout, one deterministic derivation for
live dispatch and replay alike. The record format is unchanged, so v1- and
v2-written sessions keep replaying identically both ways.
- undo maps the visible-window cut back to a log position (the verbatim
legacy-summary edge falls back to the pre-append-only destructive cut)
- replay rehydrate only loads blobs the window derivation can surface
- contextInjector drops position tracking; injection positions become a
read-time scan that splices can never desync
- fullCompaction's safety check moves to the stable-identity log
* fix(agent-core-v2): rehydrate exactly the messages the visible window surfaces
The first append-only rehydrate rule kept pre-marker real user input plus
markers, but a legacyTail derivation keeps window.slice(compactedCount)
visible — assistant/tool media in that range stayed blobref after replay
and could be resent unresolved. Decide survivors by identity membership in
the derived window instead, which covers every derivation branch at once
and skips the unselected pre-marker pool as a bonus.
* fix(agent-core-v2): pop the visible-tail swarm reminder behind a legacy marker
SwarmService.exit decides the pop on the derived window's tail, but the
swarm_mode.exit reducer tested the raw log tail — after a legacyTail
compaction the survivor reminder is visible at the window tail while the
marker is the log tail, so the pop silently skipped and the stale reminder
stayed in the model context. Mirror the visible-tail decision in the
reducer and remove the entry by its stable log identity.
* fix(agent-core-v2): settle open transcript frames when compaction lands mid-fold
An overflow-triggered compaction arrives with the failed attempt's vacuous
partial still open. The transcript appended the summary marker and reset
the fold but left the frame; the retried step's step.begin then settled it
(-1) alongside the new frame (+1), so foldedLength stayed one short of the
model-visible window and kap-server's live-tail merge could duplicate the
retry tail. Settle the frame at the marker through the shared kernel;
recoverFoldedLength recomputes the absolute count right after either way.
* fix(agent-core-v2): settle open frames at the compaction marker
Apply settleModelOpenStep inside context.apply_compaction so a marker only
ever lands on a settled frame: no partial survives a marker and nothing
mutates the log behind one, making the append-only invariant structural
(the identity-prefix check in historySafeToCompact relied on it) instead
of timing-dependent. Mirrors the transcript's settle-at-marker.
Also freeze the derived visible window before caching it (an in-place
consumer mutation now throws instead of silently polluting the shared
cache), and drop the production-unreachable legacy branch of
buildContextCompactionShape so the legacy tail layout lives only in
deriveCompactionWindow.
* refactor(agent-core-v2): tighten naming and comments in context memory internals
- Slim file headers to the package header-only comment convention
- Rename PR-introduced identifiers for clarity: getMessageLog,
ProjectionPolicy.structure, pendingToolCallIds/deferredEntries,
removedEntryCount, deriveVisibleWindowAfterCompaction,
compactedWindowMessageCount
- Extract nextProjectionPolicyForError, removeUndoOwnedEntries and
summarizeProjectionRepairs; name fold intermediates after their
business stage
- Regroup splice-replay tests by topic and unify projection-call
recording in llmRequester tests
* fix(agent-core-v2): preserve bounded context state
* docs(agent-core-v2): restore the domain identity line in the compactionHandoff header
* 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
* fix(agent-core-v2): preserve tool call extras in tool.call loop events
* fix(google-genai): keep trailing user text before function results when merging
---------
Co-authored-by: Selene <mahaoyang@corp.netease.com>
* 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.
* 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.