* fix: record crash telemetry for unhandled promise rejections
The crash handler only listened on uncaughtExceptionMonitor, which never
fires for a rejection that has a listener — and the TUI always registers
one, converting rejections into a silent exit(1) with no telemetry at
all. Observe unhandledRejection directly so those crashes still leave a
trace. Since registering a real listener suppresses Node's default
crash-on-rejection, rethrow when we are the only listener (print /
server modes), deduped so the monitor does not double-report.
* fix: fall back to text paste when the clipboard image handler rejects
The Ctrl+V image-paste dispatch chained off the async handler without a
rejection branch, so any failure inside it became an unhandled
rejection — which the CLI's crash path turns into a silent exit(1).
Treat a rejection like "no image available" and paste as text.
* fix: dedupe all rethrown rejection reasons at the crash monitor
Non-Error rejection reasons (plain objects, strings, null) were recorded
by the rejection handler but not added to the dedupe set, so the
uncaughtExceptionMonitor pass after the rethrow reported a second crash
for the same failure; null/undefined reasons also crashed the monitor's
own error-type extraction. Use a Set so primitives dedupe by value, add
every rethrown reason, and classify monitor crashes null-safely.
The TUI crash path (uncaughtException / unhandledRejection) logged the
error into an asynchronously-drained sink and then called process.exit()
on the same tick, so the one line explaining the crash was never written
to disk. Flush the diagnostic logs synchronously before exiting.
Surface the coded provider error the daemon already sends: a semantic
title per error code, the provider's raw message, and expandable
diagnostics (error code, HTTP status, request ID, SDK error name) with
copy support, instead of a bare text-only toast.
* fix(web): dedupe background subagent rows in the agents dock
* fix(web): seed agent identity on late task registration and prefer REST output in task fold
* fix(web): backfill terminal output to folded background subagent rows
* fix(web): sync subagent phase when the REST fold makes a row terminal
* fix(kap-server): fail closed on percent-encoded API paths in auth bypass
The auth hook checked the raw, still percent-encoded request URL against
its bypass policy, while find-my-way matches routes against the decoded
path. A raw /%61pi/v1/... URL therefore skipped the bearer-token check
yet still reached the /api/... handlers, allowing unauthenticated access
to every API route.
Decode the request path the same way the router does before matching,
and fail closed when the path cannot be decoded. Add tests covering
encoded /api/ paths, encoded meta documents, the encoded healthz probe,
and unaffected non-API bypasses.
* fix(agent-core-v2): make session fs path confinement symlink-aware
SessionFsService confined paths only lexically, so a symlink planted
inside the workspace could steer read, list, stat, mkdir, resolvePath
and resolveDownload to host files outside the session directory.
- add IHostFileSystem.realpath (Node fs.realpath semantics) and
implement it in the node-local HostFileSystem
- resolveWithin now re-verifies each candidate through realpath of the
longest existing prefix (so not-yet-created paths still work) and
rejects paths escaping the canonicalized workspace roots, which are
resolved once and cached
- cover symlink escapes for every fs entry point plus kap-server
read/download e2e; in-workspace symlink targets remain allowed
* fix(agent-core-v2): accept workspace roots given through a symlink
WorkspaceRegistryService.createOrTouch probes the root with the
lstat-based IHostFileSystem.stat, so a root given as a symlink to a
directory reported isDirectory=false and session creation was rejected
with fs.path_not_found ("not a directory").
Re-check a non-directory root through IHostFileSystem.realpath before
failing, while keeping the workspace identity lexical (v1 deliberately
never realpaths the root either). Covered by a unit test for a
symlink-form root and a kap-server e2e that creates a session with a
symlinked cwd and reads a file through it.
* feat(web): allow attaching any file type in chat
- Composer, paste, and drop no longer filter out non-media files;
arbitrary files upload as generic icon chips and are submitted as
file content parts
- kap-server materializes file parts into the session's attachments
dir and replaces them with a path reference, so the model opens the
file with the Read tool on demand instead of receiving inline bytes
- Images rejected by the provider format gate (SVG, AVIF, ...) are now
persisted and referenced by path instead of being dropped with a
notice; uploaded file names are sanitized before hitting disk
* fix(server): stop CSP from blocking web bootstrap script and fonts
- Move the anti-FOUC bootstrap from an inline <script> in index.html
to /boot.js: CSP 'self' never covers inline scripts, while a classic
same-origin script keeps the same render-blocking timing
- Allow data: in font-src — KaTeX and the Inter / JetBrains Mono
Variable fonts ship @font-face data URIs in their distributed CSS
- Set explicit form-action, base-uri, and frame-ancestors, which do
not fall back to default-src
- Add a regression test asserting the served index.html carries no
inline scripts or inline event handlers
* fix(web): normalize empty attachment MIME so extensionless files submit
Files with an empty File.type (Makefile, LICENSE, other extensionless
or unknown types) stored mediaType: '' on the chip, and the submit
fallback used ?? which does not catch empty strings — the wire schema
requires a non-empty media_type, so the prompt was rejected. Normalize
to application/octet-stream at attachment creation, adopt the
server-recorded MIME after upload completes, and make both submit
mappings use || so reloaded chips with '' are covered too.
* feat(web): render all user-turn attachments as chips
* feat(web): attach files by dropping them anywhere in the window
* refactor(web): share one attachment chip between composer and chat bubble
* fix(web): neutral attachment chips, paperclip attach icon, and clickable file chips
* fix(web): drop the extension badge from attachment chips
* fix(web): use the tabler paperclip for the attach button
* fix(web): whitelist attachment previews, reject active document types
Clicking a file chip navigated a new tab to a blob: URL of the uploaded
bytes whenever the type looked browser-renderable. blob: inherits the
web origin, so a text/html or image/svg+xml attachment would execute
same-origin script with the daemon credential (localStorage) and a live
window.opener.
Preview is now restricted to inert types (pdf, non-SVG images, video,
audio, non-HTML text), the blob is re-wrapped with the whitelisted MIME
instead of trusting the recorded content-type, and window.opener is
severed. Non-whitelisted types no longer silently download: the chip
reports 'unsupported' and the pane shows a transient hint.
* fix(web): recover file attachment chips from the server notice
The kap-server prompt route replaces file parts with an "Attached file
…" text notice before enqueueing, so after any snapshot resync the
attachment chip degraded into raw notice text leaking the absolute
server path — unlike image/video uploads, which already recover their
chip from the <video|image path> tag. Parse the notice the same way:
the materialized basename carries the file id, so the chip becomes
clickable again (and editable back into the composer); inline-base64
notices (content-hash named, no file id) still collapse into a
non-clickable chip instead of raw text.
The notice wording is now a client/server contract — flagged on
buildAttachedFileNotice.
* fix(web): preserve UUID file ids when rebuilding attachment chips
* fix(web): skip unresendable file chips when loading attachments for edit
---------
Co-authored-by: qer <wbxl2000@outlook.com>
* fix(web): confirm dialogs respond to Enter and await async actions
The confirm dialog's initial focus was resolved from the Button
component's $el, which is a text node in dev builds (the component has
a template-root comment, so it renders as a fragment). Focus fell back
to the header close button, so Enter cancelled instead of confirming.
Resolve the initial focus with a CSS selector on the confirm button
instead.
ConfirmOptions now accepts an async action: the dialog stays open with
a loading state (cancel/Esc/overlay suppressed) until the work settles.
The archive-session, remove-workspace, and delete-provider confirms
move from the menu components into App.vue so the dialog can await the
actual client call.
* fix(web): block superseding a confirm dialog while its action runs
A second confirm() during an in-flight action would replace the busy
dialog and inherit the global busy state, opening inert until the first
action settled. Resolve the new request unconfirmed instead.
* fix(agent-core-v2): make Gemini tool call ids unique across turns
* fix(kosong): recover tool names from entropy-suffixed Gemini call ids
The orphan tool-message fallback in toolCallIdToName stripped only one
trailing "_<suffix>" segment, so with the new
"{tool_name}_{upstream_id}_{entropy}" id shape it returned
"AgentSwarm_0" instead of "AgentSwarm" once the preceding assistant
call had been compacted away. Strip the fixed 8-hex entropy suffix
first, then the upstream id segment, in both the kosong and
agent-core-v2 providers. Also drop the inline implementation comment
that agent-core-v2 forbids outside the file header.
---------
Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
* feat: raise default per-step LLM retry budget to 10 attempts
The default of 3 attempts only produced ~1.5s of backoff (0.5s/1s), so
sustained provider overload (429) surfaced to the user almost
immediately. With 10 attempts the exponential ramp (500ms base, x2,
32s cap, 25% jitter) waits out multi-minute overload windows before
failing the turn. Applies to both agent-core (chatWithRetry) and
agent-core-v2 (stepRetry service); loop_control.max_retries_per_step
still overrides.
* test(agent-core): pin retry-dependent tests to the new default budget
- error-paths: the warn-log attempt field now reads 1/10 with the
default 10-attempt budget
- goal-session pause tests: every LLM call throws a retryable error, so
the default 10-attempt backoff (~2min) blew the 5s test timeout; pin
maxRetriesPerStep=1 since the pause behavior does not depend on the
retry count
* fix(kimi-web): improve mobile safe-area handling
* fix(kimi-web): restore dock-height fallback where ChatDock is absent
* fix(kimi-web): pin the app shell to the visual viewport height
* fix(kimi-web): pin the app shell to the visual viewport
* chore: add changeset for mobile safe-area fixes
* feat(agent-core): drop default timeouts for print-mode background tasks
- add `bash_task_timeout_s` config under [background] (0 = no timeout),
covering the background Bash default and the re-arm after a foreground
command is moved to the background on timeout
- allow `[subagent] timeout_ms = 0` (no timeout) and thread it through
Agent/AgentSwarm task registration and the swarm batch timer
- fill both with 0 in print-mode config defaults so `kimi -p` never kills
background work by wall-clock and only the model stops a task;
interactive defaults are unchanged
- sync the Bash tool description/parameter text with the effective
default and update user docs (en/zh) plus changeset
* test(agent-core): satisfy KimiConfig providers requirement in print-defaults tests
* fix(agent-core): clear the foreground deadline on detach when the background timeout is disabled
`detach()` only re-arms when `detachTimeoutMs` is defined, so passing
`undefined` with `bash_task_timeout_s = 0` kept the armed foreground
deadline and a manually detached command was still killed at its
foreground timeout. Pass `0` instead so the reset clears the timer.
Caught by Codex review on #1737.
* fix(kap-server): carry the live subagent roster in the session snapshot
* fix(kap-server): clear the subagent roster on the next main turn start
* fix(kap-server): exclude background subagents from the snapshot roster
* fix(kap-server): finalize live roster entries when the main turn aborts
* fix(kap-server): drop roster entries when foreground subagents detach
* fix(web): expand the swarm card by default while subagents are running
* docs(kap-server): qualify the roster-clearing durability claim
- change the default print background mode from 'exit' to 'steer' so
`kimi -p` stays alive while background tasks are pending and each
completion steers a new main turn
- add print-mode config defaults (applyPrintModeConfigDefaults):
effectively unbounded wait ceiling and turn cap, 72h subagent
timeout; explicit user config always wins
- thread the new `uiMode` option from SDKRpcClient through KimiCore
into session create/resume
- clamp setTimeout delays to 0x7fffffff so huge timeouts are not
clamped to 1ms by Node
* feat(kimi-code): keep print-mode goal runs alive until the goal settles
- applyPrintBackgroundPolicy waits for goal continuation turns via a goalActive hook before applying the exit/drain/steer mode, bounded by the wait ceiling
- createPrintTurnEndings skips the timeout when the remaining budget is not finite
- update the changeset to cover the goal-run lifecycle
* fix(kimi-code): wake the print goal wait periodically so settled goals exit promptly
* fix(server): synthesize session status only from the main agent's turn
A sub-agent runs its own turn on the shared session channel, and the
broadcaster synthesized event.session.status_changed from every agent's
turn.started/turn.ended. A foreground sub-agent finishing mid-turn thus
emitted a bogus idle transition that clients read as 'turn finished'
(notifications, sounds, unread dots, queued-message drain), while the
real main-agent turn end was swallowed by dedup.
Gate the status synthesis on MAIN_AGENT_ID, matching the existing
main-only rule in InFlightTurnTracker.
* fix(server): emit idle when a sub-agent turn ends as the last active agent
Defensive follow-up to the main-only status synthesis: ensureState seeds
lastStatus from ISessionActivity, which counts any agent's turn, so a
session first activated while only a sub-agent was active would stay
running for subscribers until the next main turn. A sub-agent's own
lease and loop state are already released when its turn.ended is
published, so a session-wide idle activity read cannot be a
mid-main-turn foreground sub-agent — emit idle in that case only.
Dedup keeps it a no-op everywhere else.
* revert: drop the defensive idle emit for sub-agent-only turn endings
This reverts 7918b45fe. The stale-seed scenario it guarded is
effectively unreachable in production: sessions are activated at
creation / first prompt and stay active for the process lifetime, and
persisted detached tasks are reconciled as lost on restart, never
resumed. The guard also raised a new behavioral question of its own
(failed/cancelled sub-agent endings would emit a success-style idle).
Keep the PR to the minimal main-only status synthesis; the theoretical
corner self-corrects on the next REST status pull or main turn.
* fix(server): report CLI version as server_version
- kap-server: accept opts.version in startServer, reported as
server_version (/meta, OpenAPI, session exports, lock registry,
default User-Agent); defaults to its own package version
- kimi-code: pass the CLI product version when starting the server
- add boot test covering /api/v1/meta, lock file, and User-Agent
* fix(agent-core-v2): make new sessions resumable by older v1 builds
- add wireRecord.seal() to write the metadata envelope at agent creation,
so fresh logs satisfy v1 replay's first-record-must-be-metadata invariant
- seal the wire log before any op dispatch in AgentLifecycleService.create;
no-op when the log already has records (resume / forked copies)
- seed empty agents/custom maps in session metadata so v1 Session.resume()
can index agents['main'] on a v2-created state.json
- add seal unit tests and lifecycle sealing tests
* fix(kap-server): show real message send times in snapshot history
- read per-record times stamped on wire.jsonl via reduceContextTranscript
and cache them alongside the transcript messages
- prefer each record's real dispatch time for created_at; records without a
stamp fall back to session.createdAt + index, clamped so the page stays
strictly increasing (mirrors MessageLegacyService.list)
- add tests for fallback, clamping, and the page-offset index mapping
* fix(agent-core-v2): backfill missing agents/custom maps in existing session metadata
- load() now heals pre-fix v2 state.json documents that never gained the
agents/custom maps, persisting the backfill so one open on a new build
leaves the session resumable by released v1 builds (Session.resume()
indexes agents['main'] unconditionally)
- updatedAt is deliberately untouched so the format heal does not reorder
session listings
* feat(agent-core-v2): make subagent timeout configurable, default 2h
- add [subagent] config section with timeout_ms and KIMI_SUBAGENT_TIMEOUT_MS env override
- resolve Agent and AgentSwarm per-run timeouts through resolveSubagentTimeoutMs
- update tool descriptions and tests to reflect the 2-hour default
* feat(agent-core-v2): align print-mode background policy with v1
- add printBackgroundMode/printMaxTurns to the task config section with resolvePrintBackgroundMode (keepAliveOnExit fallback)
- apply exit/drain/steer policy to kimi -p on the experimental engine, buffering turn.ended events and failing the run when a steered turn fails
- register the subagent config section from the package entry
* fix(agent-core-v2): strict equality in metadata heal, comments in file headers only
- replace == null with === undefined in the session-metadata heal to
satisfy eqeqeq (oxlint --type-aware in CI)
- move the seal() rationale into the wireRecord contract header and the
agents/custom invariant into the sessionMetadata header per the
tree's header-only comment convention
* feat(agent-core-v2): record droppedCount on full-compaction llm.request wire ops
- add per-attempt droppedCount to the llm.request source logFields so
replays can see how much history each retry round blinded
- cover compaction/loop request events in the full-compaction test
* refactor(agent-core-v2): rename select_tools capability to dynamically_loaded_tools
- rename the ModelCapability bit and catalog field across capability.ts,
catalog.ts, modelResolverService and the toolSelect gate
- update toolSelect flag description wording to match
- update all affected tests and the test harness capabilityNames helper
* chore: add changesets for v2 wire trace and capability rename
- register the cwd in workspaces.json when a v1 (TUI/SDK) session is
created, via a shared workspaceRegistryFile read/write/touch module
- merge session_index.jsonl into the catalog once at v2 startup
(awaited during kap-server boot), adding only paths absent from
workspaces.json
- make v2 workspace delete a soft delete through the v1-compatible
deleted_workspace_ids field: the session-index merge never resurrects
tombstoned ids, while an explicit createOrTouch clears the tombstone
When the model provider becomes resolvable only after the agent starts
(async OAuth / managed free-tokens registration), the builtin tool table
stayed empty: initializeBuiltinTools was gated on hasProvider at fixed
checkpoints and none fired. loopTools now self-heals on read once a
provider resolves, so tool calls no longer fail with "Tool not found".
* fix: preserve provider thinking effort values
* fix: resolve Kimi thinking effort fallbacks
* fix: use resolved Kimi effort in v2 requests
* fix: align Kimi effort resolution paths
* fix: synchronize forced Kimi effort state
* fix: synchronize forced Kimi effort in v2 state
* fix: tolerate unresolved models in v2 status
* fix(web): submit thinking level verbatim and drop the hardcoded default
Align kimi-web's thinking-level handling with the TUI:
- Submit the stored level as-is on every prompt path (prompt, steer,
skill activation, BTW side chat) instead of coercing it onto the
target model's declared efforts.
- No stored preference (undefined) instead of a hardcoded 'high'
default: prompts omit the thinking override and the daemon resolves
the config/model default, same as an unset [thinking] in the TUI.
- Model switcher pre-selects the target model's own default level when
switching models; re-selecting the current model keeps the level.
- Display the effective level (stored value, else the model default)
in the composer, mobile sheet, and /status panel.
* chore(web): remove the dead dev:stub script
The stub daemon (dev/stub-daemon.mjs) no longer exists, so the
dev:stub npm script and its docs references were dead weight.
* fix(web): pin the model default thinking level and persist picks globally
- With no stored preference, loadModels() pins the active model's
catalog default_effort as a concrete in-memory value, so what the
UI shows, what prompts submit, and what the session runs always
agree. localStorage stays reserved for levels the user picked.
- setThinking and model switches now also write the daemon-wide
[thinking] config (same mapping as the TUI's thinkingEffortToConfig),
so sessions created by other clients inherit the pick.
Align toProtocolModel with the v1 projection and the protocol wire schema by applying effectiveModelConfig and emitting support_efforts / default_effort, so clients of the v2 /models endpoint see thinking-effort levels again.