* feat(background): add print_background_mode with steer for multi-turn -p runs
- add `[background].print_background_mode` (`exit`/`drain`/`steer`) and
`print_max_turns`; when unset, falls back to `keep_alive_on_exit = true`
mapping to `drain`, preserving existing behavior
- core: `Session.handlePrintMainTurnCompleted()` returns `finish`/`continue`;
in `steer` mode the run stays alive so a background-task completion
`turn.steer`s the main agent into a new turn (matching background
subagents), bounded by `print_wait_ceiling_s` and `print_max_turns`
- cli: print driver follows every main turn instead of only the first and
defers `finish()` until the run quiesces or a limit is hit
- plumb `handlePrintMainTurnCompleted` through node-sdk RPC; docs + tests
A kimi -p run settled the moment the main agent's turn ended (end_turn),
so a goal created mid-run was cancelled during cleanup and a scheduled
cron task never fired in the same run.
- runPromptTurn now re-evaluates completion when the main agent goes idle
and stays alive while a goal is still active (the goal driver runs the
continuation turns) or while cron tasks with a future fire remain (their
fire steers a fresh turn). A ref'd handle keeps the event loop alive
during the wait since the cron scheduler tick is unref'd.
- a terminal goal.updated (e.g. the driver blocking a goal on a hard
budget, which emits no further turn.ended) also re-evaluates so the run
cannot hang.
- add getCronTasks RPC and Session.getCronTasks() so the print flow can
enumerate pending cron tasks.
* 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.
* feat: support multi-level thinking effort switching
- kimi provider: emit thinking.effort in the new wire format; keep reasoning_effort mirrored during the transition
- model catalog: thread support_efforts / default_effort from oauth through to /models
- config schema: add supportEfforts / defaultEffort on model aliases
- TUI: multi-segment thinking control in /model, new /effort command, footer effort display
- switch status uses displayName and distinguishes model vs effort-only changes
* docs: add thinking effort design plans
- thinking-effort-switching.md: implemented multi-level effort switching
- thinking-model-overhaul.md: follow-up refactor plan for the thinking state model
* docs: collapse thinking overhaul plan into a single PR
* refactor!: overhaul thinking config and effort resolution
Replace default_thinking and thinking.mode with a single [thinking] enabled/effort table. ThinkingEffort is now an open string ('off' | 'on' | model-declared effort); effort levels come from each model's support_efforts instead of a fixed enum.
Centralize default and always_thinking clamp logic in resolveThinkingEffort/defaultThinkingEffortFor, and honor an explicitly configured effort when an always_thinking model is forced back on.
TUI keeps a single thinkingEffort field instead of the boolean + level pair; 'on' is normalized to the model default at the UI boundary.
BREAKING CHANGE: default_thinking and thinking.mode are removed from config; migrate to [thinking] enabled/effort.
* refactor: rename residual thinking level wording to effort
Rename comments, error messages, parameter names, the SetThinkingPayload wire field (level -> effort), and TUI local variables so the thinking effort naming is consistent throughout. No behavior change.
* refactor: rename remaining camelCase thinking level identifiers to effort
Rename liveLevel/prevLevel/levelChanged/commitLevel/effectiveLevel to liveEffort/prevEffort/effortChanged/commitEffort/effectiveEffort in the TUI model picker and config commands.
* refactor: eliminate remaining thinking level wording in comments and tests
Rename levelLabel -> effortLabel, EffortSelectorOptions.levels -> efforts, and 'effort level(s)' / 'default level' / 'requested level' wording in comments, error messages, slash-command description, and test titles to effort. Also restore the withThinking(effort) parameter rename in the Kimi provider that was accidentally reverted.
* fix: address codex review feedback on thinking effort handling
- OpenAI thinkingEffortToReasoningEffort and Anthropic clampEffort now normalize 'on' / unrecognized efforts instead of throwing, so boolean non-Kimi models no longer crash on session start.
- ACP resolveCurrentThinkingEnabled treats a non-empty thinking.effort as enabled, matching agent-core's resolveThinkingEffort.
- REST promptThinkingSchema accepts any non-empty effort string so model-declared efforts are not rejected at the API boundary.
* test: align kimi e2e expectations with supportEfforts-gated reasoning_effort
The kimi provider now sends reasoning_effort only when the model declares support_efforts; boolean models (no support_efforts) send only thinking.type. Update the kimi e2e tests to drop the stale reasoning_effort expectation for the boolean test model.
* test: cover [thinking] effort parsing in config.test
Add effort = "high" to the documented [thinking] table in the config parse test and assert config.thinking.effort is resolved, so the new [thinking] effort field has direct parse coverage.
* docs: add thinking test coverage gap analysis
Capture the explore agent's test coverage review for the thinking overhaul PR, including P1/P2 gaps and the two open design questions, for follow-up test additions.
* feat(oauth): parse nested think_efforts from /models response
The /models endpoint now returns effort levels under a nested think_efforts object ({ support, valid_efforts, default_effort }). Parse it preferentially in both managed-kimi-code and open-platform model parsing, falling back to the legacy flat support_efforts / default_effort fields for older servers.
* refactor(oauth): only read nested think_efforts; gate on support=true
Drop the legacy flat support_efforts / default_effort fallback. The think_efforts object is now the single source, and its support flag gates the whole object — when support is not true, valid_efforts and default_effort are ignored entirely.
* chore: remove unused parseStringArray import in open-platform
* docs: finalize thinking effort release notes
Downgrade the changeset to minor with an English summary, drop the version-specific 'added in 1.0.0' info block, and present the deprecated config fields as a table (field / deprecated in 0.21.0 / description).
* refactor: drop temporary refresh toggles and kimi reasoning_effort mirror
Remove the always-true REFRESH_MODELS_ON_PICKER_OPEN / REFRESH_PROVIDER_MODELS_ON_STARTUP toggles and their stale re-enable TODOs, and stop sending reasoning_effort from the kimi provider (thinking.effort is the only wire field now).
* fix(tui): avoid persisting "on" as thinking effort
* fix: preserve persisted thinking effort across login and provider setup
* fix(tui): show actual thinking effort in /status and footer
* test(tui): align message-flow expectations with effort persistence and /status display
* fix(vis): rename thinkingLevel to thinkingEffort in config.update analysis
* 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.
* feat: add shell mode (`!`) to the CLI
Add shell mode, letting users run shell commands directly from the prompt
with `!`. Output streams live into the transcript, supports backgrounding
(ctrl+b), cancellation (Esc / Ctrl+C), input queuing while running, and
enters the conversation context with resume support.
* feat(kimi-code): show shell mode label on editor border and add tip
Render a "! shell mode" label on the top-left of the editor border while the editor is in `!` bash mode, so the active mode is visible at a glance. Also add a rotating toolbar tip (`! to run a shell command`) to surface the feature.
* feat(kimi-code): refine shell mode queue, history, and display
- Keep `!` commands out of input history so they never resurface as bare text stripped of their `!`.
- Make `!` commands non-steerable: Ctrl-S skips them (they stay queued to run after the current task) and the steer hint is only shown when something is actually steerable.
- Render queued `!` commands with a `$` prompt and the shell-mode hue so they read as commands, not as text to send to the model.
- Echo executed shell commands with a `$` prompt instead of `!`.
* fix(kimi-code): sanitize shell output and harden rendering
Captured shell command output can contain terminal control sequences (colours, cursor moves, alternate-screen switches, OSC hyperlinks, carriage-return spinners, bells). pi-tui's Text passes strings straight to the terminal, so any unhandled sequence was executed by the terminal and fought with pi-tui's own cursor control, producing the blank-screen-plus-leftover-characters mess after running commands like pnpm dev or a nested TUI.
- Sanitize CSI (incl. private modes), OSC, single-char ESC and C0 control chars (keeping newline and tab) in both the finished/resume view (previously unsanitized) and the running tail.
- Make the sanitize, format, and ShellRunComponent render paths never-throw, and cap the live running buffer, so a misbehaving command cannot crash the TUI.
- Dispose transcript children on clear so ShellRunComponent's timer is released on /clear or session switch.
* fix(kimi-code): render shell command echo with $ instead of sparkles
The shell command echo is a 'user' transcript entry, so UserMessageComponent prefixed it with the USER_MESSAGE_BULLET (sparkles), producing 'sparkles $ command'.
Add an optional bullet override to UserMessageComponent / TranscriptEntry and set it to an empty string for the shell echo (both live and resume), so the '$ command' content sits at the leading column where the sparkles marker used to be. Normal user messages keep the sparkles bullet.
* fix(kimi-code): enter shell mode when pasting a !-prefixed command
The bash-mode trigger only handled the single ! keystroke, so a pasted !cmd was inserted as literal text in prompt mode and submitted as a normal message.
After pi-tui inserts pasted content, detect an empty-prompt buffer that now starts with !, switch to bash mode, and strip the leading ! so the buffer holds only the command, matching the typed ! path.
* fix(kimi-code): restore shell mode when recalling a queued command
recallLastQueued() dropped the queued item's mode, and the Up-arrow recall only restored the text. A queued ! command (queued while another command runs, which resets the editor to prompt mode) therefore came back as a normal prompt and was submitted as a message instead of a shell command.
Return the full QueuedMessage from recallLastQueued() and restore editor.inputMode (plus the onInputModeChange sync) from the recalled item's mode.
* feat(kimi-code): use violet as the shell mode color
Replace the claude-code-style magenta/rose shellMode token with a violet that is distinct from plan-mode blue, the user role amber, success green, error red, and the teal accent.
Custom themes that omit the token fall back to this new default via the base+overrides merge, so existing custom themes keep working unchanged.
* chore: refine the shell mode changeset
* docs: document shell mode
Add a Shell mode section to the interaction guide and list the ! and Ctrl+B shortcuts in the keyboard reference, in both English and Chinese.
* test(protocol): include shell events in volatile classification check
shell.output and shell.started were added as volatile event types for shell mode; update the snapshot test's volatile-type list and count accordingly.
* fix(agent-core): surface shell command failure reason with no output
When a ! shell command fails without producing stdout/stderr (non-zero exit with no output, timeout, spawn failure), the failure reason lived only in the tool result's output and the TUI showed '(no output)'. Fold it into stderr so the live view and replay show what went wrong.
* fix(kimi-code): decode CSI-u ! to enter shell mode
In terminals with the Kitty keyboard protocol (VSCode integrated terminal, Kitty), pressing ! arrives as a CSI-u sequence, so the raw normalized === '!' comparison never matched and shell mode could not be entered by typing !. Decode with printableChar before comparing, matching every other printable-key check in the TUI.
* fix(kimi-code): do not steer while a shell command is running
Ctrl-S steers queued input into the running turn, but a shell command is not an agent turn, so steering during streamingPhase === 'shell' would launch a turn before the command output is recorded. Keep Ctrl-S a no-op during shell runs; queued messages stay queued.
* fix(agent-core): escape bash tag delimiters in shell output
Shell command output is arbitrary text; if it contains a bash tag delimiter such as </bash-stdout>, the recorded pseudo-XML wrapper breaks and replay extracts the wrong slice. Escape the content when wrapping it in agent-core and unescape when extracting during replay, so output survives round-trip intact.
* docs: document the shellMode theme token
The shellMode color token was added to the palette but not propagated to its mirrors. Add it to the custom-theme docs token table, the theme JSON schema, and the custom-theme skill token list.
* feat(agent-core): reset background task deadline on detach
Add a resettable deadline timer to BackgroundManager and let tasks register a detach timeout; when a foreground task is moved to the background, its deadline resets to the background default counted from the detach moment.
Wire this into shell mode so ! commands run with a 3-minute foreground timeout and get 10 minutes once detached to the background, instead of staying bounded by the original 60-second foreground deadline.
* feat(agent-core): lower shell mode foreground timeout to 2 minutes
Oversized AGENTS.md files are no longer silently truncated. The full
content is injected, and a warning is shown in the TUI status bar and the
web UI when the combined AGENTS.md size exceeds the recommended 32 KB.
A generic session-warnings API backs this so future warning types can be
added without changing the API surface.
* feat: add workspace add-dir support
Add multi-directory /add-dir management with session-only or project-remembered persistence, directory completion, confirmation UI, and runtime workspace/permission wiring.
* fix: honor --add-dir for resumed sessions
Pass CLI additional directories through shell and prompt resume paths, resolve caller-relative dirs against workDir, and add regression coverage.
* fix: keep additional dirs AGENTS.md out of default context
Load only user-level and cwd AGENTS.md by default, while preserving additional directory listings in the prompt context.
* feat: append /add-dir result as user message
Add a session appendUserMessage RPC and use it after /add-dir so the command result is recorded as a normal user message and surfaced in the transcript.
* docs: add add-dir research and follow-up todos
Document the add-dir / local-command-stdout research findings and the follow-up tasks for stdout wrapping, slash file completion, and hints.
* feat: wrap /add-dir output as local-command-stdout
Insert the /add-dir result as a user-role <local-command-stdout> record with an injection origin directly inside Session.addAdditionalDir. It enters the model context on the next turn but does not start a turn, and stays out of the live and resumed transcript; the transient status toast is kept for immediate feedback. --add-dir is unaffected since it bypasses addAdditionalDir.
Remove the now-unused appendUserMessage RPC and SDK method.
* feat: reopen /add-dir completion after accepting a directory
Generalize the slash-argument completion reopen so it fires whenever the text before the cursor ends with '/', not only when the literal '/' key is typed. After Tab-accepting a directory (or auto-applying a single-child dir), the next level's completion list reappears automatically, so repeated Tab keeps drilling down into subdirectories. '@' file mention is unaffected.
* feat: reopen file mention completion after accepting a directory
Extend the path completion reopen so it also fires for '@' file mentions. After Tab-accepting a directory in an '@' mention, the next level's completion list reappears automatically, matching the '/add-dir' continuous-Tab behavior.
* feat: show inline argument hints for slash commands
Render a dim ghost-text argument hint inside the input box after a slash command that takes arguments, replacing the popup-only hint that was easy to miss. The hint appears once the command is typed and disappears as soon as an argument is entered, and is truncated to fit the box width. Add argument hints for /compact, /swarm, /goal and /title; /add-dir already had one.
* test: remove stale additional-dirs AGENTS.md assertion
The subagent-host test still asserted that an additional directory's AGENTS.md content appears in the agent system prompt, but additional-dirs AGENTS.md has been intentionally excluded from the default context since an earlier commit (covered by context.test.ts). Drop the stale assertion.
* fix: resolve /add-dir paths against workdir and persist via kaos
Resolve user-supplied /add-dir paths against the current workdir instead of the project root, so launching from a subdirectory behaves like the CLI --add-dir flag. Also route the local.toml read/write through the kaos abstraction instead of host fs, so the remember path works for non-local sessions.
* fix: expand ~ in /add-dir paths before resolving
The /add-dir completer emits ~/... values, but the core treated ~/foo as a relative path because pathe isAbsolute('~/foo') is false, producing <workDir>/~/foo. Expand ~ and ~/ to the home directory (via kaos.gethome()) before resolving.
* chore: remove add-dir dev docs from the branch
These were working notes (research and follow-up todos) that don't belong in the PR.
* chore: clarify add-dir changeset for users
* docs: document /add-dir, --add-dir, and local.toml
* test: flush records before reading wire in add-dir runtime tests
FileSystemAgentRecordPersistence.append buffers records and flushes asynchronously, so readMainWire can read the wire before the local-command-stdout record lands. Flush the main agent's records explicitly in the two add-dir runtime tests to make them deterministic.
* feat(tui): add /export-md slash command
Add a new /export-md (alias: /export) command that exports the current
session conversation as a human-readable Markdown file. The export
includes YAML frontmatter metadata, an overview section, and
turn-by-turn dialogue with collapsible thinking blocks and tool
call/result details.
Also exposes Session.getContext() on the SDK to allow TUI-layer access
to the agent's conversation history.
* chore: add changeset for /export-md
* fix: use static imports instead of dynamic imports in handleExportMdCommand
* chore: simplify changeset wording