* feat(telemetry): add system metrics collection
Add periodic CPU and memory telemetry sampling with warmup capture, lifecycle cleanup, and tests.
* fix(telemetry): attach prompt session to system metrics
Add an `interrupt_reason` field to the `turn_interrupted` telemetry event so the data can tell a deliberate user cancel (`user_cancelled`) apart from a programmatic abort (`aborted`), max-steps exhaustion (`max_steps`), an error (`error`), or a hook-filtered turn (`filtered`).
The user-cancel signal comes from the existing UserCancellationError carried as the abort signal's reason, reused here without changing any loop control or external protocol semantics.
* feat(server): support restoring and listing archived sessions
- add a `:restore` session action that clears the archived flag in state.json and returns the restored session
- add an `archived_only` list query param, mutually exclusive with `include_archive`, that post-filters to archived sessions
- keep the implementation in the server layer as a temporary measure until agent-core exposes restore natively
* fix(server): paginate archived-only sessions before response
* feat(web): add archived sessions page in Settings
Browse, search, filter by workspace, sort, and restore archived sessions
from a new Archived tab in Settings, backed by the server archived_only
list and :restore action.
* fix(web): keep archived Load more visible when a page filters to empty
When a search or workspace filter empties the loaded archived page, the
Load more button was hidden inside the non-empty branch, so users could
not fetch older pages to find a match. Move the button out so it stays
available whenever more archived pages exist.
* fix(server): preserve after_id bound while draining archived pages
Draining an archived_only request that starts from after_id would switch
to before_id and cross the pivot, reintroducing the pivot and older
sessions. Take a single filtered page for after_id instead of draining
past the lower bound.
* fix(server): drain archived_only within the after_id bound
An archived_only request starting from after_id now keeps paging toward
older sessions until it reaches the pivot, instead of treating the first
page as exhaustive. The loop stops as soon as it encounters the pivot
session itself, so it never reintroduces the pivot or anything older.
* feat(web): drain all archived pages for global search and sort
When the user searches, sorts, or changes the workspace filter in the
Archived settings page, fetch every remaining archived page first so the
client-side filter and sort run over the full set rather than only the
pages loaded so far.
* refactor(web): load all archived sessions upfront instead of paginating
Fetch every archived session once when the Archived settings tab opens and
drop frontend pagination entirely. Search, sort and workspace filter now run
over the full set, removing the empty-page and cursor bookkeeping that
previously caused bugs.
---------
Co-authored-by: qer <wbxl2000@outlook.com>
* feat(agent-core): feed AskUserQuestion answers back as question text and option labels
The flattened answers record the model receives was keyed by synthesized
ids (q_0 / opt_0_1), forcing a cross-message positional lookup against the
original tool call to understand what the user picked — both unreadable in
transcripts and a real model-misreads-the-choice badcase.
- toAgentCoreResponse now takes the original broker request and translates
wire ids back to question text (keys) and option labels (values);
unknown ids are kept verbatim, missing request falls back to raw ids
- wire protocol unchanged: clients still answer with option ids; the
resolve route reads the pending request before settling it
- question texts must be unique per call and option labels unique per
question, enforced in the tool execution path (AJV cannot express the
zod refine) and mirrored on the exported schemas
- web transcript card resolves both the new label form and legacy id
transcripts; TUI and ACP paths already produced the text form
* fix(agent-core): align multi-select answer join across clients and harden question schema
- Join multi-select labels with ', ' in the server translator, matching
what the TUI reverse-RPC path already emits, so the model sees one
format regardless of which client answered
- Trim segments in the web transcript resolver before label matching:
TUI-answered multi-select transcripts (', '-joined) previously lost
their highlight to a spurious leading-space Other row
- Move the question-text/legacy-q_<i> answer lookup out of the SFC into
askUserToolParse as answerFor(), per that module's testability intent
- Require non-empty question text and option labels (.min(1)) so empty
strings are rejected by AJV at the tool boundary instead of failing
deeper in the protocol layer
* fix(agent-core): resolve option ids only within the answered question
The translator's option-id lookup was a single flat map across all
questions, so a stale or malformed response pairing one question with
another question's option id (q_1 + opt_0_0) was silently translated
into a label that was never offered for that question. Scope the lookup
to the answered question's own options; cross-question and unknown ids
now both pass through verbatim, staying diagnosable.
* feat(agent-core): progressive tool disclosure via select_tools
Keep MCP tool schemas out of the immutable top-level tools[] and let the
model load them on demand, preserving the provider prompt cache:
- kosong: Message.tools (append-only load primitive, serialized as Kimi
messages[].tools with type:function wrapping and no content),
Tool.deferred (stripped once in generate() so loaded tools stay
executable without re-entering the top level), select_tools capability
bit (UNKNOWN/catalog default false).
- select_tools builtin: load-by-exact-name, three-branch semantics
settled per name (Loaded / Already available / Unknown), schemas read
from the live registry, injection-origin schema messages survive undo.
- ToolsDiffInjector: <tools_added>/<tools_removed> announcements at turn
boundaries and post-compaction, folded from history (undo/compaction/
resume self-heal), appended only when the loadable set changes.
- Loaded-tools ledger = history scan + defer-window pending set (cleared
on /clear); loop re-reads the executable table per step so a selected
tool dispatches on the next step of the same turn; preflight
distinguishes not-loaded from loaded-but-disconnected.
- Cross-cuts: projection strips protocol context for non-select_tools
models (lossless mid-session model switch both ways), compaction
filters it from the summarizer input and rebuilds loaded schemas
keep-all after folding, token estimation counts message.tools, request
logging reflects the post-strip wire tools.
- Three-condition gate: capability.select_tools x capability.tool_use x
tool-select experimental flag (KIMI_CODE_EXPERIMENTAL_TOOL_SELECT).
Any gate closed reproduces the inline request byte-for-byte; all
current models keep the capability off, so behavior is unchanged
until a supporting model is catalogued. The SDK catalog-to-alias
mapping forwards the capability so catalog-driven setups can enable it.
* feat(kosong): skip tool-declaration-only messages in non-Kimi providers
Message-level tool declarations (messages[].tools) are a Kimi wire
feature. The other providers' explicit field construction already keeps
the tools field off the wire, but the content-free leftover message
would be rejected (OpenAI: system message without content) or serialize
as a garbage <system></system> turn (Anthropic/Google system-to-user
wrapping). Skip such messages entirely via a shared predicate; a message
that also carries content only loses the tools field, as before.
Unreachable in kimi-code (the projection gate strips dynamic-tool
context for models without the select_tools capability before any
provider sees it) — defense-in-depth for direct kosong consumers.
* fix(agent-core): survive runtime flag flips and align tool table with post-compaction state
Two fixes from PR review:
- Register select_tools unconditionally and gate only its exposure in
loopTools. The tool-select flag can flip at runtime (config reload
calls setConfigOverrides on the live resolver) without
initializeBuiltinTools re-running; previously the disclosure shape
activated while the tool itself was unregistered, cutting the session
off from MCP entirely until a model/cwd change rebuilt the builtins.
A profile listing the name explicitly still never surfaces it in
inline mode, and execution guards the flip race defensively.
- Resolve the per-step tool table AFTER beforeStep, next to
buildMessages. beforeStep can run full compaction, which trims loaded
schemas and rewrites the ledger; a table captured before it could
still dispatch a tool whose schema the model no longer has. The
executable table and the request messages now always reflect the same
state, so a trimmed tool is rejected with select guidance instead of
executed.
* fix(agent-core): drop unused Tool import in dynamic-tools
* fix(agent-core): baseline compaction guard after post-compaction reinjection
The reinjected reminders (loadable-tools manifest, goal) are re-appended
after every compaction, but the nothing-new-since-compaction baseline was
captured before injectAfterCompaction. With a large manifest the guard
could re-trigger auto-compaction against a floor that cannot shrink.
Raise the baseline to the true post-compaction floor once reinjection
completes; the earlier capture stays as a fallback when reinjection
throws.
---------
Co-authored-by: fengchenchen <fengchenchen@moonshot.ai>
Default `thinking.keep` to "all" when Thinking is on so prior `reasoning_content` is kept across turns. Add `[thinking] keep` to config.toml and keep `KIMI_MODEL_THINKING_KEEP` as an override (env > config > default); off-values disable it.
This package only ever contained a package.json with no sources, dependencies, or scripts, and nothing in the repo imports it (the CLI uses @moonshot-ai/migration-legacy instead). Remove the directory and drop its entries from flake.nix and the changeset config, then refresh the lockfile.
* fix(tui): complete @ file mentions across additional workspace roots with fd
When additional workspace directories are added via /add-dir, @ file completion fell back to a readdir-based scanner capped at 2000 entries, so deeply nested files in large projects never appeared. Route @ completion through fd across every root instead, keeping the query pushed down to fd and deduplicating by absolute path. The readdir fallback remains for when fd is unavailable.
* fix(tui): preserve per-root full-path fallback for @ mentions
Address review feedback: decide the scoped-versus-full-path fallback per root instead of globally. When one root has the scoped directory but another does not, the latter still runs a whole-tree --full-path search with the original query, so a match that only exists under that root is not hidden just because a sibling root happens to contain the prefix directory.
* fix(tui): fall back to filesystem when fd binary is not executable
Address review: when fdPath is non-null but the binary cannot be spawned (managed fd removed or lost execute permission), @ completion returned null because pi-tui swallows the spawn error into an empty result, so the catch never ran. Probe fd with accessSync(X_OK) before delegating and use the filesystem fallback when it is not executable, while still returning null for genuine no-match results.
* fix(tui): trust bare fd command names when probing executability
Address review: when fd is discovered on the system PATH, detectSystemFdPath returns the bare name (fd/fdfind). accessSync checked that literal string relative to cwd and never searched PATH, so a valid system fd was treated as unavailable and @ completion fell back to the capped scanner. Trust bare names (spawn resolves them via PATH) and only probe absolute/relative paths, which is how the managed fd is referenced and which can go stale.
* chore: remove accidentally committed plan files
* test(pi-tui): stabilize paste-burst test by freezing the clock
The paste-burst heuristic uses an 8ms inter-character interval that a slow or busy CI runner can exceed between synchronous handleInput calls, which resets the burst and lets Enter submit. Freeze Date so the synchronous keystrokes always register as one burst, making the assertion deterministic.
* chore: ignore top-level plan directory
- add GET /api/v1/workspaces/{workspace_id}/skills backed by a listWorkspaceSkills core RPC and ISkillService.listForWorkDir, reusing the session skill-loading path so results match a new session
- web: populate the composer slash menu from workspace skills before a session exists, then fall back to session skills once one is active
* feat(session): rebuild session index on boot and self-describe workDir
- persist workDir into state.json so session dirs are self-describing and
summaries do not depend on the index's one-way-hashed workDir
- relax readSessionIndex so a stale or non-absolute index workDir no longer
drops an otherwise valid entry
- serialize in-process index appends to avoid torn jsonl lines
- add SessionStore.reindex() and run it once at server boot so the
scan-free request path can find sessions whose index line is missing
or stale
* chore: add changeset for session index rebuild
* fix(session): repair index entries with a stale workDir during reindex
- send tool declarations, system prompt, and sampling/thinking settings in the camelCase shape the Google SDK forwards so tool calls reach the model
- thread tool-call extras (thought signatures) through the loop tool-call event into context so Gemini 3 can resume a tool turn
- update and add tests for the corrected request shape and signature round-trip
Background (detached) shell commands were exempt from the 16 MiB output
ceiling, so a runaway background command could fill the disk or crash
the process. Apply the same cap to background shell commands and stop
feeding the disk write chain once it trips. Scope the ceiling to
process tasks so subagent and user-question results, which are appended
once and must be persisted, are left untouched.
* feat: hold print-mode turn until background subagents drain
In `kimi -p` (print mode), when the main agent ends a turn while background
subagents (`kind === 'agent'`) are still running, hold the turn open and
idle-wait until they finish, flushing their completions into the turn so the
model can react before the run exits.
Previously, the main agent could end its turn after launching background
subagents; the print flow then drained them with their completion
notifications suppressed, so the main agent never saw the results and the run
exited with the work abandoned (e.g. no nomination). This was the root cause
of the swarm-alpha-mining eval failures.
The hold is gated on a new `drainAgentTasksOnStop` session option (set by the
print flow), only affects `kind === 'agent'` background tasks, and is bounded
by `background.printWaitCeilingS`. Backfill / fan-out is handled by
re-enumerating active tasks. Other background task kinds and non-print modes
are unaffected.
* feat(server): add --dangerous-bypass-auth and --keep-alive flags
- --dangerous-bypass-auth disables bearer-token auth on every REST and
WebSocket route and advertises it via /api/v1/meta so the web UI skips
the token prompt; the startup banner drops the token and shows a red
danger notice
- --keep-alive keeps the daemon running instead of idle-killing after 60s;
implied by --host / --allowed-host and always on in --foreground mode
* fix(server): address review feedback on bypass-auth
- keep the token and skip the bypass notice when a daemon is reused, since
the requested --dangerous-bypass-auth flag is not applied to the
already-running server
- clear the cached dangerous_bypass_auth web state on HTTP 401 so a stale
bypass value cannot hide the token prompt after the server restarts
without the flag
* fix(pi-tui): make the viewport anchor follow above-viewport content shifts
The anchor pins a buffer row index, but an above-viewport length change
shifts the content living at every index below it. The pinned window
then suddenly showed content further along (visible upward creep), and
the rows that slid above the window top were lost: never committed to
scrollback, which holds older bytes at those indices. During streaming,
every above-viewport net shrink (a finished agent row collapsing, a
merged step, a spinner line disappearing) permanently swallowed that
many rows, and the blank area under the input box kept growing.
doRender now scores two hypotheses for such frames — window stayed put
vs window content shifted by the length delta — and when the shift
explains the frame strictly better, moves the anchor with the content:
a pure shift re-anchors with no painting at all (the screen already
shows exactly that content), and a shift with local in-window changes
(spinner/timer rows) repaints the window at the shifted anchor.
Commit order stays continuous, so the exactly-once scrollback invariant
is preserved: no loss, no duplication.
Covered by e2e case07 (above-viewport shift), which fails on the
previous revision; the stale-content unit test now asserts the
follow-the-content window instead of the old swallowed-row behavior.
* revert(pi-tui): restore upstream differential rendering behavior
The fork's viewport/scrollback rendering patches (clamping the diff to
the visible viewport, viewport re-anchoring on collapse, the pinned
anchor with commit-on-advance, cursor visibility guarding, and the
content-shift anchor follow) accumulated interacting edge cases faster
than they could be stabilized: blank screens, duplicated scrollback
spans, vanished rows, and a growing blank area under the input box.
Revert src/tui.ts to the upstream 0.80.2 differential rendering
behavior: a change above the viewport triggers a destructive full
redraw again. Verified line-by-line against the upstream source — the
only remaining divergences are the TypeScript strict-mode syntax
adaptations and the narrow-terminal fixes (Container width clamping
and overwide-line truncation replacing the upstream crash-and-throw),
which are kept.
Also remove the rendering-bug e2e ledger and the shrink test suite
that specified the reverted behavior, restore the pre-fork rendering
tests (the transient-content test asserts the upstream full-redraw
behavior again), and drop the e2e glob from the test script. Editor
input-history and paste-burst changes are untouched.
Known trade-off, accepted for now: the original scroll-position yank
during streaming (destructive redraws emitting ESC[3J) returns; the
rendering rework will restart from this clean baseline.
* chore: downgrade the web thinking-effort changeset to patch
* feat(media): materialize video uploads to cache and reference by path
- copy TUI video placeholders into the shared cache instead of
inlining the original source path
- emit <video path="..."> tags so ReadMediaFile / the provider's
VideoUploader owns upload behavior
- apply the same cache materialization to server prompt video
submissions, matching the TUI flow
- update TUI unit tests and server e2e test to assert cache-path
behavior
* fix(web): make uploaded videos play in the chat
Render the server's <video path> tag as a real video and reconcile the echoed user message so the bubble no longer shows raw markup or a duplicate. Serve file downloads with byte-range support and fetch video bytes with the bearer credential into a blob URL, since browsers cannot authorize a <video> src on their own. Also let users click an uploaded image to open it in the preview panel.
* fix(web): use authenticated source for uploaded image previews
openMediaPreview stored the raw getFileUrl as sourceUrl, and FilePreview renders it with a native <img> that sends no Authorization header, so the enlarge action 401'd for uploaded images. When the media carries a fileId, fetch the bytes through the authenticated API client and preview a blob URL instead, revoking it when the preview is replaced or closed.
* fix(web): ignore stale authenticated media fetches
AuthMedia fetches the file bytes asynchronously; when the component is reused with a new fileId before a prior fetch resolves (e.g. queued thumbnails keyed by index), the older response could still create a blob URL and show the previous file. Add a per-request sequence guard (and an unmount guard) so a stale response is discarded and its blob URL revoked instead of being applied.
* fix(web): gate media path tags on file-store id shape
Treating any standalone <video path="..."> text as an uploaded daemon file and stripping the basename into getFileUrl is only valid for server cache files named after the file-store id (f_…). TUI/ReadMediaFile tags use arbitrary cache names like <uuid>-<label>, and older transcripts may point at paths like /tmp/foo.mp4; those produced a broken /files/<basename> request. Only extract a fileId when the basename matches the file-store id shape, otherwise leave the raw tag as text.
* fix(web): invalidate pending media preview on close
Closing an uploaded-image preview before getFileBlob() resolved left previewRequestSeq untouched, so the fetch callback still passed its seq check, created a blob URL, then skipped attaching it because previewFile was already null — leaking up to the file size until another preview opened. Bump previewRequestSeq on close so the in-flight callback bails before creating the blob URL.
* fix(web): defer authenticated media fetch until near viewport
AuthMedia fetched the full image/video into a Blob on mount whenever a fileId was present, bypassing native loading="lazy" and preload="metadata". Opening a session with several historical large video uploads started many full downloads and held all blobs in memory even if the user never scrolled to or played them. Use an IntersectionObserver to defer the fetch until the element nears the viewport.
* fix(web): revoke preview blob when leaving the file panel
Switching to another detail panel only flips detailTarget and never calls closeFilePreview, so an in-flight getFileBlob could still create a blob URL after the file panel hid, and an already-shown blob URL was held until the next file preview. Check detailTarget before creating the blob URL, and reset/revoke the preview when detailTarget leaves 'file'.
---------
Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
* fix(pi-tui): stop scrollback duplication from viewport rewinds
Rewinding the viewport anchor repaints rows that the terminal
scrollback already holds, and the next scroll commits them again —
every rewind duplicated its span. Two paths triggered it during
streaming oscillation (content shrinking then growing back): the
shrink re-anchor rewound immediately, and the clamped differential
path then painted the shifted content through the screen.
Rework the shrink/shift handling around a shared in-place viewport
repaint that never scrolls, with the anchor treated as the scrollback
high-water mark that must never move backward:
- Partial shrinks keep the anchor pinned: the content bottom hovers
above a bounded blank gap that the next growth naturally fills. No
rewind means duplication is impossible by construction.
- Only a collapse past the viewport top (compaction, clears) rewinds,
as nothing sensible could be shown otherwise; the content has
changed so drastically there that the repainted span is not
recognizable as a duplicate.
- Above-viewport length changes repaint the visible window in place
instead of painting through: nothing scrolls, scrollback keeps the
stale old version, and the anchor only advances when the content
outgrows the pinned window.
- Deleted-tail changes within a pinned viewport repaint at the pinned
anchor instead of falling back to a destructive full render.
Pure appends keep flowing through the screen into scrollback, and
equal-length above-viewport changes keep the bounded clamped diff.
* fix(pi-tui): commit skipped rows on anchor advance and guard cursor visibility
Address two review findings on the pinned-anchor rendering:
- An anchor advance (growth past the pinned viewport combined with an
above-viewport change) repainted the screen in place without
scrolling, so the rows between the old and new anchor were never
committed to scrollback and vanished. repaintViewport now paints from
the old anchor and lets the paint loop scroll the skipped rows out,
committing each exactly once with fresh content.
- positionHardwareCursor recorded hardwareCursorRow on a logical row
outside the visible window when the cursor marker sat above a pinned
viewport (tall editor after a deep shrink), desyncing every later
differential move. It now hides the cursor and keeps the bookkeeping
on the real cursor row when the marker is not visible.
Also add an e2e rendering-bug ledger (packages/pi-tui/e2e): one
xterm-emulated repro per production rendering bug, asserting the
renderer invariants (monotonic anchor, exactly-once commit, cursor
bookkeeping sync). The two findings above are case05/case06.
* ci: run the pi-tui node:test suite in a dedicated job
pi-tui's tests (unit + e2e) run on node:test, which the root vitest
run silently skips, so CI never executed them. Add a test-pi-tui job
that runs the package's test script.
* feat(agent-core): guide the model away from repeating denied or failed tool calls
- system.md: add a diagnose-before-retrying paragraph next to the existing
permission-denial guidance, covering failed tool calls
- permission: when the user rejects an approval on the main agent, tell the
model not to re-attempt the exact same call (sub agents already had an
equivalent hint)
* fix(agent-core): close abandoned tool exchanges and dedupe duplicate tool_use ids
A turn that dies between a recorded tool.call and its paired tool.result
(e.g. a transcript write failure mid-batch) used to leave
pendingToolResultIds open forever: every later message was stranded in
deferredMessages and user input was silently swallowed.
- runOneTurn now defensively closes any dangling tool calls when a turn
ends (completed, cancelled, or failed), synthesizing an error result
that names the cause, with a warn log and a tool_exchange_abandoned
telemetry event
- the projector drops assistant tool calls whose id already appeared
earlier (first occurrence wins): a duplicate id is wire-invalid on
strict providers and not repairable by the strict resend; reported via
the existing projection-repair log and telemetry
- resume-side closePendingToolResults now logs what it closes (warn for
a mid-history gap, info for the routine trailing interruption)
* chore: add changesets for tool exchange fixes
* fix(agent-core): scope duplicate tool_use id dedup to the strict resend
Unconditional dedup regressed providers that emit per-response counter
ids (e.g. call_0 in every step) and accept their own duplicates: later
tool exchanges silently vanished from the projected history, and a
duplicate call's own recorded result was left dangling.
- the dedupe pass is now opt-in via dedupeDuplicateToolCalls and enabled
only in strictMessages, so the normal projection keeps the history the
provider produced
- the pass also drops every tool result after the first for an id, so no
dangling tool message survives; when the kept call has no result of
its own, the surviving one is reattached by the adjacency repair
- kosong now classifies the Anthropic "tool_use ids must be unique" 400
as a recoverable request-structure error so it triggers the strict
resend
* feat(cli): wait for background subagents before exiting kimi -p
When `background.keep_alive_on_exit` is enabled, `kimi -p` now waits for
all background subagents to reach a terminal state before exiting, bounded
by `background.print_wait_ceiling_s` (default 3600s). This lets concurrent
background subagents run to completion in single-turn runs instead of being
torn down when the main agent's turn ends.
* fix(agent-core): route image-compression captions through hidden system reminders
Prompt ingestion (server upload/base64 route, TUI paste, ACP) annotates a
compressed image with an inline <system> caption inside the user's own
message. That raw markup rendered verbatim in every user-visible history
projection (TUI session replay, web UI) and leaked into session titles.
Split the caption out at the appendUserMessage chokepoint and deliver it
through the built-in system-reminder injection (origin
{kind: 'injection', variant: 'image_compression'}), which every UI already
hides. The model still receives the full note; ingestion sites and the wire
protocol are unchanged. Session titles/lastPrompt strip the caption the same
way. Tool-result captions (MCP) keep the established <system> convention.
Covered by unit tests plus an end-to-end smoke suite that drives
rpc.prompt/steer through the real turn pipeline and asserts the provider
wire request, stored history, replay records, and resume parity.
* chore: tighten changeset wording per gen-changesets conventions
* feat(agent-core): strengthen the language-matching rule in the default system prompt
* chore: refine changeset wording
* fix(kaos): enrich PATH from the user's login shell at startup
When kimi-code is launched from a context that skipped the user's shell
profile (GUI launchers, non-login parent shells), process.env.PATH misses
entries like /opt/homebrew/bin, so commands spawned by the Bash tool
cannot find user-installed tools such as gh.
LocalKaos.create() now probes the user's login shell once
($SHELL -l -c env, 5s timeout, memoised) and appends the missing PATH
entries to process.env.PATH. Existing entries keep their order and
priority; probe failures silently leave PATH untouched. Windows is
skipped: the problem is specific to POSIX login-shell profiles.
* fix(kaos): fall back to the account login shell when $SHELL is unset
launchd/daemon launches can leave $SHELL unset or blank — the very
contexts whose PATH is impoverished — so the login-shell PATH probe
would give up exactly where it matters most. Resolve the shell from the
OS user database (os.userInfo().shell) before giving up; lookups that
throw (uid without a database entry) or yield nologin shells degrade
silently as before.
* fix(kaos): preserve empty PATH components when merging login-shell PATH
POSIX command lookup treats an empty PATH component (leading colon,
trailing colon, or double colon) as the current directory. The merge
previously filtered those out of the current PATH and rewrote the value
even when nothing was appended, silently dropping cwd lookup for users
who rely on it.
Keep the current PATH string verbatim as the prefix, append only the
missing login-shell entries, and skip the env write entirely when the
login shell contributes nothing — an unset PATH stays unset, a set PATH
is never rewritten. Empty login-shell components are still never
imported.
* fix(kaos): only import absolute login-shell PATH entries
A `.` or relative component in the login-shell PATH is cwd-dependent
lookup with another spelling, and LocalKaos runs commands from arbitrary
workspace directories — importing one would let a command name resolve
from an untrusted project cwd. Tighten the merge's skip condition from
"empty" to "not absolute", which subsumes the empty-component check.
* fix(kaos): invoke the login-shell probe's env by absolute path
A bare `env` inside `$SHELL -l -c` resolves through the inherited PATH
from the workspace cwd. If that PATH carries a cwd-dependent component
(which the merge deliberately preserves), a repo-planted `env` binary
would run automatically at session startup and could feed the probe an
arbitrary PATH. /usr/bin/env is guaranteed on mainstream POSIX systems
and also bypasses profile function shadowing.
* feat(agent-core): guide the model away from repeating denied or failed tool calls
- system.md: add a diagnose-before-retrying paragraph next to the existing
permission-denial guidance, covering failed tool calls
- permission: when the user rejects an approval on the main agent, tell the
model not to re-attempt the exact same call (sub agents already had an
equivalent hint)
* fix(agent-core): close abandoned tool exchanges and dedupe duplicate tool_use ids
A turn that dies between a recorded tool.call and its paired tool.result
(e.g. a transcript write failure mid-batch) used to leave
pendingToolResultIds open forever: every later message was stranded in
deferredMessages and user input was silently swallowed.
- runOneTurn now defensively closes any dangling tool calls when a turn
ends (completed, cancelled, or failed), synthesizing an error result
that names the cause, with a warn log and a tool_exchange_abandoned
telemetry event
- the projector drops assistant tool calls whose id already appeared
earlier (first occurrence wins): a duplicate id is wire-invalid on
strict providers and not repairable by the strict resend; reported via
the existing projection-repair log and telemetry
- resume-side closePendingToolResults now logs what it closes (warn for
a mid-history gap, info for the routine trailing interruption)
* chore: add changesets for tool exchange fixes
* fix(agent-core): scope duplicate tool_use id dedup to the strict resend
Unconditional dedup regressed providers that emit per-response counter
ids (e.g. call_0 in every step) and accept their own duplicates: later
tool exchanges silently vanished from the projected history, and a
duplicate call's own recorded result was left dangling.
- the dedupe pass is now opt-in via dedupeDuplicateToolCalls and enabled
only in strictMessages, so the normal projection keeps the history the
provider produced
- the pass also drops every tool result after the first for an id, so no
dangling tool message survives; when the kept call has no result of
its own, the surviving one is reattached by the adjacency repair
- kosong now classifies the Anthropic "tool_use ids must be unique" 400
as a recoverable request-structure error so it triggers the strict
resend
Compaction runs at the point of maximum context for the task, and the next
turn resumes with less. So the handoff note now records the plan for the
remaining work — upcoming steps, settled decisions, and foreseeable obstacles,
plus any work that can be pre-committed — instead of only the immediate next
command. Update the affected compaction snapshots and one hardcoded
input-token assertion (the instruction is ~163 tokens longer).
* fix(pi-tui): repaint viewport in place when content collapses above it
When content shrinks past the viewport top while a line above the
viewport also changes, the clamped differential path left the render
loop empty, cleared deleted lines past the screen bottom, and desynced
the cursor anchor — leaving the viewport blank with the input box gone
until a full redraw.
Repaint the visible viewport in place for that case (no ESC[3J, so the
scrollback and the user's scroll position are preserved), and clamp
deleted-line clearing to the screen bottom so it can never scroll
untracked.
* fix(kimi-code): clear the screen fully on session reset
The collapse repaint in pi-tui intentionally preserves scrollback, so
/new, /clear, and session switches no longer got a clean screen as a
side effect of the destructive full redraw — the previous session's
text stayed above the welcome banner.
Session resets want a pristine screen, so force a destructive full
render explicitly instead of relying on the renderer's shrink
behavior.
* fix(pi-tui): delete kitty images straddling the viewport top on collapse repaint
A multi-row kitty image can start above prevViewportTop while its
reserved rows are still visible. The collapse repaint's image-delete
range started at prevViewportTop and missed the image line carrying the
id, leaving a stale overlay that also dropped out of
previousKittyImageIds tracking. Widen the range to include such a
straddling block.
* chore: shorten session reset changeset wording
* fix(pi-tui): re-anchor the viewport whenever content shrinks below the screen bottom
previousViewportTop only ever grows during normal rendering, so after a
shrink the content bottom could hover above the screen bottom, leaving
dead rows that nothing repaints. Upstream masked this by frequently
doing destructive full redraws, which re-anchored as a side effect; the
fork removed those redraws without replacing the re-anchoring.
Generalize the collapse repaint into a re-anchor check at the top of
the differential path: whenever prevViewportTop exceeds
max(0, newLines - height), repaint the visible viewport in place with
the tail of the new content. The input area snaps back to the screen
bottom, scrollback and the user's scroll position stay intact (no
ESC[3J), and the previous collapse branch becomes a defensive
destructive fallback.
Update the three renderer tests that encoded the old behavior
(destructive redraw on shrink, viewport hover after clamped shrink) to
assert the re-anchored behavior instead.
* fix(kimi-code): full repaint on ctrl+o expansion toggle
Expanding tool output shifts content above the viewport; the clamped
differential render paints the shifted content through the screen,
stacking a duplicate copy below the stale one in scrollback on every
toggle. The toggle is a deliberate user action (like /clear), so do a
destructive full render instead: scrollback holds exactly one copy and
the expanded output stays readable by scrolling up.
* chore: simplify user-facing changeset wording
* fix(agent-core): recover sessions bricked by orphan tool results
A stray `tool` message with no preceding assistant `tool_calls` permanently
bricked a session on OpenAI-compatible providers: every turn re-sent the same
malformed history and got a 400, and switching model/provider did not help.
Two independent gaps caused this:
- kosong did not recognize the OpenAI / DeepSeek / vLLM / Qwen phrasings of the
tool-exchange structural 400 (`role 'tool' must be a response to a preceding
message with 'tool_calls'` and the mirror `assistant message with 'tool_calls'
must be followed by tool messages`), so the post-400 strict-resend fallback
that drops the orphan never fired.
- The legacy-restore compaction path kept a verbatim tail
`history.slice(compactedCount)`; when the cut landed inside a tool exchange the
tail began with an orphan tool result whose assistant was summarized away. The
normal projection does not repair a leading orphan, so the malformed history
was baked in and re-sent every turn.
Recognize the additional phrasings so the strict resend un-bricks any session,
and trim leading tool results from the legacy-restore tail so the orphan is
never persisted in the first place.
* fix(agent-core): drop orphan tool results at the projection boundary
Rework the legacy-restore half of the fix based on review feedback: mutating
`_history` at restore time desyncs every consumer that models the history from
the wire records — the transcript reducer's fold length would overcount and
make MessageService skip unflushed live-tail messages.
Keep the restored history faithful to the wire records instead, and drop a
`tool` result whose call is nowhere in the history at the projection boundary,
on every request-building projection: the normal wire (`messages`), the
post-400 strict resend (`strictMessages`), and the compaction summarizer. An
orphan is wire-invalid on strict providers and useless to the model either
way, so it never reaches a provider — no longer relying on recognizing the
provider's 400 phrasing to recover. Fragment projections (e.g. token-estimating
a history slice) leave results untouched, since a matching call may
legitimately sit outside the slice.
* feat(agent-core): keep head and tail of user messages during compaction
Compaction used to keep only the most recent 20k tokens of real user
input, so the original task statement was the first thing to vanish in
long sessions. Now, when the user-message pool fits the 20k budget it is
still kept whole; when it overflows, the oldest 2k tokens and the most
recent 18k are kept instead, with an elision marker between the two
segments telling the model what was omitted and that the summary covers
it. The summary prefix and the default system prompt describe the new
shape as well.
The new `keptHeadUserMessageCount` record field keeps restore and the
wire-transcript folded length consistent: records without it (written by
older versions) restore with the original tail-only selection that
produced them, and the vis model-mode projection mirrors the same
head/marker/tail rebuild.
* style(agent-core): drop redundant spread over slice in head selection
* feat(agent-core): announce image compression and keep originals readable
Every image ingestion point (ReadMediaFile, MCP tool results, clipboard
paste, REST upload/inline base64, ACP) now places a <system> caption next
to a compressed image stating the original vs. delivered dimensions, byte
size, and format, so downsampling is never silent to the model.
Originals stay readable: file uploads point at the stored blob, and
in-memory images are persisted into the session's media-originals dir
(content-addressed, size-capped, removed with the session; temp-dir
fallback when no session is known).
ReadMediaFile gains region (crop in original-image pixel coordinates,
delivered at full fidelity) and full_resolution (skip downscaling, with
an explicit error over the per-image byte limit), so the model can zoom
into fine detail instead of silently degrading on large images.
* fix(agent-core): exempt compression captions from the MCP text budget
The caption announcing an image's compression was inserted before the
shared 100K text budget was applied, so a chatty MCP result (page text +
screenshot) consumed the budget first and the caption was evicted — or
sliced mid-string into an unclosed <system> fragment — while the
downsampled image survived, silently reintroducing the exact degradation
the caption exists to report, and orphaning the persisted original.
Split the size-limit pass in two and reorder the pipeline: the text
budget now runs on the tool's own text BEFORE compression inserts
captions (exempt by construction), and the per-part binary cap still
runs after compression so compressible screenshots are kept.
* fix(agent-core): harden crop error reporting and document readback semantics
- cropImageForModel rejects non-finite region coordinates with a clean
message instead of surfacing the codec's internal validation dump
- the full_resolution and cropped-region over-budget errors now include
exact byte counts alongside the rounded sizes, so a file a hair over
budget no longer reads "is 3.8 MB, over the 3.8 MB limit"
- read-media.md notes that re-reading a file without region or
full_resolution reproduces the same downsampled image
* feat(tui): include shell commands in input history
Shell commands entered through the `!` prompt are now saved to input history. Recalling one restores bash mode, and in bash mode Up only cycles through previous shell commands while a normal prompt browses all history.
* docs(interaction): document shell command recall in input history
Note that shell commands are now saved to input history and can be recalled in Shell mode, in both the English and Chinese interaction guides.
* feat(pi-tui): add setHistoryFilter and onRecall to editor history
Add two first-class hooks to the editor's history navigation: setHistoryFilter to limit which entries Up/Down visit, and onRecall to decorate a recalled entry before it is shown. Draft restore, direction-aware cursor placement, and undo behavior are unchanged.
* refactor(tui): use pi-tui history filter for shell command recall
Replace the CustomEditor navigateHistory shadow with pi-tui's setHistoryFilter + onRecall hooks, wired in the editor-keyboard controller. This keeps pi-tui's draft-restore and direction-aware cursor behavior intact (the shadow dropped both) and moves the shell/prompt filtering and mode-restore logic into the business layer.
* feat(pi-tui): save and restore host state with the history draft
Add onHistoryDraftSave/onHistoryDraftRestore hooks so hosts can stash their own state when entering history browsing and restore it when the user navigates back to the draft. The saved host state is discarded when browsing ends any other way (typing, submit), mirroring the editor draft lifecycle.
* fix(tui): restore input mode when returning to the history draft
Wire pi-tui's history draft save/restore hooks to the editor input mode. Without this, recalling a shell entry and then pressing Down back to an empty draft left the editor in bash mode, so the next typed message was submitted as a shell command.
* fix(pi-tui): capture host draft state before running the history filter
Fire onHistoryDraftSave before the history filter runs when entering browse, so the host's filter can read the browse-entry mode rather than a mode that changes as entries are recalled. The captured state is still only committed once a matching entry is found.
* fix(tui): lock history filter to the browse-entry mode
Lock the history filter to the input mode captured when entering browse. Previously the filter read inputMode live, so after recalling a shell entry (which flips to bash mode) a second Up would only show shell commands.
* docs: add pi-tui narrow-width fix plan
* fix(pi-tui): stop wordWrapLine infinite recursion on wide graphemes at width 1
* docs: extend pi-tui narrow-width plan with emoji grapheme regression coverage
* fix(pi-tui): clamp container render width to a minimum of 1
* fix(pi-tui): truncate overwide rendered lines instead of throwing
* perf(pi-tui): fast-path overwide line detection and enlarge width cache
* test(pi-tui): assert exact truncated viewport in overwide line test
* docs: record review amendments in pi-tui narrow-width plan
* test(pi-tui): add editor narrow-width regression tests
* docs: record task 4 review amendments in pi-tui narrow-width plan
* fix(pi-tui): guard blank-line padding against negative widths
* docs: record task 5 review amendments in pi-tui narrow-width plan
* docs(pi-tui): document local divergences from upstream
* chore: add changeset for pi-tui narrow width fixes
* docs(pi-tui): point Text guard test to its actual test file
* test(pi-tui): translate narrow-width test comments to English
* docs: remove internal pi-tui narrow-width plan
* docs(pi-tui): translate AGENTS.md to English
* feat(agent-core): align model-facing prompts with actual tool behavior
A hunk-by-hunk accuracy pass over every model-visible prompt surface
(system.md, tool .md descriptions, zod describes, profile role prompts,
and injected reminder strings), with each claim verified against the
implementation and, where possible, empirically (ripgrep semantics).
Fix descriptions that drifted from the code:
- Grep `glob` matches against each file's absolute path, so
`src/**/*.ts` silently matches nothing — document the working forms
- Glob `path` accepts relative paths; results are files-only
- FetchURL no longer promises a content-type-to-mode mapping the
default provider does not honor
- cron: a pinned-date 5-field expression repeats yearly unless
`recurring: false`; drop a bench-only env knob from cron-list
- skill `args` expansion covers $NAME/$1/$ARGUMENTS and the trailing
ARGUMENTS: line; goal reminder no longer cites a nonexistent
developer-message channel
Disclose enforced-but-silent behavior:
- cron fires deliver only while the session is idle; expressions with
no fire within 5 years are rejected at create time
- VCS metadata directories are always excluded from Glob/Grep, even
with include_ignored; sensitive-file guard exemptions
(.env.example/.env.sample/.env.template, public SSH keys)
- large images may be downsampled while the <system> block reports
original dimensions; subagent summaries under the length floor are
sent back for expansion; background-disabled Agent calls are
rejected before launch; AGENTS.md beyond ~32 KB triggers a
performance warning (surfaced in the /init prompt)
Resolve cross-surface contradictions:
- AskUserQuestion background describe/envelope no longer teach polling
- AgentSwarm subagent_type documents that resume keeps original types
- bash.md scopes &&-chaining to dependent commands and steers
independent read-only commands to parallel calls
- the shared system prompt no longer names tools that read-only
subagent profiles lack
Add missing guidance:
- denied/rejected tool calls mean the user declined that action —
adjust, don't retry or route around (root agent)
- plan subagent now knows it is read-only; coder subagent knows its
final message is the entire handoff; explore subagent knows web
tools are in scope
- gh CLI routing for GitHub-hosted work; FetchURL login-wall note;
a dual-use content-safety boundary; scope discipline,
surrounding-idiom, and dependency-verification norms; file:line
citation convention; progress notes on long multi-phase tasks
* fix(agent-core): let the model fetch a background answer after the completion notice
In sessions with background persistence (any agent with a homedir), a
background question's answer is flushed to output.log and the completion
notification carries an <output-file> pointer, not the answer text. The
previous envelope wording ("use TaskOutput only to re-read the answer if
you missed the notification") gated the normal post-completion fetch
behind a missed-notification condition, so a model could acknowledge the
notice and continue without ever reading the user's answer.
Reword the envelope to state that the completion notice may carry a
pointer and to direct the model to read that file (or call TaskOutput
once) for the answer, while still forbidding polling before the user
responds. Align the background param describe the same way ("notified
automatically" rather than "the answer arrives", polling scoped to the
pending window).
* fix
Moonshot / Kimi (OpenAI-compatible) rejects a history whose tool message
references a tool_call_id with no matching tool_calls entry in the preceding
assistant message as `400 tool_call_id is not found`. The
TOOL_EXCHANGE_ADJACENCY_MESSAGE_PATTERNS only covered Anthropic's
tool_use/tool_result phrasing, so isRecoverableRequestStructureError returned
false, the strict-resend fallback in executeLoopStep never fired, and the
session stayed permanently stuck re-sending the same rejected history every
turn (observed in the field after a manual compaction busted the prompt cache
and forced full revalidation of a latently misordered prefix).
Add the tool_call_id-anchored pattern so the whole recovery chain — strict
projection (adjacency repair, orphan-result drop, synthetic results) plus the
one-shot resend — now also covers the default provider. Covered by classifier
unit tests and an e2e resend-and-recover case.
* feat(agent-core): sharpen the compaction handoff prompt
Refine the first-person handoff note the model writes at compaction so it
preserves what actually gets dropped instead of what already survives:
- Lead with the intent of the latest request, not a verbatim re-transcription
(the recent user messages are already kept verbatim beside the summary);
name which request governs when several are in play.
- Carry forward tool results — the concrete values, key lines, schemas — not
just the commands that produced them.
- Keep settled decisions separate from still-open questions, and name the
context the next turn must go and re-check.
- Write in the conversation's language, keep the note proportional to the task,
and don't re-transcribe the auto-attached TODO list.
Also correct the system prompt's description of the post-compaction shape: the
recent user messages come first, followed by a first-person summary (not a
rigidly sectioned report), and a newer kept message supersedes the summary.
Update the affected inline snapshots and the compaction request token count.
* fix(agent-core): preserve an oversized latest request in the handoff note
selectRecentUserMessages truncates a kept user message to the size cap,
keeping only its prefix, so when the latest request itself exceeds the cap
only its head survives verbatim beside the summary. Telling the summary
"don't re-transcribe, it survives verbatim" then permanently dropped the
tail — often the actual ask. Keep the intent-not-transcription guidance,
but require preserving the at-risk parts of a long current request.
* fix(agent-core): cap foreground shell output to prevent OOM crash
A foreground command that streams a very large or unbounded amount of output (e.g. `b3sum --length 18446744073709551615`) grew the live-output buffer until Node aborted with a JavaScript heap out-of-memory error (exit 134). The per-command output is now capped at 16 MiB: on breach the command is gracefully terminated (SIGTERM -> grace -> SIGKILL) and the result carries a message pointing at redirecting large output to a file. The per-task output ring buffer is also made O(1) per chunk (was O(n^2)), which previously starved the event loop and the foreground timeout. Background/detached tasks are exempt.
* fix(agent-core): stop buffering output after the foreground cap trips
After the 16 MiB foreground ceiling tripped, appendOutput still enqueued every subsequent chunk into the per-task disk write chain during the SIGTERM grace window. A producer that ignores SIGTERM could keep that chain — and the chunk strings each pending write retains — growing until SIGKILL, re-introducing the same out-of-memory risk the cap prevents. Once the cap has tripped, keep only the bounded in-memory ring buffer and stop feeding the disk write chain. Interrupt/timeout capture behaviour is unchanged (they do not set outputLimitTripped).
A raw undici `terminated` error — an SSE/HTTP response body cut mid-flight,
common on long streaming responses — fell through convertAnthropicError to a
generic base ChatProviderError, which isRetryableGenerateError treats as fatal,
so it was never retried. Route raw non-SDK errors through the shared
classifyBaseApiError heuristic (already used by the OpenAI path) so a dropped
stream is classified as a retryable APIConnectionError and retried instead of
failing the turn.
* feat: add KIMI_MODEL_THINKING_EFFORT to force a thinking effort
Send thinking effort only when the model declares it in support_efforts, and add the KIMI_MODEL_THINKING_EFFORT environment variable as an escape hatch to force a specific effort regardless of declared support.
* test: align thinking effort expectations with support_efforts gating
Update the kimi adapter e2e and compaction tests that asserted the previous pass-through behavior on models without support_efforts.
* feat: redesign web ui & add design system
* feat(web): add motion to redesigned UI and add changesets
Animate toast enter/leave, dialog open, and workspace-list and tool-row expand/collapse instead of snapping, and add the changesets covering the web redesign.
* fix(web): remove undo message exit animation
* fix(web): route agent tools to AgentTool and focus dialog on open
- toolRegistry matched the raw 'agent' name, but normalizeToolName folds
agent/subagent into 'task', so agent calls fell through to GenericTool
and lost the inline Open button for the subagent detail panel.
- Dialog's focus watcher only fired on change; callers that mount with
open already true (Login, Settings, ...) never moved focus into the
modal. Run it immediately so initial focus and restore-on-close work.
* feat(web): add logo long-press design-system easter egg
Hold the sidebar logo for 3 seconds to open a dialog showing the design system page. Also trim and rebalance the redesign changesets.
* fix(web): silence Sidebar v-show warning by making it single-root
Nest the design-system Teleport inside the sidebar <aside> so the component has a single element root. App applies v-show to Sidebar, which needs an element root to attach to; the fragment root logged a "non-element root node" warning on every reactive update and the collapse did not take effect.
* fix(web): thinking toggle, tool-group i18n, agent detail button
- Show the default-thinking switch as on whenever thinking is effectively enabled (enabled !== false), matching the core resolver.
- Route the grouped tool-call header and status through vue-i18n.
- Hide the subagent "Open detail" button when no matching task exists (e.g. a completed foreground subagent after a refresh).
* fix(web): use strict equality in agent detail button guard
oxlint eqeqeq flagged the loose != null check; resolveAgentTaskId returns string | undefined, so compare with !== undefined.
* chore(web): remove stray design mockups and screenshots
Remove the design exploration mockups, screenshots, prompts, and notes that were accidentally committed under apps/kimi-web/design. Keep design-system.html, which the sidebar logo easter egg still references.
* fix(web): keep sessions on continuation failure, treat absent thinking as on
- Keep sessions already loaded from earlier pages when a continuation page fetch fails, instead of replacing the workspace with an empty page.
- Treat an absent thinking config as enabled in the settings toggle, matching the core resolver (thinking is on unless explicitly disabled).
* feat(web): open design-system easter egg on 10 logo clicks
Replace the 3-second long-press trigger with 10 consecutive clicks on the Kimi mark; the count resets after a short idle. The long-press was unreliable because pointerleave cancelled the timer on any drift.
* fix(web): treat cancelled swarm members as finished
phaseForTask now lets a terminal status (completed/failed/cancelled) override a stale subagentPhase, so a cancelled swarm member no longer stays live and suppresses the finished AgentSwarm card.
* feat(web): use 1s long-press for design-system easter egg
Switch the logo easter egg back to a long-press, shortened to 1 second, and make it reliable this time: use pointer capture plus touch-action:none so a slight drift no longer cancels the hold.
* fix(web): use plain Spinner for activity notices
ActivityNotice renders for non-chat loading states (e.g. compaction), so it must use Spinner per the design-system rule that reserves MoonSpinner for the chat first-response state.
* docs(web): add a11y guidance to design system
* docs(web): drop stale design README link
* fix(web): restore model search focus and define panel header weight
- Bind the model picker's search Input to searchRef so useDialogFocus moves focus into it on open instead of the dialog's close button.
- Use the defined --weight-semibold token for panel header titles (--weight-bold is not declared, so the shorthand was invalid).
* fix(web): let any open dialog own Escape over the side panel
Track open design-system Dialog instances in a shared count and include it in App.vue's anyOverlayOpen, so a dialog whose open state lives outside App.vue (such as the sidebar session search) captures Escape before the background side panel closes.
* fix(web): base dock-work flag on filtered dock task lists
Foreground subagents are excluded from the dock task lists, so a session whose only task is a foreground subagent no longer renders an empty workbar above the composer.
* fix(web): create subagent task before forwarding text deltas
A client that subscribes from a snapshot after subagent.spawned already fired never received the lifecycle taskCreated; the reducer only applies taskProgress to existing tasks, so assistant text deltas were dropped and the live subagent detail stayed blank. Emit taskCreated (via patchSubagent) before the text progress, mirroring the tool-progress path.
* fix(web): keep plan, swarm, and goal mode toggles per session
Plan, swarm, and goal modes were stored as global scalars on the web client and a single localStorage key each, so they leaked across sessions. Bind them to the active session via per-session maps, persist per session, and apply server status/events to the originating session so background sessions keep independent state.
* fix(web): keep subagent detail reachable for synthesized tasks
When the web client subscribes after a subagent already spawned, the synthesized subagent task has no parentToolCallId, so the Agent tool's Open-detail button was hidden and the panel would not open. Fall back to the single unmapped subagent task when resolving the detail target in both the button visibility and the panel open paths.
* fix(web): keep session kebab menu from being clipped
Teleport the SessionRow kebab menu to body and anchor it with fixed positioning so the collapsing group-sessions list's overflow:hidden no longer clips the dropdown.
* fix(web): apply staged modes to the created session by id
When starting the first prompt, apply the staged plan/swarm/goal modes to the just-created session's per-session maps by id instead of via the activeSessionId-based setters, so a session switch during the selectSession await can't drop the modes for this session or pollute another.
* refactor(web): unify confirmation dialogs into a single modal
Replace the inconsistent confirmation patterns (native confirm(),
two-step menu arming, hand-rolled inline strips, bare buttons) with one
modal ConfirmDialog driven by a global useConfirmDialog() composable,
and consolidate the duplicated confirm/cancel i18n keys.
* feat(web): inline message queue with separate stop button
- Send button always sends/enqueues; interrupt moves to a separate red Stop
button shown only while running, so the two can no longer be confused.
- Queued prompts now render inline at the tail of the transcript (after the
running turn) instead of behind the dock panel: click to edit, remove, drag
the grip to reorder, with image thumbnails and a "next up" marker.
- Remove the dock queue panel and the QueuePane component; Steer stays on
Ctrl/Cmd+S.
* chore: add changeset for web queue UX
* feat(web): prompt reliability, sidebar menu, and composer/markdown polish
- Fix spurious errors when question/approval/task actions were already complete
- Add loading feedback to question and approval prompts; block double-clicks
- Make the question "Other" option selectable by row click and let Enter advance/submit
- Consolidate workspace section actions into an overflow menu
- Tighten markdown prose line-height and block spacing
- Recall input history only when the caret is at text start
* chore(kimi-code): upgrade pi-tui to 0.78.1 and adapt native helpers
Bump @earendil-works/pi-tui from ^0.74.0 to ^0.78.1. pi-tui 0.75.5 replaced its koffi-based Windows VT input with a bundled native helper, and 0.76.0 added a darwin native helper for Terminal.app Shift+Enter.
- SEA build: teach native-deps to collect pi-tui's per-target .node files, drop the koffi registry, and add a native-file-only collect mode so only package.json + the target .node ship (28 -> 2 files).
- Redirect pi-tui's absolute-path native require() into the native-asset cache through the Module._load hook, and extend the native smoke test to actually load the helper.
- npm package: ship pi-tui's native/ directory so macOS Terminal.app Shift+Enter and Windows Shift+Tab keep working for npm installs.
* chore: add changeset for pi-tui upgrade
* chore: vendor @earendil-works/pi-tui 0.80.2
Fork the upstream pi-tui source into packages/pi-tui for local modification. Pristine snapshot of @earendil-works/pi-tui@0.80.2; the apps/kimi-code dependency on ^0.78.1 from npm is left unchanged.
* feat(kimi-code): integrate vendored @moonshot-ai/pi-tui
Replace the npm @earendil-works/pi-tui dependency with the vendored @moonshot-ai/pi-tui workspace package so the fork can be modified locally.
- Point apps/kimi-code imports and native-deps at @moonshot-ai/pi-tui.
- Make pi-tui source-first (exports -> src, publishConfig.exports -> dist, mirroring node-sdk) and strict-clean: bracket access for process.env / named capture groups, an override modifier, and non-null assertions for noUncheckedIndexedAccess.
- Bump the root tsconfig target to ES2024 and enable allowImportingTsExtensions (needed for pi-tui's /v regex and .ts imports, which node --test requires).
- Add packages/pi-tui to flake.nix workspaces and exclude the vendored source from oxlint.
* fix(pi-tui): export package.json for native asset resolution
The SEA native-asset collector resolves the package root via
require.resolve('@moonshot-ai/pi-tui/package.json'). The vendored
package's exports field only exposed ".", which blocked the
"./package.json" subpath and broke build:native:sea with
ERR_PACKAGE_PATH_NOT_EXPORTED.
* chore(kimi-code): sync pi-tui native prebuilds at build time
Copy packages/pi-tui/native prebuilds into apps/kimi-code/native
during build instead of tracking a manual copy in git. Only the
.node prebuilds are copied (not the C sources); the directory is
now a build artifact covered by .gitignore.
* fix(pi-tui): avoid destructive full redraw during streaming
When the first changed line is above the viewport, the differential
renderer fell back to fullRender(true), which clears scrollback and
yanks the user's viewport. On Windows Terminal this jumps to the
absolute top (microsoft/Terminal#20370).
Clamp the diff to the visible viewport when content length is
unchanged (spinner tick / markdown reflow above the viewport), so
streaming no longer triggers a full redraw in those cases. Length
changes still fall back to fullRender to reset the viewport.
* fix(kimi-code): update pi-tui imports in files merged from main
Two files added on main (effort-selector, plugin-command) still
imported the old @earendil-works/pi-tui package name; point them at
the vendored @moonshot-ai/pi-tui.
* chore(nix): update pnpmDeps hash after lockfile regen
* fix(pi-tui): clamp above-viewport diff even when content shrinks
Previously, when the first changed line was above the viewport and
content length changed (e.g. spinner removed at end of streaming), the
renderer fell back to fullRender(true), which clears scrollback and
yanks the viewport to the absolute top on Windows Terminal.
Always clamp the diff to the visible viewport instead, preserving the
user's scroll position. Stale bytes remain in scrollback but are not
visible.
* fix(kimi-code): keep activity placeholder to avoid streaming shrink
When streaming ends, removing the activity spinner shrank the content
by two rows, which (combined with transient→final code highlighting
above the viewport) triggered a destructive full redraw. Keep a one-row
placeholder in the activity pane when idle so the content does not
fully shrink.
* chore: refine streaming scroll changeset wording
* chore: remove obsolete pi-tui native helpers changeset
* chore: add pi-tui changesets and document the pi-tui changelog rule
Add changesets for the fork integration, package manifest export, and
viewport clamp fix so the vendored pi-tui keeps its own changelog.
Update the gen-changesets skill to treat @moonshot-ai/pi-tui as a
special internal package that lists itself instead of the CLI, with a
separate CLI changeset only when the change is user-visible there.
* chore(nix): update pnpmDeps hash after merging main
* fix(changeset): drop private kimi-code-docs from google-genai changeset
* chore(telemetry): track conversation undo, shell mode, and effort changes
- conversation_undo: fires when an undo reverts history (double-Esc selector, /undo, /undo N)
- shell_command: fires when a ! shell command runs in the TUI
- thinking_toggle: now fires on any effort change (not just on/off flips) and carries { enabled, effort, from }; align the no-session TUI path to the same payload
* chore(telemetry): track conversation_undo in core undoHistory
Tracking it in the TUI only covered the TUI; web/acp/REST undos reach
core.rpc.undoHistory directly and were missed. Emit the event from the
agent undoHistory RPC after the undo succeeds so every host is covered,
and drop the now-redundant TUI track.
Addresses review feedback on #1271.
* feat: add model alias overrides
Preserve user model overrides across provider catalog refreshes and resolve effective model metadata for runtime, TUI, protocol, and ACP consumers.
* fix: apply model display name overrides
Show overridden model display names in the footer, welcome panel, status output, and model switch confirmations.
* fix: pass through kimi effort when undeclared
Keep support_efforts authoritative when declared, but pass requested Kimi thinking effort through when the model does not declare support_efforts.
* fix: honor model overrides in effort commands
Use effective model metadata for /effort choices and for always_thinking clamping when resolving thinking effort.
* feat(agent-core): compress oversized images before sending to the model
Downsample images to a 2000px longest-edge and per-image byte budget at the
single prompt-ingestion chokepoint (the prompt/steer RPC) and on tool results
(ReadMediaFile, MCP), so every client transport — CLI, web, desktop, ACP, SDK —
is covered uniformly inside the core. PNG screenshots stay lossless and only
degrade to JPEG when the byte budget cannot otherwise be met. Best-effort: the
original image is sent unchanged if compression fails.
* fix(agent-core): serialize prompt/steer RPCs to avoid a turn-claim race
The prompt/steer RPC handlers await image compression before turn.launch()
synchronously claims the active turn, so two overlapping calls could both
compress first — letting the faster-to-compress one win the turn and strand the
other on agent_busy. Run these two RPCs through a per-agent serialization chain
so they claim in submit order; cancel and the other RPCs stay immediate.
* fix: update flake.nix pnpmDeps hash for the jimp dependency
Adding jimp to the workspace changed pnpm-lock.yaml, so the pnpmDeps
fixed-output hash was stale and the nix build failed. Update it to the value
the CI nix build reported.
* fix(agent-core): guard image compression against decompression bombs
A tiny-byte, huge-dimension image (e.g. a solid 30000x30000 PNG) would be fully
decoded into a multi-gigabyte bitmap by Jimp before any resize — an OOM vector
the byte budget never catches. Skip compression when the sniffed pixel count
exceeds MAX_DECODE_PIXELS (~100 MP), before the decode; oversized images pass
through uncompressed as they did before compression existed.
* fix(agent-core): cap decode byte size before compressing images
Compression runs before downstream size caps (e.g. the 10MB MCP per-part
limit), so a huge or invalid base64 image from an MCP tool was Buffer.from-
decoded — and handed to Jimp — just to be dropped afterward. Add a
MAX_DECODE_BYTES ceiling (64MB, overridable) checked before the base64 decode
and before Jimp, the byte-side complement to the pixel-count guard; oversized
payloads pass through uncompressed.
* refactor(agent-core): compress images at ingestion, not on the turn RPC
Move image compression off the prompt/steer RPC path and back to each ingestion
site (CLI paste, server upload resolution, ACP conversion; ReadMediaFile and MCP
already compressed at their producers). Compressing on the RPC control path put
an async step before the synchronous turn-claim, which spawned a series of
races: prompt/steer interleaving, and — with a cancel arriving mid-compression —
an ineffective abort that let a cancelled prompt launch anyway.
Treating compression as a pure input-stage transform (done while the content
part is built, before it ever enters the agent loop) removes those races
structurally: rpc.prompt/steer are plain synchronous handlers again, and the
serialization/cancel-window machinery is gone. Records stay compressed, resume
stays consistent, and coverage degrades gracefully (a new client that skips
compression just sends a larger image, as before this feature).
* fix: compress inline base64 prompts and honor ACP cancels mid-compression
Two contained ingestion-site follow-ups:
- server: resolvePromptMediaFiles now also compresses images submitted as an
inline `{ kind: 'base64' }` source, not just uploaded files, so the REST
inline-base64 path gets the same downsampling.
- acp-adapter: AcpSession tracks a pending-abort flag while prompt() awaits
image compression (before any turn exists). A session/cancel in that window
flips it, so the prompt returns `cancelled` instead of launching a turn the
client already stopped.
* fix(acp-adapter): cover all concurrent pre-turn prompts on cancel
The pending-abort marker was a single session field, so with two
`session/prompt` requests compressing large inline images at once the later
one overwrote it and a `session/cancel` could mark only one — the other
launched after the client had cancelled. Track a token per in-flight prompt in
a set and flip them all on cancel so every pre-turn prompt is covered.
* chore(node-sdk): declare jimp as a devDependency
The SDK re-exports the image compressor, whose lazy `import('jimp')` (inside
the bundled agent-core code) is inlined into the published dist. jimp was
resolved only transitively via agent-core, so declare it as an explicit build
input here — matching the CLI — to make the bundling reliable rather than
phantom. It stays a devDependency: jimp is bundled, not a runtime dependency.
* fix(provider): honor base_url for google-genai and vertexai providers
The google-genai and vertexai provider types silently ignored a configured
base_url and always hit generativelanguage.googleapis.com (e.g. a Gemini-
compatible proxy URL + key could not be used). Plumb the endpoint through to
the @google/genai SDK via httpOptions.baseUrl:
- kosong: add baseUrl to GoogleGenAIOptions and inject it into the client's
httpOptions alongside the existing headers (the SDK merges headers and
overrides the base host).
- agent-core: forward provider.baseUrl in the google-genai and vertexai
branches, with GOOGLE_GEMINI_BASE_URL / GOOGLE_VERTEX_BASE_URL env
fallback. vertexai keeps deriving location from an aiplatform host.
- docs: document base_url for both providers, noting the host root only
must be given because the SDK appends the API version itself.
Covered by unit tests asserting the URL reaches the kosong config and the
SDK client's httpOptions.
* fix(provider): use the effective base_url for vertex location detection
The vertexai branch forwarded the endpoint from config `base_url` OR the
GOOGLE_VERTEX_BASE_URL env fallback, but service-account detection
(`hasVertexAIServiceEnv` / `vertexAILocation`) still derived the region from
`provider.baseUrl` only. Supplying the regional endpoint via the env fallback
(with a project but no explicit GOOGLE_CLOUD_LOCATION) therefore left location
undefined and silently downgraded Vertex ADC to API-key Gemini routing.
Resolve the effective base URL once and use it for both forwarding and location
derivation, so the env fallback behaves exactly like `base_url`. Add a
changeset for the kosong + agent-core patch release.
* feat(agent-core): slim WebSearch to query-only; fetch page content via FetchURL
The coding search backend no longer honors `limit`/`enable_page_crawling` and
always returns full page content, which was token-heavy and frequently
truncated. Realign the web tools around a search+fetch split:
- WebSearch request sends only `text_query`; the tool exposes only `query`.
- Search results drop inline page content and add the source site; full page
content is now fetched on demand via FetchURL.
- Add a citation reminder to both WebSearch and FetchURL results (in the
FetchURL front note so it survives body truncation).
- Update tool descriptions and reference docs accordingly.
* fix(agent-core): satisfy no-base-to-string lint and drop redundant | undefined
- Assert the exact serialized WebSearch request body instead of String()-coercing
a BodyInit, fixing the type-aware no-base-to-string lint error.
- Drop redundant `| undefined` from WebSearchResult optional fields per AGENTS.md.
- Add changeset.
* chore(changeset): bump agent-core to minor for WebSearch input-contract change
Removing the `limit`/`include_content` tool inputs tightens a closed
(`additionalProperties: false`) schema, so previously-valid args are now
rejected — an incompatible change for a released package. Bump minor rather
than patch.