- text-mode `tool.progress` output is suppressed when `--final-message-only` /
`--quiet` is set, matching the minimal-output contract (it bypassed the
final-only writer and wrote every chunk to stderr).
- add kitchen-sink final-only invariant tests (stream-json and quiet) that feed
every in-turn event kind and assert stdout is exactly the final message with a
silent stderr — one guard that catches any direct-write leak at once.
Add a `Record<Event['type'], StreamJsonRoute>` coverage table that classifies
every protocol event by how print mode's stream-json output treats it. Because
it is keyed by the full event union, adding a new event type fails `typecheck`
until it is explicitly classified, so print mode can never silently drop a new
event. A companion test keeps the notification/activity/tool_progress rows in
sync with the mappers in run-prompt.ts (now exported for testing).
- `tool_progress` JSON now carries `custom_kind`/`custom_data`, so stream-json
clients keep structured `kind: "custom"` payloads (e.g. the MCP OAuth
authorization URL) instead of an opaque `{"kind":"custom"}`.
- The headless `/goal` summary is suppressed on stdout under
`--final-message-only`, matching the single-final-assistant-message contract
(the goal status is still conveyed by the exit code).
The exit drain still waits for running background tasks, but no longer emits
their `background.task.terminated` JSON in `--output-format stream-json
--final-message-only`, preserving the single-final-assistant-message contract.
- `--quiet` with neither `--prompt` nor `--input-format` now fails validation
("Quiet mode requires --prompt or --input-format.") instead of entering print
mode and crashing on an undefined prompt.
- `PromptFinalJsonWriter.finish()` skips emitting when there is no final text,
matching the text writer — a failed `--final-message-only` turn no longer
prints a spurious `{"role":"assistant","content":""}` before its error line.
Adds a changeset for the print-mode stream-json work.
Surface the activity layer the TUI renders on stdout as JSON so the
stream-json output is both complete and entirely JSON:
- live tool output as `{"type":"tool_progress",...}` (moved off stderr in
stream-json mode; text mode keeps the stderr chunk)
- subagent lifecycle as `{"type":"subagent","event":...}`
- `{"type":"warning"|"skill_activated"|"mcp_server_status"|"compaction"|
"goal_update"|"agent_status"|"tool_list_updated",...}`
Activity events flush the pending assistant message first to preserve
ordering, and are emitted only in stream-json mode. Adds prompt-runner
tests and updates the kimi-command reference docs.
- emit model thinking as its own `{"role":"assistant","type":"thinking"}` JSONL line
- add `--input-format text|stream-json` (multi-turn JSON prompts over stdin)
- add `--final-message-only` and the `--quiet` shorthand
- surface background-task/cron events as `{"type":"notification",...}` lines
- report turn failures as `{"type":"error",...}` JSON and map retryable
provider errors to exit code 75 (keeps stream-json stdout entirely JSON)
- wait for background tasks before exit, gated by background.keepAliveOnExit,
bounded by background.printWaitCeilingS
Adds CLI option/validation tests, prompt-runner behaviour tests, and updates
the kimi-command reference docs (en/zh).
Align outlier telemetry events with the conventions already used by
tool_call, api_error, permission_approval_result, and the plan events:
- duration / latency_ms / duration_s -> duration_ms
- success boolean -> outcome enum ('success' | 'error')
- bare type -> provider_type
The exit event switches from seconds to milliseconds to match the
duration_ms convention, so its numeric scale changes accordingly.
* feat(web): preserve open side panel across session switches
* feat(web): scope composer input history to current session
* fix(web): suppress side panel open animation on session switch
* fix(web): preserve per-session scroll position on session switch
* chore: add changeset for per-session scroll position
* fix(web): preserve follow-bottom state per session
* fix(web): scope composer attachments to their session
* fix(web): restore saved scroll position on session switch
* feat(web): play a sound when a turn completes
Synthesize a short chime when a session finishes a turn. Opt-in via Settings -> Notifications (off by default); the audio context is unlocked on the first user gesture so it also plays while the tab is backgrounded.
* feat(web): notify and play a sound when a question needs an answer
Reuse the existing notification/sound toggles so they also fire when the agent asks a question (the awaiting-answer state). Generalize the Settings labels to cover both cases.
* fix(web): don't queue the chime on a suspended audio context
A suspended AudioContext has a frozen clock, so tones scheduled on it would play stale when the context later resumes (e.g. on the next click). Only schedule the chime when the context is actually running; if it is still suspended, try to unlock it for next time and skip this one.
* fix(web): gate question notifications behind explicit opt-in
Question notifications surface question text, so they must not fire for users who only opted into turn-completion alerts (which default on). Split question notifications into their own persisted preference that defaults off, with a separate Settings toggle. Completion notifications keep their existing default-on behavior.
* fix(web): show the question text in question notifications
Lead with the actionable question text in the desktop notification body, keeping the short header as context (e.g. 'Storage: Which database?'). Previously the header alone was shown, so users had to open the tab to learn what was being asked.
Remove the NewSessionDialog path so every new-session entry in the web UI enters the onboarding composer, creating the session only when the first message is sent. This removes the last web flow that produced an empty session.
* chore(web): remove the /sessions slash command
* feat(web): hide empty sessions from the session list
Add an optional exclude_empty parameter to the session list API; the web client passes it so unused "New Session" entries are hidden by default, with pagination and has_more computed on the filtered set.
* fix(protocol): add exclude_empty to the session list query schema
Keep the shared protocol schema in sync with the server route so clients using the protocol type see the new parameter.
* fix(protocol): keep exclude_empty off the child session list schema
listSessionChildrenQuerySchema aliased the main list schema, so it inherited exclude_empty even though the /sessions/{id}/children route does not filter by it. Split it so generated clients are not misled.
* fix(agent-core): recover from context overflow 413
- track provider-observed effective context limit after overflow
- compact with the reduced limit before retrying the turn
- treat large plain 413 responses as recoverable context overflow
- add CLI patch changeset
* feat(managed-kimi-code): support Anthropic-compatible protocol
- switch managed provider to anthropic when models declare anthropic protocol
- add base64 video content blocks to the kosong anthropic provider
- downgrade unsupported media parts to text placeholders by capability
- pass prompt cache key as Anthropic metadata.user_id for session affinity
* feat(agent-core): add protocol/type to request and video upload telemetry
- turn_started now carries `type` (configured provider wire type) and
`protocol` (effective transport, i.e. alias.protocol ?? provider.type)
- new video_upload event reports mime type, size, latency and
success/failure, plus type/protocol/model context
- ResolvedRuntimeProvider gains `type` and `protocol` fields
When the first message of an empty session is submitted, the optimistic
user turn unmounts the empty-session composer before the post-flush text
watcher can persist the cleared draft. The docked composer then mounts
and reloads the stale text from localStorage.
Clear the persisted draft synchronously in the submit / steer / slash
command paths instead of relying on the text watcher, so the next mount
always starts empty.
* feat: cap completion tokens to remaining context window for chat-completions
* test: cover dynamic completion budget for kimi and openai-legacy
* fix: leave compaction budget uncapped to avoid one-token summaries
* fix: add hookCount to plugins-selector test mocks
* perf(tui): cache rendered message lines across frames
Cache render(width) output in the transcript container and message components, returning cached lines when content, theme, and width are unchanged. Removes the per-frame full-transcript re-render that caused the TUI to lag as history grew.
* perf(tui): bound transcript with sliding window and step merging
Keep the TUI responsive as conversations grow by bounding the live
transcript:
- Sliding window: keep only the most recent 50 turns in the component
tree; older turns are destroyed (entry + component).
- Step merging: within each turn, keep only the most recent 30
thinking / tool steps rendered; older ones collapse into a summary.
- Expand (Ctrl+O) only reaches the most recent 3 turns.
All thresholds are overridable via KIMI_CODE_TUI_* env vars; 0
disables the corresponding feature.
* chore: add changeset for tui transcript window
* chore(tui): remove KIMI_TUI_PERF render timing log
* fix(tui): show the server token when handing off via /web
The /web slash command opened the session deep link without the bearer token, so the web UI was not authenticated and the token was never shown, unlike the kimi web subcommand. Resolve the persistent server token, append it as the #token= fragment so the browser signs in on load, and show it in green below the status line so it can be copied before the terminal exits.
* test(plugins-selector): add required hookCount to fixtures
PluginSummary/PluginInfo gained a required hookCount field, so the app's typecheck failed on fixtures that did not provide it. Add hookCount: 0 to the test summaries (none of these fixtures declare hooks).
* feat(web): auto-grow composer and add expandable editing mode
- Grow the chat textarea with its content up to a 1/4-viewport cap.
- Add an expand toggle above the send button for a taller editor; in that
mode Enter inserts a newline and Cmd/Ctrl+Enter or the button sends,
then the editor collapses back.
* fix(web): reset expanded composer state on session change
The composer instance is reused across sessions (not keyed by session id), so the expanded preference leaked into the next session's draft, leaving it stuck in the tall editor with Enter inserting newlines. Collapse back when the active session changes.
* fix(web): match expand-toggle threshold to theme resting height
The modern/kimi global theme overrides the composer min-height to 40px (the scoped default is 56px), so a hard-coded 56px threshold kept the expand toggle hidden until a third line under the default theme. Read the computed min-height from the element instead.
* fix(web): recompute expand-toggle visibility after collapsing
While expanded the computed min-height is 70vh, so a multi-line draft measured there sets isGrown=false. Collapsing did not recompute it, hiding the toggle even though the collapsed draft was still multi-line. Recompute growth after every toggle via a shared helper. The expanded state itself is unchanged and stays at 70vh until toggled or sent.
* fix(web): collapse expanded editor on slash-command submit
Known slash commands return early from handleSubmit, above the post-send collapse, so sending an expanded /goal, /btw, /compact, or skill command left an empty 70vh editor. Collapse in the slash-command path too.
* fix(web): refocus textarea after toggling expand
Clicking the expand toggle leaves focus on the button, so subsequent keystrokes do not reach the textarea and Enter would activate the button again instead of inserting a newline. Return focus to the textarea after toggling.
* fix(web): refit textarea when collapsing after image-only sends
When the expanded editor collapses on an image-only send, the text is already empty so the draft watcher never re-runs autosize; the textarea kept the inline height measured at 70vh and the collapsed cap left an oversized empty box. Route all send/steer collapses through a helper that re-runs autosize after the 70vh min-height is removed.
---------
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
- add `kimi server run --allowed-host <host...>` (repeatable or
comma-separated; leading dot matches a domain suffix) and thread it
through daemon spawn into startServer
- merge CLI allowed hosts with KIMI_CODE_ALLOWED_HOSTS for both the HTTP
and WebSocket Host checks
- include the rejected host and allow guidance in the 403 error message
* feat(agent-core): support per-hook cwd and env in HookEngine
* feat(agent-core): support hooks in plugin manifest and aggregate via PluginManager
* feat(agent-core): merge plugin hooks into session hook engine
* chore: add changeset for plugin hooks
* feat(agent-core): strengthen default system prompt
Add high-confidence, prompt-only guardrails to the default agent system prompt:
- Personality/candor: extend the HELPFUL/CONCISE/ACCURATE line with CANDID, and
require plainly stating what could not be run, reproduced, or verified.
- Reminders: avoid cheerleading; voice evidence-based disagreement; deliver
complete code with no placeholders; update now-stale comments/docstrings after
a change; re-check the user's latest request before finalizing a reply.
- Context Management: explain automatic compaction — continue from the summary,
re-establish transient state with tools, do not restart from scratch.
- Output formatting: replies render as Markdown in the terminal; keep lists flat;
no emojis unless the user uses them first.
- Project Information: frame injected AGENTS.md as project context, not a
privileged instruction channel that can override system rules.
Prompt text only; no code or template-variable changes.
* feat(agent-core): hoist key working rules into the system prompt
Lift a few high-leverage rules from individual tool descriptions up into the
default system prompt, so they shape default behavior before any specific tool
is in play (kept terse and integrated, not bolted on):
- Planning: for multi-step or multi-file work, maintain a `TodoList` (one item
in_progress, mark done as it finishes) and prefer `EnterPlanMode` first when
the approach isn't settled.
- Default to making progress, not asking: once the goal is clear and sanctioned,
carry it through and work blockers yourself; ask only when the answer would
change the next step. Explicitly does not override stopping to discuss an
unclear goal or waiting for go-ahead before writing code.
- Tool routing: prefer dedicated tools (Read/Glob/Grep/Write/Edit) over raw
shell when one fits; keep Bash for genuine shell work.
- Definition of done: verify with the checks that cover the change before
marking it complete, independent of whether a TodoList is in use.
- Delegation: explore subagents also keep intermediate file contents out of your
own context — you get a conclusion back, not a pile of dumps.
Prompt text only; no code or template-variable changes.
* fix: clarify guidelines for file pattern matching and tool usage in explore.yaml and system.md
* fix(agent-core): hide the Skills section from agents without the Skill tool
Subagents (coder/explore/plan) inherit the root system prompt but lack the Skill tool, yet KIMI_SKILLS was rendered unconditionally — leaking the full skill listing into agents that cannot invoke any skill. Gate KIMI_SKILLS on the profile's tool set and wrap the '# Skills' section in {% if KIMI_SKILLS %} so it disappears for those profiles.
Also note in the Working Directory section that Bash enforces none of the workspace/secret-file guards, so the model must hold that discipline itself.
Tests: assert the Skills section renders for the root agent and is absent for Skill-less subagents; update the prompt-rendering fixtures for the new gating.
* fix(agent-core): gate Agent, background-task, and TodoList guidance by tool availability
Subagents (coder/explore/plan) inherit the root system prompt but lack the Agent, TaskList, and TodoList tools, so they were shown usage guidance for tools they cannot call. Derive HAS_AGENT/HAS_TASKLIST/HAS_TODOLIST from each profile's tool set and gate those sections with inline {% if %}, so they render only for agents that hold the tool.
Root rendering is byte-identical (the inline tags collapse to the original text when the flag is set). The cross-tool secret-file guard stays shared, since explore/plan still hold Read/Grep/Glob.
Tests: assert the gated guidance is present for the root agent and absent for explore/plan, while the shared secret-file guard remains.
* refactor(agent-core): move Agent-delegation and Glob-anchor guidance into the tool descriptions
The Agent-delegation paragraph in the system prompt duplicated mechanics already documented on the Agent tool itself (new-vs-resume, zero-context briefing, foreground default / run_in_background threshold), so remove it. HAS_AGENT still gates the explore-delegation bullet, which carries the 'when to delegate' nudge the tool description deliberately omits.
Move the proactive 'anchor the pattern up front' guidance into the Glob tool description (it previously only described the reactive 'refine after hitting the cap' path) and drop the now-redundant Glob bullet from the system prompt.
Tests: drop the assertions tied to the removed Agent paragraph; HAS_AGENT gating stays covered via the explore bullet.
* test(agent-core): add guidance for blast-radius and concrete examples in agent profiles
* docs: update descriptions for skill-tool and fetch-url; enhance web-search citation instructions
* feat(agent-core): disclose enforced constraints in tool descriptions; fix GetGoal field doc
Surface runtime-enforced behavior in the Agent / AgentSwarm / AskUserQuestion / Goal
tool descriptions so the model learns the rules from the tool, not from a failed call:
- Agent: resuming excludes subagent_type (setting both is rejected)
- AgentSwarm: at least 2 items unless resuming, prompt_template required and must
contain {{item}}, distinct resulting prompts; plus Agent-vs-AgentSwarm fan-out note
- AskUserQuestion: result is {answers}; an empty answers with a dismissal note means
the user declined — fall back to best judgment instead of re-asking
- CreateGoal: creating fails when a goal already exists (use replace)
- SetGoalBudget: state the hard 1s-24h time-budget band
- UpdateGoal: do not mark blocked merely because work is hard/slow/incomplete
- GetGoal: drop the advertised self-report / evaluator-verdict fields — GoalSnapshot
never held them, so the tool never returned them
Each change is covered by a description assertion.
* fix(agent-core): soften AskUserQuestion answers-keying wording to match the code
The answers object is passed through from the host/RPC layer (QuestionAnswers is
Record<string, string | true>); this code does not key it by question text. Describe
what the keys identify instead of asserting a guarantee the code does not provide.
* feat(agent-core): tighten Bash/Grep/Write/Edit tool descriptions
- Bash: prefer the cwd argument (or absolute paths) over a cd from an earlier
call, since each call runs in a fresh shell
- Grep: note that files_with_matches is ordered most-recently-modified first
- Write: do not create documentation/README files unless the user asks
- Edit: frame replace_all with its rename-across-file use-case
Each change is covered by a description assertion.
* feat(agent-core): refine plan-mode/todo/cron tool descriptions
- ExitPlanMode: describe what a good plan contains (specific, verifiable steps
grounded in the codebase, not vague filler)
- TodoList: stop calling it useful 'in Plan mode' — plan-mode planning goes to
the plan file; TodoList tracks execution progress
- CronCreate: warn that a one-shot whose pinned day/month already passed this
year is rejected; document the 50-task session cap and the 8 KiB prompt cap
- CronCreate: drop the bench-only KIMI_CRON_NO_STALE / KIMI_CRON_NO_JITTER env
knobs from the model-facing description (CI-only; the model never sets them)
Each change is covered by a description assertion.
* refactor(agent-core): dedupe ExitPlanMode options docs into the param schema; trim EnterPlanMode workflow
- ExitPlanMode: the options field mechanics (label format, recommended, count,
single-option=plain-approval, reserved labels) now live only in the options
param describe; the tool description routes to it and keeps the yolo/manual UI
behavior it uniquely documents. The options consistency test now enforces a
single source of truth (describe) plus the schema-consistency guard, instead
of requiring the same facts in both surfaces.
- EnterPlanMode: trim the duplicated 'What Happens in Plan Mode' steps to a
pointer (the full workflow is injected unconditionally once plan mode is
active), keeping the explore-subagent recommendation.
* fix(agent-core): correct prompt/code inaccuracies found in the final audit
Every item below was re-verified against the live code:
- Skill: drop the never-fired recursion-depth cap (production never seeds depth);
keep the <kimi-skill-loaded> 'already loaded, don't re-invoke' guard
- TaskOutput: terminal_reason can also be `failed`, not just timed_out/stopped
- Grep: count_matches emits per-file `path:count`, with the total reported separately
- Plan mode: the reminder names TaskStop/CronCreate/CronDelete as blocked (they are
hard-denied by plan-mode-guard-deny)
- Bash: the failure trailer is non-zero-exit-specific; timeout/interrupt differ
- CreateGoal: replace also covers a blocked goal, not just active/paused
- UpdateGoal: it also injects the completion/blocked outcome prompt, so it does more
than 'only record the status'
- FetchURL: state the universal http/https contract instead of provider-internal SSRF
and 10 MiB limits (the primary Moonshot fetcher enforces neither)
- TodoList: query mode triggers on omitting `todos`, not on zero args
- TaskList: command/PID/exit code are shell-task fields only
- CronCreate: the returned fields include `cron`
- SetGoalBudget: turn/token budgets are rounded up to >= 1, not rejected below 1
Each change is covered by a description/param assertion; plan.test.ts snapshots
refreshed for the longer plan-mode reminder.
* fix(agent-core): gate prompt tool guidance on runtime availability, not declared profile tools
The HAS_* / Skills gating computed flags from the profile's declared tools, but
Agent/AgentSwarm only register when a subagentHost exists (ToolManager
.initializeBuiltinTools). A runtime built without a subagentHost (e.g. direct SDK
construction) therefore rendered the explore-delegation guidance for an Agent
tool the model could not call.
SystemPromptContext now carries an optional availableTools; buildTemplateVars
gates on it when present and falls back to the declared tools otherwise. useProfile
passes the profile tools minus Agent/AgentSwarm when no subagentHost is wired, so
the render reflects what the model can actually call. The normal session path
(subagentHost always defaulted) is unchanged.
* fix(agent-core): exempt the plan-mode plan file from the Write *.md ban
Plan mode writes its plan to plans/<id>.md (plan/index.ts) and the reminder tells
the model to create it with Write when missing, which contradicted Write's blanket
'do not create *.md unless asked' guard. Carve the plan file out of the ban.
* fix(agent-core): scope plan-mode prompt guidance and the Write *.md ban to runtime reality
- Gate the TodoList bullet's "enter plan mode via EnterPlanMode" suggestion
on a new HAS_ENTERPLANMODE flag. A custom profile that keeps TodoList but
drops EnterPlanMode no longer steers the model toward a tool it cannot call;
the default profile render is unchanged.
- Reframe the Write *.md prohibition around intent (unsolicited docs) instead
of a blanket extension ban, so artifacts a task or project instruction
requires — the plan-mode plan file, a repo-mandated changeset — are no
longer contradicted by the tool's own rules.
* refactor(agent-core): move tool-coupled guidance into tool descriptions
The default system prompt carried tool-usage guidance behind {% if HAS_* %}
gates that re-derived, in prose, the availability the tool schema already
encodes — and the same guidance was duplicated in each tool's own
description. Drop the four gated blocks (background Bash, Agent/explore,
TodoList, EnterPlanMode) and the compaction TaskList/TodoList bullets; the
tool descriptions, shipped only when the tool is registered, already carry
the same instructions, so subagents and tool-trimmed profiles are no longer
pointed at tools they lack.
Fold the two genuinely unique lines into the tool descriptions: bash.md
gains "return control after starting a background task", agent.md gains the
context-hygiene reason to delegate. Collapse the compaction bullets into one
tool-agnostic sentence. Remove the now-unused availableTools / HAS_* render
machinery.
* fix(skill-tool): clarify no-reinvoke guard and argument handling in tool description
* feat(fetch-url): indicate content retrieval mode in output for better model context
* fix(agent-core): correct goal-budget rounding and task-output failure docs
set-goal-budget.md said turn/token budgets are "rounded up", but the code uses
Math.round — say "rounded to the nearest whole number" instead. task-output.md
implied every failed task carries terminal_reason/stop_reason, but a plain
non-zero command exit carries only status plus exit_code; describe that exit_code
path and reserve terminal_reason for non-exit endings (timeout, explicit stop,
or an internal error with no exit code).
* fix(agent-core): scope free-work guidance by role and steer one-shots near-term
The blast-radius paragraph told every profile that local work — including
editing files — may be done freely, but the read-only explore/plan subagents
render it too; scope it to "work your role permits" so it no longer undercuts
their read-only constraints.
The one-shot cron guidance leaned on a year-boundary heuristic ("avoid a
day/month already passed this year") that misfires across Dec 31 to Jan 1 and
duplicated a limit the code already enforces. Replace it with a plain near-term
nudge and leave the hard future-window guard in code.
* fix(enter-plan-mode): clarify availability of Agent tool in plan mode description
* fix(agent-core): surface Grep count_matches total and pagination in output
count_matches put the aggregate "Found N occurrences" summary and the
"Results truncated... use offset=N to see more" notice on the result's
message field, which normalizeToolResult drops before the result reaches the
model. The model saw only the path:count lines and could miss the total and,
worse, the pagination cue — so it would not know to page through truncated
counts. Append both to output after the path:count lines, the same way the
content and files_with_matches modes already inline their notices.
* fix(grep): reorder count summary and results in output for clarity
* chore(changeset): consolidate prompt-hardening changesets into one
Squash the five per-change changesets for this PR into a single concise
entry; they all bump @moonshot-ai/kimi-code (patch) for the same
system-prompt and tool-description hardening work.
* fix(agent-core): stop the agent from blocking on background tasks
Both the Agent and Bash background-launch messages invited the model to "peek
at progress" via TaskOutput, and the foreground-vs-background guidance had been
thinned to a single parameter hint. Together that led the model to launch a
background subagent and then immediately wait on it through TaskOutput —
defeating the point of background execution.
Make both launch messages take the same anti-wait stance the user-detach path
already uses (do NOT wait, poll, or call TaskOutput on it), restore
foreground-by-default guidance in the Agent background description (run in the
background only when you have other work and do not need the result to proceed),
and add a TaskOutput backstop against using it to sit and wait. Also fold the
fix into the consolidated changeset.
Register a `kimi update` alias for the existing `kimi upgrade` command via commander's .alias(), so both forms run the same upgrade flow. Document the alias in the command reference and add a routing test.
* feat(feedback): support attaching logs and codebase
Add an attachment picker to /feedback (none / logs / logs + codebase).
Codebase uploads scan the working directory with sensitive files excluded
and are sent through a new multipart upload API on the oauth/node-sdk layers.
* fix(feedback): fall back to logs when codebase scan fails
* tiny fix
* fix(feedback): make diagnostic uploads partial-safe
* refactor(feedback): reuse harness session export and normalize upload url types
* docs(slash-commands): note optional feedback attachments
* refactor(feedback): reorganize feedback upload modules
Move the attachment orchestration out of tui/commands/info.ts into a
dedicated feedback/feedback-attachments.ts, and split the former
codebase-upload/attach.ts into a generic multipart uploader
(feedback/upload.ts) and an archive lifecycle module
(feedback/archive.ts). Both session and codebase archives now flow
through a single upload lifecycle, which also removes the temp-dir
leak that occurred when codebase packaging failed.
Rename FeedbackCodebaseArchive to FeedbackArchive and the
codebase-upload/ directory to codebase/ so module boundaries match
their actual responsibilities (scan + package only).
* ci: run unit tests on windows
* fix(migration-legacy): align workdir bucket key with agent-core
computeWorkdirBucket used a local node:path-based resolve that yields backslash-separated paths on Windows, while agent-core's encodeWorkDirKey uses pathe (forward slashes on every platform). The SHA-256 inputs diverged, so migrated sessions were written to a bucket that the session picker never reads, making them invisible on Windows.
Alias computeWorkdirBucket to encodeWorkDirKey so both sides stay byte-identical, drop the local slugify copy, and update the workdir-bucket test reference accordingly.
* test(acp-adapter): expect platform-native separators in e2e-fs path
The e2e-fs test asserted the fs/readTextFile wire path as the raw POSIX targetPath, but AcpKaos.toClientPath converts '/' to '\' when the inner LocalKaos reports pathClass 'win32' (Windows). On Windows the wire path became '\Users\test\x.ts' and the assertion failed.
Mirror toClientPath in the test: expect backslash separators on win32 and the raw path otherwise. Implementation is unchanged.
* test(sdk): normalize workDir and skillDir paths in session tests
SessionStore.create/list and the skill loader normalize paths through pathe (forward slashes). The SDK tests compared the resulting workDir and skill loaded-dir against raw mkdtemp / node:path strings, which use backslashes on Windows (and node:fs realpath also returns backslashes for the skill dir), failing three toMatchObject assertions.
Build the expected paths with agent-core's normalizeWorkDir so they match the internal pathe representation on every platform. The skill dir keeps its realpath() (the loader realpaths the root) and only normalizes separators.
* test(skill): normalize realpath to forward slashes in scanner tests
resolveSkillRoots normalizes every root.path through fs.realpath followed by replacing backslashes with forward slashes (scanner.ts). The scanner tests compared root.path against node:fs realpath directly, which returns backslashes on Windows, so twenty assertions failed (toEqual / toContain / toHaveLength) even though the resolved paths were identical.
Wrap realpath at the top of the test file to mirror the implementation's normalization, so every comparison uses the same forward-slash form on every platform.
* test: skip Unix-only permission tests on Windows
The Unix file-permission assertions (mode bits like 0o600 / 0o700 and chmod 000 making a path unreadable) have no equivalent on Windows, which uses ACLs; fs.chmod there can only toggle the read-only bit. These six tests failed on Windows with mismatched mode values or a missing 40411.
Skip them on win32 via it.skipIf(process.platform === 'win32'): oauth FileTokenStorage (0600 file, 0700 dir), agent-core BackgroundTaskPersistence (0700 tasks dir), agent-core createPerIdJsonStore (0700 subdir), migration-legacy atomicWrite (0600 file), and server fs:browse (chmod 000 -> 40411).
* test(tui): make platform-sensitive assertions cross-platform
The TUI implementations are already platform-aware (pathe-style paths, pathToFileURL, quoteShellArg cmd/POSIX quoting, Alt+V on Windows for paste expansion), but the tests hard-coded POSIX expectations and failed on Windows.
Align the assertions with the implementation's platform behavior: footer-goal-badge matches the '[goal' badge prefix instead of /goal/ (toolbar tips contain '/goal'); tool-call expects backslash relative paths on win32; plan-box builds the file:// URL via pathToFileURL; custom-editor sends Alt+V on win32 for paste expansion; file-mention-provider normalizes the expected description to forward slashes; kimi-tui-startup builds the resume command with quoteShellArg; kimi-tui-message-flow builds the expected install path with resolve().
* test: align path assertions with pathe on Windows
Several test suites asserted paths produced by node:path/node:os/node:fs against values that agent-core, node-sdk and kaos normalize through pathe (forward slashes). On Windows the two forms diverge (backslashes vs forward slashes), failing about 19 assertions.
Mirror the implementation's normalization in the assertions via a local toPosix helper (or agent-core's normalizeWorkDir), so expected paths use forward slashes on every platform: kaos LocalKaos, node-sdk export/list/resume/config/transport sessions, cli FileMentionProvider, and agent-core skill-session.
* test(native): build path expectations with node:path.resolve
paths.mjs builds every path with node:path.resolve, which yields backslash-separated absolute paths on Windows. The path-helpers tests asserted against template strings that mixed the backslash appRoot with forward-slash segments, so Object.is failed on Windows even though the strings looked identical.
Build the expectations with the same resolve(appRoot, ...) helper so the separators match on every platform.
* fix: make Windows CI tests pass across all packages
Fix the remaining Windows CI failures so the Windows test job can go green. The changes fall into a few categories:
- Path separators: agent-core/node-sdk/kaos normalize paths via pathe (forward slashes); align test expectations and a couple of implementations (native cache base, workspace registry) with that.
- Platform-only services: skip launchd/systemd manager suites on win32 (Windows uses schtasks).
- Process/signal lifecycle: skip or relax tests that rely on POSIX signals / SIGTERM semantics that Windows does not support.
- Hook shell syntax: rewrite hook test commands from POSIX shell (single quotes, semicolons, stderr redirects, if/then/fi) to node -e / .cjs files that run under cmd.exe.
- CRLF: make Bash tool description stripping tolerate CRLF line endings.
- Misc: realpath short-name divergence, port-retry timing, telemetry spawn, fs-watch timing, snapshot path normalization, etc.
* fix: remove unused basename import in workspaceRegistryService
Fix lint error (no-unused-vars): basename from node:path is no longer used after switching to posixBasename from pathe.
* fix: align resume harness pathClass and wait for banner state on Windows
Two more Windows CI fixes:
- createResumeNoSideEffectKaos now reports pathClass 'win32' on Windows so tool descriptions (e.g. Glob's Windows note) match the live agent in expectResumeMatches, fixing usage/description deep-equal drift.
- kimi-tui-startup once-banner test now waits for writeBannerDisplayState to land before asserting, since the atomic write can lag behind the render on Windows.
* fix: resolve remaining Windows unit test failures
Make the new Windows CI job green across agent-core, kaos, node-sdk and server:
- Align the resume harness kaos pathClass with the live agent so platform-conditional tool descriptions (Glob's Windows note) match in expectResumeMatches instead of drifting on win32.
- Rewrite hook commands in agent-core tests as cross-platform node one-liners; single-quote echo, >&2 and ';' do not work under cmd.exe.
- Add .gitattributes enforcing LF so raw-imported templates (e.g. the compaction instruction) produce byte-identical token counts on Windows and POSIX.
- Terminate the full process tree on Windows in both the hook runner and kaos (taskkill /T /F) so grandchildren cannot outlive their parent and keep the cwd locked.
- Normalize workDir path separators in two kimi-sdk session tests to match the stored canonical form.
- Avoid cmd.exe arg-quoting pitfalls in the kaos cmd.exe test, and run the Windows process-tree kill test from a script file with the pid path passed via argv.
- Give the first fs-git e2e test more time on Windows and retry the temp-dir cleanup; skip the fs-watch overflow-burst assertion on Windows where fs-event coalescing prevents the single-window spike.
* ci: retrigger checks
* fix: resolve remaining Windows failures after merging main
- Terminate the spawned git/gh process tree on Windows in FsGitService (taskkill /T /F on timeout) so a timed-out 'gh pr view' cannot leave a grandchild holding the workspace cwd, which made the fs-git e2e cleanup fail with EPERM.
- Give the fs:git_status e2e suite a longer timeout on Windows and retry the temp-dir cleanup longer to ride out the slower child-process teardown.
- Make the third-party plugin install trust test assert the resolved install path via node:path so it matches the Windows-resolved path (D:\tmp\...) as well as the POSIX one.
* fix: align workspace registry roots and harden fs-git cleanup on Windows
- workspace-registry test: compare normalized (forward-slash) roots, since the registry and session index both store workDir via pathe.resolve (forward slashes on every platform). realpath() yields backslashes on Windows and diverged from the stored root.
- fs-git e2e: bump the temp-dir cleanup retries and the afterEach timeout, since Windows child-process teardown after server.close() is asynchronous and can keep the workspace cwd locked for several seconds.
* test: stub openUrl in kimi-tui-message-flow feedback tests
The /feedback command falls back to openUrl(FEEDBACK_ISSUE_URL) when submission fails, which spawned a real browser window on every test run. Mock #/utils/open-url (matching the existing login/message-replay/server test convention) so the suite never opens a browser.
* test: harden fs-git e2e cleanup against Windows cwd locks
On Windows, git/gh child processes and the session core process can outlive server.close() and keep the temp workspace as their cwd, so rmSync fails with EPERM even after a long retry. Add rmSyncRobust that retries and, if the cwd is still locked, swallows EPERM/EBUSY on Windows — the OS reclaims the temp dir and a cleanup hiccup must not fail an otherwise-passing test.
* test: harden server e2e cleanup against async teardown races
server.close() does not fully await the server's asynchronous teardown, so on a loaded CI runner the temp home/workspace dirs can still be held or written to when the afterEach rmSync runs, failing with EPERM (Windows) or ENOTEMPTY (Linux). Use a rmSyncRobust helper (retry + swallow EPERM/EBUSY/ENOTEMPTY) in the fs-git and question e2e cleanup. Also fix a leftover `throw err` (renamed to `throw error`) that broke the typecheck.
* feat(web): add a copy button to user messages
Mirror the assistant per-message copy button on user turns, copying the message text with the same checkmark feedback.
* fix(web): place user message copy button with undo and time
Move the copy button out of the line-number role row into the modern bubble meta row, grouped with the undo and time buttons, with matching styling and tooltip.
* feat(web): show full accumulated subagent progress
The subagent detail panel only showed the most recent 40 progress lines because the reducer truncated the accumulated output. Keep the full history so the panel reflects the entire process as it grows.
* fix(web): preserve subagent progress across task updates
The projector emits a taskCreated (without reducer-owned outputLines) right before every taskProgress, and the taskCreated branch replaced the task object outright, resetting outputLines to empty on each progress event. So the panel still only showed the latest chunk. Preserve the accumulated outputLines when replacing a task, and update the test to mirror the real taskCreated-before-taskProgress path.
* feat(web): clean up subagent progress text
Drop the noisy 'Started a step' line and summarize tool calls with a concise target (path / command / pattern) instead of the full JSON args, so the subagent progress panel shows what the subagent is actually doing.
* fix(web): strip numeric index from subagent tool result names
Some subagents name tool calls with a trailing index (e.g. Read_0, Bash_4), which surfaced in tool.result progress lines since the label did not resolve. Strip the index before resolving the label.
* fix(web): bound non-subagent task output and subagent progress text
Restore a tail cap for background bash/tool task output (which can grow without bound) while keeping subagent progress in full, and cap individual subagent tool.progress chunks so a single huge output cannot dominate the panel.
* feat(web): group and fold subagent progress output
Drop the noisy 'Finished' lines, group tool output under its call, and fold long output (first 5 + last 2 lines, expandable) so the subagent progress panel shows the call rhythm at a glance.
* feat(web): show edit diffs inline in tool call cards
Render a line-by-line diff inside Edit/Write tool cards in the web chat, reusing the existing diff-line style from the changes panel. The diff is built client-side from the tool input, so no protocol or server changes are needed; the header chip now shows real +/- counts.
* fix(web): extend diff line backgrounds through horizontal scroll
Diff rows were only as wide as the viewport, so the add/del background stopped where long lines overflowed and the area revealed by horizontal scroll had no color. Size each row to its content so the background paints the full line.
* fix(web): make all diff rows fill the longest line width
Size the diff container to the longest line and have every row fill it, so add/del backgrounds form one continuous band across the whole horizontal scroll instead of stopping at each line's own length.
* fix(web): resolve oxlint errors in diff line builder
Use Array.at(-1) and Array.from instead of index access and new Array(length), which oxlint flags as errors.
* fix(web): show tool output for failed edit/write calls
When an Edit/Write call fails, render the tool output (the failure reason) instead of the requested diff, so error cards explain why no change was applied.
* fix(web): fall back to tool output for replace_all and append edits
A single-pair diff misrepresents replace_all edits (which change many occurrences) and from-empty diffs misrepresent append writes (which extend an existing file). Skip the synthetic diff in those cases and render the truthful tool output instead.
* feat(web): open edit diffs in the right-side detail panel
Move the Edit/Write diff out of the inline tool-card body into the shared right-side detail layer, opened by clicking the card (like the subagent detail panel). The panel shows the line diff when it faithfully represents the operation, otherwise the tool output (replace_all, append, errors), so failed calls explain why no change was applied.
* feat(web): toggle detail panels closed when their trigger is clicked again
Clicking the same Edit/Write card, file link, or git-status area a second time now collapses the right-side detail panel, matching the existing thinking / compaction / subagent toggle behavior.
* fix(web): cap client-side edit diff to avoid freezing on large inputs
The line-level LCS allocates an (oldLines+1) x (newLines+1) matrix and runs eagerly when an Edit/Write card renders. For large edits (e.g. a 5k x 5k block, ~25M cells) this can freeze or exhaust the chat. Cap the matrix at 1M cells and fall back to the raw tool output beyond that.
* fix(web): keep the edit diff panel in sync with live tool state
Store only the tool id and re-derive the diff panel payload from the live tool call in the session turns, so a panel opened while the tool is still running reflects later status/output changes (e.g. an eventual error shows the failure output instead of the stale attempted diff).
* fix(web): do not render a synthetic diff for Write calls
Write only reports the new content, so the client cannot tell a new file from an overwrite of an existing one. A from-empty diff showed overwrites as all additions and no deletions, which is misleading. Fall back to the tool output for every Write (append already did).
* fix(web): also cap diff output rows, not just the LCS matrix
The matrix-size cap alone let through asymmetric edits (e.g. one line replaced by hundreds of thousands) whose small matrix still produced a huge row array that the chip computed on render. Cap each side's line count too, so the diff output is bounded.
* fix(web): keep edit/write cards expandable when the diff panel is unwired
Clicking an Edit/Write card opens the right-side diff panel, but the nested ChatPane in the side chat does not wire that event, so the click became a no-op and the card could no longer expand to show its output. Gate the panel behavior behind a toolDiffPanel prop (enabled only in the main conversation); elsewhere the cards expand inline as before.
* fix(web): close the tool diff panel when its target disappears
When a session resync removes the tool call an open diff panel is showing, toolDiffTarget becomes null but detailTarget stayed 'toolDiff', leaving an empty aside that Escape would not close. Gate sidePanelVisible on toolDiffVisible so the panel collapses once its target is gone.
* fix(reload): re-inject plugin session-start reminder after /reload
Reload reloaded plugins and resumed the session, but the model kept seeing the stale plugin session-start reminder from before the reload, so plugin skill changes only took effect in a fresh session.
Append a fresh plugin_session_start reminder to the main agent after reload, gated on a new forcePluginSessionStartReminder flag that only the explicit /reload command sets, so config and experiment toggles that reuse the reload RPC do not spam the transcript.
* fix(reload): keep reload result fresh and neutralize stale plugin reminder
Append the plugin session-start reminder before constructing ResumeSessionResult so SDK callers reading getResumeState() see the refreshed plugin context instead of a pre-reload snapshot.
When a plugin with a prior plugin_session_start reminder is disabled or removed, append a neutralizing reminder so the model does not keep following stale plugin instructions.
* fix(reload): neutralize stale plugin reminder after compaction
A full compaction folds the discrete plugin_session_start reminder into a compaction_summary, so the origin-only scan no longer detects it. Also treat a compaction_summary in history as a signal to neutralize, so disabling or removing a plugin after compaction still emits a superseding 'no active plugin session starts' reminder.
* fix(reload): thread plugin reminder option through KimiHarness.reloadSession
KimiHarness.reloadSession is a public SDK entry point; forward forcePluginSessionStartReminder to both the active-session and RPC reload paths so SDK callers using the harness can opt into the refreshed plugin reminder too.