Commit graph

92 commits

Author SHA1 Message Date
github-actions[bot]
14cd8d098b
ci: release packages (#296)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 22:54:57 +08:00
Luyu Cheng
ac37d74484
feat: pursue a goal autonomously (#270) 2026-06-02 22:52:23 +08:00
_Kerman
191059d400
feat: add background ask user questions (#315) 2026-06-02 22:12:25 +08:00
qer
3502d870fc
chore: scrub internal identifiers from public text (#340) 2026-06-02 21:54:42 +08:00
Haozhe
7284f30479
fix(oauth): fix multi-provider custom registry import losing providers (#335)
- Add `applyCustomRegistryEntries` helper to apply all entries in-memory before a single write
- Update CLI Add Platform flow to use it, replacing interleaved in-memory mutations with removeProvider RPCs
- Add regression tests for repeated imports and provider field refreshing
2026-06-02 20:05:00 +08:00
liruifengv
eeefa98083
feat(cli): add automatic and manual upgrades (#334) 2026-06-02 18:39:50 +08:00
liruifengv
1f8c36af28
fix: improve tool output preview rendering in TUI (#317)
* fix: trim trailing empty lines from shell output previews

* fix: append … to multi-line Bash command header previews

* fix: truncate tool output by visual wrapped lines instead of raw newlines

* chore: add changeset for tool output preview fixes
2026-06-02 15:07:21 +08:00
7Sageer
3c5dee8836
feat(cli): add kimi provider subcommand for non-interactive provider management (#313)
* feat(cli): add `kimi provider` subcommand

Add a non-interactive equivalent of the TUI `/provider` command:

- `kimi provider add <url> --api-key <key>` imports every provider in a
  custom api.json registry, persisting `source` so the next TUI launch
  refreshes the model list automatically.
- `kimi provider remove <id>` deletes a provider and its model aliases.
- `kimi provider list [--json]` prints configured providers with model
  counts and source labels.
- `kimi provider catalog list [providerId] [--filter] [--url] [--json]`
  browses the public models.dev catalog.
- `kimi provider catalog add <providerId> --api-key <key> [--default-model]`
  imports a known provider straight from the catalog.

All actions reuse `fetchCustomRegistry`, `applyCustomRegistryProvider`,
`fetchCatalog`, and `applyCatalogProvider` from the existing oauth/SDK
helpers.

* fix(cli): satisfy oxlint rules in provider subcommand

- Use `Array#toSorted()` instead of `Array#sort()` to avoid mutating
  arrays returned from `Object.keys()` / `Object.entries()`.
- Drop redundant boolean-literal comparisons on `model.capability.*`
  fields (already typed as `boolean | undefined`).
- Remove the unnecessary `source as Record<string, unknown>` assertion
  in `providerSourceLabel` — `ProviderConfig.source` is already typed
  that way in the schema.
- Drop the empty-object fallback in `{ ...(config.models ?? {}) }`
  inside the test harness.

* fix(cli): address review findings on provider subcommand

P1 — `provider add`: `harness.removeProvider` re-reads the config from
disk (see `agent-core/src/rpc/core-impl.ts removeKimiProvider`), so
calling it mid-loop discarded providers we had already applied in
memory but not yet persisted. Importing a registry that added a new
provider then replaced an existing one silently lost the new one.
Drop every stale id up front in a single batch, then apply each entry
against the resulting fresh config.

P2 — `catalog add`: `applyCatalogProvider` always writes
`defaultThinking`. Hardcoding `false` would silently disable thinking
for thinking-capable models when the user had it on. Thread the prior
`defaultThinking` through.

P2 — `catalog add`: `removeProvider` clears `defaultModel` when it
pointed at one of the provider's aliases, so capturing
`previousDefaultModel` AFTER the removal yielded `undefined`. Capture
both `defaultModel` and `defaultThinking` BEFORE the removal so
re-importing a configured provider (e.g. to rotate the api key)
preserves the user's chosen default.

Tests:

- `makeHarness` now models the on-disk semantics of `removeProvider`
  (clears `defaultModel` when an alias matches, returns fresh disk
  view), so behavior that depended on the buggy in-memory mock is
  exercised honestly.
- Three new regression tests, each verified to fail against the
  pre-fix handler.

* fix(cli): address follow-up review on catalog default semantics

Two more findings on `catalog add`:

P2 — `default_thinking` fallback to `false` was wrong even after the
previous fix. `resolveThinkingLevel` (agent-core/.../thinking.ts:23)
treats `defaultThinking === false` as an explicit "off" request and
silently disables thinking before per-model defaults kick in. A
first-time `kimi provider catalog add anthropic --default-model
claude-opus-4-7` was therefore still persisting `default_thinking =
false` for thinking-capable models. The handler now always restores
the previous `defaultThinking` (including `undefined`) — the only
way to let the runtime resolver pick the per-model default.

P2 — Restoring `default_model` was unconditional, even when the
refreshed catalog no longer ships that model. `applyCatalogProvider`
drops the old aliases and only populates the current catalog, so
restoring an alias the catalog no longer contains would point
`default_model` at a non-existent entry and break the next session.
The handler now checks whether the alias still resolves and clears
it otherwise.

Test harness:

- The fake `setConfig` now mirrors the real `mergeConfigPatch`
  semantics (deep-merge with `undefined` keys skipped), so tests can
  honestly assert that `setConfig({defaultModel: undefined})` does
  NOT wipe a key from disk — only `removeProvider` can.

Two new regression tests, each verified to fail against the pre-fix
handler.

---------

Co-authored-by: 7Sageer <158020838+7Sageer@users.noreply.github.com>
2026-06-02 14:58:47 +08:00
caiji
58e2915c0f
fix(tui): clamp session picker lines to terminal width to fix narrow-terminal crash (#247)
* fix(tui): clamp session picker lines to terminal width

The /sessions picker drew long session ids, the inline time/(current)
badge, and long prompts past the terminal edge on narrow terminals. The
pi-tui renderer enforces that no line exceeds the terminal width and
threw 'Rendered line exceeds terminal width', crashing the app.

Clamp every line the picker emits to the terminal width via
truncateToWidth so it degrades gracefully instead of crashing.

Fixes #240

* fix(tui): simplify changeset and de-CJK session picker test

Address review feedback on #247:
- shorten the changeset to a single sentence
- replace the Chinese test title with an English one

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-06-02 14:53:47 +08:00
liruifengv
6de3d97d82
fix(tui): drain terminal input before exit (#314) 2026-06-02 14:04:17 +08:00
qer
a10cb94bc0
feat(tui): hint the /dance easter egg and highlight its sub-commands (#312)
- Add a footer rotation tip that surfaces the otherwise-hidden /dance command.
- Paint /dance on|off in the status hint so the sub-command reads as a command instead of blending into the dimmed text.
2026-06-02 13:36:36 +08:00
liruifengv
a4511ffc87
fix: show full model name in footer without truncating provider prefix (#310) 2026-06-02 13:19:29 +08:00
_Kerman
8b392a9c26
fix: restore main typecheck (#307) 2026-06-02 13:06:59 +08:00
_Kerman
a217ff09aa
feat: add /undo slash command and keep replay in sync (#277) 2026-06-02 12:48:59 +08:00
_Kerman
573c56e829
refactor: background task manager persistence (#285) 2026-06-02 12:48:31 +08:00
ArkhAngelLifeJiggy
fb35bca032
fix: replace chalk.yellow with theme-aware hex in session-directory warning (#229)
* fix: replace chalk.yellow with theme-aware hex in session-directory warning

* fix: address Codex review — fold theme tests into existing suite, fix guard test bugs

- Move theme-colors.test.ts assertions into existing terminal-theme.test.ts to
  avoid a generic feature test file (Codex P2)
- Fix chalk-named-color-guard.test.ts:
  - Replace require('fs') with ESM import (readdirSync) (P1)
  - Fix over-broad comment-skip logic — use proper inBlockComment state
    tracker instead of line.includes('*') which silently skipped valid
    code lines containing asterisks (P1)
  - Remove dead code (const entries = [['dir', '']]) (P2)

---------

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-06-02 12:15:06 +08:00
liruifengv
3d7e20e697
fix(tui): replace stale /connect hints with /provider (#303) 2026-06-02 12:04:12 +08:00
_Kerman
811f252625
feat(tui): show MCP server summary in welcome panel and update /mcp hints (#223) 2026-06-02 11:09:10 +08:00
github-actions[bot]
121a6dd2a1
ci: release packages (#237)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 10:22:31 +08:00
Haozhe
a580cd3a98
feat(glob): add brace expansion, fix backslash escaping, and lower match cap (#282)
* feat(glob): add brace expansion and fix backslash escaping

- add brace expansion support for glob patterns with up to 64 sub-pattern cap
- remove up-front rejection of pure-wildcard and **/ prefix patterns; rely on 100-match cap\n- fix backslash escape handling in globPatternToRegex and inside character classes
- include matched count in truncation messages\n- show glob pattern, path, and include_dirs in TUI tool-call headers

* fix(kaos): use charAt to avoid TS strict undefined index error
2026-06-01 21:36:33 +08:00
qer
178827db47
feat: add hidden dance easter egg (#260)
* feat: add hidden dance easter egg

* chore: update dance changeset

* fix: dispose dance easter egg on stop

* chore: clarify dance disposer naming

* chore: simplify dance lifecycle cleanup
2026-06-01 21:02:36 +08:00
Haozhe
42bb9141d8
feat(tui): add /provider command, custom registry import, and tabbed model selector (#264)
* feat(tui): add /provider command, custom registry import, and tabbed model selector

- add "/provider" slash command for managing AI providers with CRUD UI
- add custom registry import via api.json URL and Bearer token
- introduce tabbed model selector grouped by provider
- add fetchCustomRegistry and applyCustomRegistryProvider in oauth package
- replace deprecated "/connect" command with unified "/provider" flow
- update provider and slash-command documentation

* feat(tui): background provider model refresh at startup

- add `refreshAllProviderModels` utility supporting managed OAuth, open platforms and custom registries
- wire background refresh into `KimiTUI` startup via `AuthFlowController`
- add `providerSwitchHint` option to `ModelSelector` and enable it in `TabbedModelSelector`
- update `TabbedModelSelector` hint wording from "switch" to "provider"
2026-06-01 18:46:40 +08:00
qer
a1dfbfeb16
fix: clarify Kimi Platform login copy (#274) 2026-06-01 18:43:01 +08:00
Haozhe
817ad1fe9a
chore(flake): simplify nix build and add ci validation (#257)
* chore(flake): simplify nix build and add ci validation

- Replace dynamic pnpm-workspace.yaml parsing with hardcoded workspacePaths
  and workspaceNames to reduce format assumptions
- Remove update-pnpm-deps script and kimi-code-pnpm-deps package; use
  lib.fakeHash for standard hash mismatch workflow
- Remove nodejs_latest fallback in nodejsFor, hardcode to nodejs_24
- Add nix-build CI workflow that posts hash-mismatch details to PR comments
- Remove unused Nix installation step from release.yml
- Add workspace maintenance note to AGENTS.md
2026-06-01 11:47:38 +08:00
Haozhe
ee69d0ac29
feat(cron): render scheduled reminders in TUI and expose fired events (#204)
* feat(cron): render scheduled reminders in TUI and expose fired events

- add CronMessageComponent for distinct cron transcript entries in TUI
- emit cron.fired events from agent-core and expose via node-sdk
- report cron fire times with local ISO timestamps including timezone offsets
- render cron_job and cron_missed origins during session replay instead of skipping
- handle warning events in CLI and session event handler

* docs(cron): clarify that users must ask the model to manage cron tasks

- cron-create.md: instruct model to proactively tell users how to cancel/modify reminders
- cron-list.md: add guideline that users cannot directly manage cron tasks\n- cron-delete.md: emphasize users must ask model to cancel reminders
2026-06-01 11:34:35 +08:00
Kai
a24bfb1df3
feat: force adaptive thinking via KIMI_MODEL_ADAPTIVE_THINKING (#232)
* feat: force adaptive thinking via KIMI_MODEL_ADAPTIVE_THINKING

Add the KIMI_MODEL_ADAPTIVE_THINKING env var and a matching
adaptive_thinking model-alias field that force adaptive thinking
(thinking: { type: 'adaptive' }) on or off, overriding the Anthropic
model-name version inference.

This lets custom-named staff endpoints that back an adaptive-capable
model opt in even when the model name does not encode a parseable Claude
version (which would otherwise fall back to budget-based thinking).

The kosong AnthropicChatProvider resolves the adaptive decision once in
withThinking() as `_adaptiveThinking ?? supportsAdaptiveThinking(model)`
and threads it through clampEffort/supportsEffortParam so forced adaptive
also unlocks the max effort level.

* fix: make adaptive_thinking imply the thinking capability

A model alias that sets adaptive_thinking=true (or env
KIMI_MODEL_ADAPTIVE_THINKING=true) without listing a thinking
capability previously resolved to thinking=false for custom-named
endpoints not in the capability catalog. The model picker then
classified the alias as "thinking unsupported" and forced thinking
off (persisting default_thinking=false).

Treat a forced adaptive flag as advertising the thinking capability
in resolveModelCapabilities, so the config.toml one-field opt-in
agrees with the KIMI_MODEL_* env path (which already defaults
capabilities to include thinking).

* fix: honor adaptiveThinking in the model picker, not the capability resolver

The earlier resolveModelCapabilities change had no observable effect:
modelCapabilities.thinking is consumed only for completion-budget and
media gating, never to decide whether thinking is dispatched or whether
the model picker offers a thinking toggle.

The mechanism that actually forced thinking off for an adaptive_thinking
alias is the TUI model picker, which reads ModelAlias.capabilities
directly (availableModels comes straight from config.models). Revert the
resolver change and instead make thinkingAvailability() treat
adaptiveThinking=true as a thinking toggle, so a custom-named config.toml
alias configured with only `adaptive_thinking = true` is no longer
classified "unsupported" (which forced thinking off and persisted
default_thinking=false on select). The KIMI_MODEL_* env path was already
covered by DEFAULT_CAPABILITIES.
2026-05-30 09:49:08 +08:00
github-actions[bot]
d64b15d153
ci: release packages (#170)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-29 22:23:07 +08:00
qer
bab2da7b1c
feat(plugin): install plugins from a GitHub repository URL (#221)
* feat(plugin): install plugins from a GitHub repository URL

Allow `/plugins install <github-url>` (and marketplace `source` entries) to
take a GitHub repo URL directly. A new `github` source kind joins the
existing `local-path` and `zip-url` kinds.

Recognized URL forms (parsing in source.ts):

- `https://github.com/<o>/<r>`                       — bare; resolves to latest
                                                       release tag, falling back
                                                       to default branch HEAD.
- `https://github.com/<o>/<r>/tree/<ref>`            — branch / tag / SHA;
                                                       value passed to codeload
                                                       in its short form so the
                                                       backend resolves either.
- `https://github.com/<o>/<r>/releases/tag/<tag>`    — explicit tag, uses
                                                       refs/tags/<tag> to avoid
                                                       same-named-branch ambiguity.
- `https://github.com/<o>/<r>/commit/<sha>`          — explicit commit SHA.

The resolver deliberately avoids `api.github.com`: its 60/hour anonymous
quota is shared with every other tool on the egress IP (browser, gh CLI,
IDE integrations) and a first-time install failing because some other tool
ate the budget is unacceptable UX. Instead we:

- GET `github.com/<o>/<r>/releases/latest` with manual redirect and parse
  the `Location` header (302 → tag URL; 404 → no own release).
- Fall back to `codeload.github.com/<o>/<r>/zip/HEAD` for repos with no
  releases (or for forks that inherit upstream tags but have no own release
  page, which redirect to bare `/releases`).
- Only treat the explicit 404 from `/releases/latest` as "no release" — 5xx,
  403, 429, and any other non-2xx status surface a hard error rather than
  silently installing the default branch, so the user knows when transient
  GitHub issues changed the install path.

UI changes in the TUI:

- `/plugins install` now shows a live Braille spinner while resolving and
  downloading, then flips to a final status that distinguishes Installed
  (fresh) vs Updated (same repo identity, new version) vs Migrated (source
  changed, e.g. CDN zip-url → GitHub).
- `/plugins list`, the `/plugins` overview, and `/plugins info` show the
  install provenance inline. `zip-url` installs now display the URL host
  (e.g. `via code.kimi.com`, `via 127.0.0.1:port`) instead of the opaque
  `zip-url` literal. GitHub installs show `github <owner>/<repo>@<ref>`.
- Three-tier trust badge driven by the marketplace context recorded at
  install time: `official` (green) for `tier: official`, `curated` (blue) for
  `tier: curated`, `third-party` (muted) for anything not installed through
  the marketplace selector. CLI `/plugins install <url>` always records as
  third-party; the marketplace selector passes the tier through. A
  re-install replaces the marketplace context: switching to a third-party
  source clears the badge, which matches the underlying trust change.

`installed.json` gains optional `github` and `marketplace` fields
(back-compatible). PluginSummary surfaces `source`, `originalSource`,
`github`, and `marketplace` so the TUI can label installs without an extra
round trip to PluginInfo. The SDK's `session.installPlugin(source)` gains
an optional `{ marketplace }` second argument so the marketplace selector
can forward `{ id, tier }` through RPC; the CLI install path omits it.

Tests: 112 plugin-suite tests (URL parser, resolver, store round-trip,
manager integration). The manager integration tests assert codeload URLs
shape (short form for `/tree/<ref>`, explicit `refs/tags/` for
`/releases/tag/`) and verify marketplace context is persisted across
reloads and cleared on a third-party re-install.

* chore(changeset): plugin install from GitHub

* docs(plugins): document GitHub install URLs and trust badges

* fix(plugin): preserve URL-encoded characters in GitHub ref names

Git permits ref characters that have special meaning in URLs — most
notably `#`, which is a valid tag character (e.g. `release#1`) but the URL
fragment delimiter. The resolver decoded the tag from GitHub's
`/releases/latest` 302 redirect Location header and then interpolated the
raw value into the codeload URL. The literal `#1` became a fragment and
the HTTP request reached the server as `…/refs/tags/release` — a wrong or
truncated ref, leading to install failure for a release whose URL was
otherwise valid.

Two symmetric changes:

- The codeload URL builder now splits the ref on `/` (so multi-segment
  refs like `feat/foo` keep their path separators) and percent-encodes
  each segment.
- The GitHub URL parser now percent-decodes each segment from the URL's
  pathname when extracting `/tree/<ref>`, `/releases/tag/<tag>`, and
  `/commit/<sha>`. Storage and display see the human-readable Git ref
  name; the resolver re-encodes on the way out.

Malformed `%xx` sequences in user-typed URLs are tolerated: we keep the
raw segment so the caller surfaces a normal "ref not found" error
downstream instead of crashing during parse.

* fix: restrict plugin trust badges

* chore: remove fetch when show plugin list

---------

Co-authored-by: qer <Anna_Knapprfr@mail.com>
2026-05-29 22:18:16 +08:00
Kai
2bbea75ee4
feat: define model via KIMI_MODEL_* environment variables (#212)
* feat: define model via KIMI_MODEL_* environment variables

Add a KIMI_MODEL_* environment-variable channel that synthesizes a
provider (__kimi_env__) and model alias (__kimi_env_model__) in memory
and selects it as the default model, without editing config.toml.
Supports provider type (kimi/anthropic/openai), base URL, API key,
context size, capabilities, anthropic max_output_size, openai
reasoning_key, and full thinking settings.

Runtime config reads go through a new loadRuntimeConfig wrapper; the
config.toml write-back paths keep using readConfigFile so the
synthesized model is never persisted back to disk.

* feat: env-model defaults and friendly welcome model name

- KIMI_MODEL_MAX_CONTEXT_SIZE defaults to 262144 (256K) when unset
- KIMI_MODEL_CAPABILITIES defaults to image_in,thinking when unset
- TUI welcome banner shows the model display name / id instead of the
  internal __kimi_env_model__ alias key

* fix: never persist env model to config.toml; validate default_thinking

- Strip the synthesized __kimi_env__ provider / __kimi_env_model__ model
  (and a default_model pointing at it) in writeConfigFile, so the env model
  and its shell API key cannot be persisted via a getConfig -> setConfig
  patch round-trip (e.g. running /login or /connect in env-model mode).
- Reject a non-empty but unparseable KIMI_MODEL_DEFAULT_THINKING value
  (fail-fast) instead of silently keeping config.toml's existing default.

* fix: preserve on-disk default_model when stripping env model; fix thinking docs

- stripEnvModelConfig restores config.toml's default_model from raw instead of
  erasing it when the runtime default points at the env alias, so a real
  default_model survives a getConfig -> setConfig round-trip.
- Correct KIMI_MODEL_DEFAULT_THINKING docs: unset follows the global default
  (Thinking on), not Off.

* fix: restore env-injected fields to on-disk values in stripEnvModelConfig; update tests for thinking behavior
2026-05-29 21:40:56 +08:00
liruifengv
96bbc471c4
feat: add experimental feature-flag system (#205)
Introduce a central, env-driven flag registry in agent-core. Each flag is declared once with an id, full env var name, default, and surface. Within agent-core, flags are consulted through a process-global 'flags' constant that reads live process.env. Resolution precedence: master switch KIMI_CODE_EXPERIMENTAL_FLAG > per-feature KIMI_CODE_EXPERIMENTAL_<NAME> > registry default, with lenient boolean parsing via parseBooleanEnv. FlagId is a literal union derived from the registry for compile-time autocomplete and typo-checking.

SDK boundary: KimiHarness.getExperimentalFlags() returns the resolved values over RPC, and the SDK re-exports only the flag *types* — no runtime value crosses the boundary. The TUI caches that snapshot once at startup and reads it synchronously for command gating.

Gate the plugin system behind the 'plugins' flag, off by default: PluginManager.load() consults flags.enabled('plugins'), so when off no installed plugins are loaded or activated, and the TUI /plugins command is hidden from the palette and resolves as an unknown command.

Tests cover the resolver precedence matrix, registry invariants, the FlagId type guard, the live-env singleton, the plugin-load gate, the getExperimentalFlags RPC, and the TUI command gating.
2026-05-29 19:55:10 +08:00
qer
b9860e9f6e
feat: align datasource plugin with generic workflow (#215) 2026-05-29 19:51:23 +08:00
liruifengv
caaa6d83ee
fix(update): don't report success when native update fails (#214)
* fix(update): don't report success when native update fails

The native auto-updater spawned `bash -c "curl -fsSL … | bash"`. A
pipeline's exit status is that of its last command, so when curl could
not connect (e.g. a dead proxy) it produced no output, the trailing bash
read empty stdin and exited 0, and the whole command looked successful —
printing "Updated … Restart the CLI" while nothing had been installed.

Run the spawned shell with `set -o pipefail` so curl's non-zero status
propagates. installUpdate() then rejects and runUpdatePreflight() warns
and continues on the current version instead of claiming success.

* chore: add changeset for native update fix
2026-05-29 19:45:01 +08:00
Kai
f3269eacb9
fix(tui): show real terminal status for background agents (#197)
* fix(tui): show real terminal status for background agents

The Agent tool's run_in_background=true call returns a non-error
ToolResult whose body just says "status: running". The transcript
card derived its done/failed badge from that result, so every
terminated background agent — including ones reconcile reclassifies
as lost on resume — kept the green "✓ Completed" label even when
the actual task failed, was killed, or never came back.

Push the real BackgroundTaskInfo.status into the matching Agent
card so the badge reflects what happened. The card's resolver
prefers subagent agentId (live) and falls back to the description
on resume; on resume the apply step also runs after replay
finishes so the agent group can reach the borrowed components.

Also adds an agent-core regression test that pins live, busy,
group, race, and resume scenarios for the bg notification chain.

* fix(tui): also propagate bg agent terminal status to standalone cards

Standalone Agent cards (only one Agent tool call in a step, never
upgraded into an AgentGroupComponent) bypassed the previous
`setBackgroundTaskTerminalStatus` path: the standalone header reads
`getDerivedSubagentPhase`, which still derived `done` from the
non-error spawn-success ToolResult, and the method did not request
a header/content rebuild. Lost/failed/killed bg agents in this
shape still rendered as `✓ Completed`.

Thread the override through `getDerivedSubagentPhase`, populate
`subagentError` with the friendly failure message so both render
paths share one source of truth, and trigger the same header +
content rebuild that `onSubagentFailed` does. Also include the
override in `hasSubagentState` / the subagent-block early-return
so a replayed solo bg agent (no replayed subagent block, no
sub-tool activity) switches to the subagent-aware layout instead
of the generic `Used Agent` rendering.

Adds two standalone-render regression tests so the path no longer
relies on the grouped snapshot to stay correct.

* feat(agent-core): make resume actionable from the lost-task notification

A backgrounded subagent that ends as `lost`/`failed`/`killed` is
already a soft-recoverable thing — `subagentHost.resume` will
reanimate the persisted Agent instance — but the LLM had to dig
through the original spawn-success ToolResult to find the right id
and figure out the recovery shape on its own. The two look-alike
identifiers (the BackgroundManager `task_id` aka `source_id`, and
the `subagentHost` `agent_id`) regularly got confused in practice.

Surface what the model needs at the moment of decision:

  - Add `agent_id` as a top-level `<notification>` attribute for
    agent-* tasks, so the right id is structural, not buried in
    prose. Render path keeps backward-compat by omitting the
    attribute when no agent_id is known (bash tasks, old sessions).
  - On non-success agent terminal states, append a recovery
    paragraph to the body: the precise `Agent(resume=...)` call,
    the disambiguation between `agent_id` and `source_id`, the
    `run_in_background` option, and what state survives the
    restart vs. what may need to be redone.
  - Tighten the spawn-time `resume_hint` with the same
    disambiguation and an explicit pointer at the
    `task.lost`/`task.failed`/`task.killed` recovery trigger.
  - Persist `agent_id` and `subagent_type` in PersistedTask so the
    recovery body still works after a session restart, where
    in-memory `BackgroundTaskInfo.agentId` would otherwise be
    undefined. Optional fields keep the disk schema
    forward/backward compatible — pre-PR records load without
    them and silently fall back to the original short body.

* fix(tui): route bg-agent terminal events by stable agent_id, not description

`tc.subagentAgentId` is left undefined for every backgrounded agent.
`handleSubagentSpawned` early-returns for `runInBackground` before
calling `tc.onSubagentSpawned`, and the wire replay path drops the
`subagent` block entirely (`toolCallFromReplayMessage` returns only
id/name/args). So the `agentId` branch in
`applyBackgroundTaskTerminalStatus` never matched in practice, every
call fell through to the description-based fallback, and the
persisted `agent_id` we added in the previous commit was effectively
dead. That fallback also has a real failure mode: if a foreground
Agent and a backgrounded Agent share the same `args.description`,
the only candidate found is the live (unrelated) card, which gets
incorrectly relabeled as the lost task's terminal state.

Parse `agent_id: agent-N` out of the AgentTool spawn-success
ToolResult body inside `getSubagentAgentId` so the id is always
recoverable, regardless of whether the in-memory subagent metadata
was ever populated. Foreground and backgrounded Agent cards now
carry distinct ids and route correctly.

Also pipe the real `subagent.failed` error through to the parent
card. The background branch of `handleSubagentFailed` previously
only appended the dedicated transcript entry; the parent Agent
card was left with the generic "Background agent failed" written
by the later `background.task.terminated` event. Add an optional
`errorText` to `setBackgroundTaskTerminalStatus` /
`applyBackgroundTaskTerminalStatus` and pass `event.error` through
on the failed branch — the real reason now reaches both the card
and the entry.

* fix(tui): treat agent_id as authoritative when matching bg terminal events

Previously `applyBackgroundTaskTerminalStatus` always tried agent_id
first and then fell back to description match on miss. That fallback
caused two cross-card bugs:

  1. On resume, `applyTerminalBackgroundAgentStatuses` iterates every
     persisted terminal task, including ones whose tool calls fell
     outside the `REPLAY_TURN_LIMIT` window and were never mounted.
     Description fallback could route an old `lost` status onto an
     unrelated recent Agent card sharing the same `args.description`.

  2. During the live spawn → terminate window, the same card briefly
     lives in both `_pendingToolComponents` and `transcriptContainer`.
     A description-only walk visits the same component twice and flags
     itself ambiguous, dropping the otherwise unambiguous update.

When `args.agentId` is provided we now match only by id and skip on
miss. With `getSubagentAgentId` already parsing `agent_id: agent-N`
out of the spawn-success ToolResult, the id path is reliable for
both live and resume even though `tc.subagentAgentId` is never
populated for backgrounded agents. Description fallback is preserved
solely for old pre-PR sessions whose persisted records lack
`agent_id` — same best-effort behavior as before.
2026-05-29 17:26:27 +08:00
liruifengv
14a0348855
fix(tui): avoid leaking footer when resuming a missing session (#202)
* fix(tui): 避免 resume 不存在的 session 时泄漏 footer

启动期间 footer 在构造时就被挂入渲染树,而 init() 早期的 setAppState
会排出一次渲染,在 await 时真正执行(pi-tui 的 stopped 默认为 false,
未 start 也会 doRender),把 footer 画到终端。resume 不存在的 session
时,init() 随后抛错,这次过早渲染就残留在错误信息上方。

将 footer 改为就绪态 chrome:从 buildLayout 移除挂载,改在 initMainTui
中 init() 成功之后再 mountFooter。致命启动错误会在挂载前抛出,footer
不进渲染树,过早渲染只画空树,错误信息落在干净的行上。

* chore: 精简 changeset 描述

* chore: 精简注释

* test: 适配跨工作目录 resume 校验,补全 workDir mock
2026-05-29 15:38:50 +08:00
_Kerman
5159af341c
fix(agent-core): preserve blocked prompt hook context (#200) 2026-05-29 15:12:36 +08:00
_Kerman
8913440541
feat: show warning when resuming across working directories (#118) 2026-05-29 15:11:46 +08:00
liruifengv
588145dc9b
feat(tui): expand and prioritize footer rotating tips (#199) 2026-05-29 15:08:34 +08:00
liruifengv
64964a0dda
fix(tui): show plan usage as percent used to match web console (#192)
The `/status` and `/usage` "Plan usage" rows previously rendered the
progress bar from the used ratio while labelling it "X% left", so the
bar direction and the number disagreed.

Display "X% used" instead, aligning the number with the bar, and move
the reset hint to the right without parentheses to mirror the web
console layout.
2026-05-29 13:48:30 +08:00
_Kerman
564721fe16
fix: clarify subagent and background task stop messages as user-initiated (#189) 2026-05-29 13:22:34 +08:00
liruifengv
681ccc5b85
chore(changelog): sync 0.5.0 and reword 'assistant' to 'agent' (#171) 2026-05-28 22:46:50 +08:00
github-actions[bot]
eb93fdfe4a
ci: release packages (#140)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-28 22:14:35 +08:00
qer
0a766584cb
fix: polish plugin manager hints (#164)
* fix: polish plugin manager hints

* fix: refine plugin manager rows
2026-05-28 21:18:14 +08:00
Haozhe
971fce6e52
feat(agent-core): add session-scoped cron tools with persistence (#157)
* feat(agent-core): add cron ClockSources abstraction

ClockSources splits wall-clock and monotonic time so the cron scheduler
can be driven by an injected/simulated clock without breaking the lock
heartbeat. resolveClockSources reads KIMI_CRON_CLOCK to switch between
system, env-var-backed, and file-backed wall clocks; monotonic time is
always process.hrtime.bigint() and never overridable.

* feat(agent-core): add 5-field cron expression parser

parseCronExpression handles the standard 5-field syntax with the
cron-style dom/dow OR rule. computeNextCronRun uses field-by-field
jumping (not minute scanning) so sparse expressions like '0 12 1 1 *'
stay fast. hasFireWithinYears caps the search at 5 years so syntactically
legal but never-firing expressions ('0 0 31 2 *') return null instead of
spinning forever — required by CronCreate validation.

* feat(agent-core): add deterministic cron jitter

Recurring jobs shift forward by min(10% of period, 15min); one-shot
jobs landing on :00/:30 shift earlier by up to 90s. Offset is hashed
from task.id so reschedules and restarts stay stable. KIMI_CRON_NO_JITTER
disables both branches for reproducible benches.

* feat(agent-core): add CronTask type and cron prompt origins

CronTask matches what gets persisted to tasks.json (with durable stripped).
CronJobOrigin carries coalescedCount and stale so the agent can react to
collapsed fires without separate channels; CronMissedOrigin tags the
boot-time AskUserQuestion path.

* test(agent-core): guard against Date.now() in cron scheduler files

oxlint 1.59 does not support no-restricted-syntax, so the ESLint-style
guard from the plan is implemented as a vitest scan. The four guarded
files (scheduler/persist/lock/jitter) must route every wall-clock read
through ClockSources.wallNow(); clock.ts is excluded because that is
where the abstraction is defined. Non-existent files are skipped so the
guard activates automatically when later commits introduce them.

* feat(agent-core): add cron telemetry event-name constants

Four event names emitted by later commits (cron_scheduled, cron_fired,
cron_missed, cron_deleted) live with the cron module rather than in the
generic telemetry interface so a typo can't drift the metric and the
abstraction stays domain-free.

* feat(agent-core): add in-memory SessionCronStore

Holds cron tasks scoped to a single CLI session — vanish on exit. Phase 2
will add a file-backed sibling that shares the shape. Ids are 8 hex
characters with a collision-retry cap; createdAt is supplied by the
caller's wall clock so the store stays clock-pure (and exempt from the
Date.now() guard for the same reason).

* feat(agent-core): add session-only CronScheduler engine

The scheduler is a pure callback-driven loop: it gets tasks from a
source(), gates on isIdle()/isKilled?(), computes next fire via
cron-expr + jitter, and invokes onFire with the coalesced count when a
task is due. lastSeenAt is in-memory only — coalesce semantics make a
restart skip acceptable, but persisting last-fire would silently swallow
legitimately-due fires. pollIntervalMs=null lets P1.8 disable the
auto-tick timer for bench scenarios.

* feat(agent-core): add CronManager Agent integration layer

CronManager owns a SessionCronStore + CronScheduler and wires them to
the Agent: scheduler.isIdle reads agent.turn.hasActiveTurn, isKilled
reads KIMI_DISABLE_CRON, onFire builds a CronJobOrigin and routes
through agent.turn.steer. Stale flag is computed on read (7-day age,
recurring only, KIMI_CRON_NO_STALE shorts it) so manager doesn't have
to mutate persisted tasks. handleMissed takes a renderer callback so
P2.7 can plug in the AskUserQuestion text without bringing render
imports into Phase 1.

* feat(agent-core): add CronCreate tool (session-only path)

CronCreate validates the expression, enforces the 5-year fire window
(blocking '0 0 31 2 *' typos), caps prompt bytes at 8KB, caps active
jobs at 50 per session, and rejects durable=true until Phase 2 adds the
file-backed store. Manager exposes emitScheduled/emitDeleted so tools
never reach into agent.telemetry directly.

* feat(agent-core): add CronList tool

Read-only tool surfacing every scheduled cron job for the session. Each
record carries id / cron / humanSchedule / nextFireAt / recurring /
durable / ageDays / stale, formatted in the same key: value\n---\n
shape as TaskList. nextFireAt is the post-jitter timestamp so the model
sees what the scheduler will actually fire on. Malformed cron strings
render with null nextFireAt instead of throwing — defends against any
future direct store inserts.

* feat(agent-core): add CronDelete tool

Validates the 8-hex id shape up front and routes deletion through the
manager so cron_deleted telemetry stays consistent with cron_scheduled.
Not-found is reported as an error so the model corrects itself rather
than silently believing the delete succeeded — the next CronList would
still show whatever id was missed.

* feat(agent-core): wire CronManager + cron tools into Agent

Agent gains a public cron field constructed and started in the
constructor. The scheduler's setInterval is .unref()'d so the cron
timer never keeps the process alive, and isKilled (KIMI_DISABLE_CRON)
short-circuits every tick, so eager start is safe.

ToolManager registers CronCreate/CronList/CronDelete next to the
background tools. initializeBuiltinTools runs lazily after the Agent
constructor finishes, so this.agent.cron is already defined when the
tools are constructed.

* feat(agent-core): add manual-tick env + SIGUSR1 bench hook

KIMI_CRON_MANUAL_TICK=1 forces the scheduler into manual-drive mode
(pollIntervalMs: null), and in the same gate SIGUSR1 binds to a
no-throw manager.tick() so bench scripts can advance the scheduler
with kill -USR1 <pid> without a custom RPC.

SIGUSR1 binding is opt-in (rather than always-on) for two reasons:
the auto-tick interval already advances the scheduler, and a CLI with
many subagents would otherwise pile up listeners and trip Node's
10-listener default. Tests cover the env gate, signal swallowing,
listener cleanup, and the no-bind path when the env is unset. The
AgentTestContext harness gains an onTestFinished cleanup so the
auto-started cron manager never leaks across test files.

* test(agent-core): add end-to-end session cron smoke

Exercises the full Agent → ToolManager → CronScheduler stack through
the production CronCreateTool surface. Local-time anchor + injected
ClockSources make coalescedCount=3 deterministic across host
timezones. A second case walks the Create → List → Delete tool cycle
to confirm the three-tool surface composes round-trip.

* test(agent-core): extract shared cron test harness

Pulls the duplicated createAgentStub + createClocks helpers out of the
five cron test files into test/agent/cron/harness/stub.ts. The shared
stub keeps the lightweight-Agent shape (only turn + telemetry surfaces
need to look real) while letting individual tests opt into the
options that mattered locally (hasActiveTurn / steerReturns).

* test(agent-core): trim cron test file headers

Compresses each cron test header to a couple of lines describing what
the file covers. The long rationale blocks were process documentation
(why-this-file-exists, why-stub-vs-real-agent, plan-doc references)
that didn't help anyone reading the test later. The few details that
mattered (local-time anchor, coalescedCount math) stayed inline next
to the code that needs them. E2E test also picks up the shared
createClocks helper instead of defining its own.

* test(agent-core): convert cron tool output assertions to inline snapshots

The 21 multi-field toMatch / toContain assertions across CronCreate /
CronList / CronDelete tests covered the same ground a snapshot would
have but cost more diff churn when a format detail changes. Errors
become single-line snapshots; success outputs go through a small
scrubCronOutput helper that replaces the random 8-hex id and ISO
timestamp with stable placeholders so the snapshot is deterministic
across TZ and run.

* refactor(cron): remove durable flag, env clock source, and enable cron tools in default profile

- Remove the unimplemented durable persistence field from the entire cron stack (types, tools, manager, tests, docs) to avoid misleading the model into promising cross-session persistence that does not exist yet.
- Drop the env:VAR clock source in favour of the file:path source for test/bench control.
- Register CronCreate, CronList, and CronDelete in the default agent profile.

* fix(agent-core): address PR #136 typecheck and Codex review for cron tools

Typecheck:
- Bracket-access KIMI_CRON_* env reads under TS4111 in cron source/tests.
- Add `approvalRule` to CronCreate/CronList/CronDelete tool executions.
- Guard `cron-expr.detectStep` against undefined array elements; cast the
  readonly snapshot in session-store.test through `unknown`; import the
  missing `ClockSources`/`ContentPart` symbols in manager.test.
- Extend `isReplayUserTurnRecord` switch to cover the new `cron_job` and
  `cron_missed` origins so kimi-code stays exhaustive.

Semantics (from Codex review):
- CronCreate re-reads `wallNow()` and re-checks the session cap inside
  `execute()`, so manual-approval delays and concurrent prepared calls
  can't backdate the schedule or breach the cap.
- One-shot jitter floors the pull-forward at `task.createdAt`, so a
  brand-new `:00`/`:30` reminder can't end up before its scheduling time.
- Scheduler coalesce loop reapplies the same jitter as the delivery path
  and advances `lastSeenAt` to the last actually-delivered ideal fire;
  a not-yet-due jittered slot is no longer lost. One-shot fires always
  report `coalescedCount: 1`.
- Manager removes recurring tasks after the first stale fire and emits a
  `cron_deleted` event, matching the 7-day auto-expire contract.
- CronList anchors one-shot `nextFireAt` at `createdAt`, so a pending
  today's slot isn't rendered as tomorrow.

Tests cover each of the above and a changeset is added.

* fix(agent-core,tui): address deep review + Codex review on PR #136

Closes the deep-review pass and the four Codex review rounds that
followed on the cron tools feature. Consolidated rather than landed
as a series so the PR history reads as one fix wave on top of the
original Phase-1 cron implementation.

## Lifecycle + structural

- Session.close() now stops every agent's CronManager via
  `Promise.allSettled` (mirroring `stopBackgroundTasksOnExit`).
  Without this the 1s setInterval and its closure-captured
  Agent/Session graph leaked on every closed session.
- Agent constructor gates `cron.start()` and the three Cron tool
  instantiations on `type !== 'sub'`. Subagents no longer pile up
  empty 1Hz timers or duplicate SIGUSR1 listeners under
  `KIMI_CRON_MANUAL_TICK=1`.

## Permission + plan-mode parity

- `PlanModeGuardDenyPermissionPolicy` denies CronCreate and
  CronDelete during plan mode (CronList stays allowed); matches the
  TaskStop precedent.
- `DEFAULT_APPROVE_TOOLS` includes CronList for parity with TaskList
  / TaskOutput so manual-approval mode doesn't prompt on read-only
  listings.

## Cron-fire envelope + projector

- Cron fires wrap `task.prompt` in a `<cron-fire jobId=… cron=…
  recurring=… coalescedCount=… stale=…><prompt>…</prompt></cron-fire>`
  XML envelope (mirrors `notification-xml.ts`). Without this the
  `coalescedCount` and `stale` cues documented in cron-create.md
  were invisible to the LLM.
- Projector `isInjectionUserMessage` recognises `<cron-fire ` so the
  envelope isn't merged into adjacent real user messages.
- TUI `SessionReplayController.renderUserMessage` skips cron_job /
  cron_missed origins (matches `isReplayUserTurnRecord`'s exclusion);
  resumed sessions no longer render the raw envelope as user text or
  miscount cron records toward the replay turn limit.

## Scheduler invariants

- `tick()` only advances `lastSeenAt` / removes one-shots after a
  successful `onFire`. A throw in `agent.turn.steer` previously
  silently lost the fire; now the next tick re-detects and retries.
- New `getNextFireForTask(id)` on the scheduler (delegated through
  the manager) lets CronList render the same instant the scheduler
  will fire. Previously CronList computed from `nowMs` and could
  report tomorrow's slot while a current-period jittered delivery
  was still pending.

## Cron parser + validation

- `parseCronExpression` now rejects non-cron numeric forms via a
  `parseCronInt` helper guarded by `^\d+$` — `''` / `'1e1'` /
  `'0x10'` / `'+5'` / `'-5'` no longer silently become 0, 10, 16,
  5, or `0-5`.
- `nextRunWithinMinutes` bounds search by a wall-time deadline
  instead of iteration count. Each iteration can skip a month, so
  the old `capMinutes + 10_000` cap let `0 0 30 2 *` walk ~200 000
  years before bailing; the new path returns null in microseconds.
- `oneShotJitteredNextCronRunMs` returns `idealMs` (not `createdAt`)
  when the pulled-forward time would precede `createdAt`. The old
  clamp made an 08:59:30-scheduled `0 9 * * *` fire on the very next
  tick — ~29 s before ideal — instead of at 09:00.

## CronList + CronCreate output

- CronList output adds a `prompt:` row, JSON-encoded so newlines
  stay on one line, truncated to ~200 UTF-8 bytes on a char
  boundary. The model can recall a task's intent after compaction
  and use it as the source for the documented refresh ritual.
- CronCreate normalizes `args.cron` whitespace BEFORE
  `parseCronExpression` so `parsed.raw` is single-line. Otherwise
  inputs like `"1\n2\n3\n4\n5"` (legal — parser accepts any \s+)
  produced a multi-line `humanSchedule:` row via the cronToHuman
  raw-fallback branch.

## Misc hardening

- `KIMI_CRON_CLOCK=file:<path>` reads at most 64 bytes via
  `openSync` + `readSync` so a stray-large file can't OOM.
- SIGUSR1 handler logs swallowed `tick()` exceptions to stderr when
  `KIMI_CRON_DEBUG=1` (matches scheduler's debug pattern); silent in
  production.
- Documentation rewrite across cron-list.md / cron-create.md /
  cron-delete.md so the documented stale + nextFireAt behaviour
  matches the implementation (recurring stale tasks auto-delete after
  the final fire; `nextFireAt` is an ISO timestamp; refresh ritual
  is "just CronCreate again — the old id is already gone").

Tests cover each of the above. Suite at 2090+ passing across
agent-core, kosong, and the kimi-code app; typecheck clean across
all workspace packages.

* feat(cron): persist scheduled tasks across kimi resume

Add per-id JSON persistence so cron tasks survive a kimi resume of the same session.

Core changes:
- CronManager: addTask / removeTasks mirror mutations to <sessionDir>/cron/<id>.json
- CronManager.loadFromDisk() rehydrates the in-memory store on resume
- CronManager.flushPersist() drains pending writes for graceful shutdown
- SessionCronStore.adopt() inserts persisted tasks with original id and createdAt
- Extract shared createPerIdJsonStore utility from background/persist.ts
- Refactor background/persist.ts to use createPerIdJsonStore (no behavior change)
- New tests: resume.test.ts, persist.test.ts, per-id-json-store.test.ts
- Update cron-create / cron-list / cron-delete docs to reflect session lifetime

* fix(agent-core): fix cron-stop-on-close test import for tsgo compat

* feat(cron): persist lastFiredAt cursor and scope approval rules

- persist lastFiredAt across resume so recurring tasks don't replay
- add one-shot pinned-date guard to reject >1-year-out first fires
- scope CronCreate approvalRule to exact payload (cron, prompt, recurring)
- add resume replay tests and corrupt-cursor fallback test
- stop cron before awaiting background shutdown so due ticks cannot
  start a fresh turn while session.close() is mid-flight
- filter cron_job / cron_missed origins from markdown export so the
  internal <cron-fire ...> envelope no longer leaks into user-facing
  exports
2026-05-28 21:05:46 +08:00
_Kerman
28d2b5c018
refactor(agent-core): make Agent constructable and consolidate provider-manager (#161) 2026-05-28 20:33:10 +08:00
liruifengv
07dd604c3c
feat: add /auto slash command and --auto CLI flag (#163)
* feat: add /auto slash command and --auto CLI flag

* feat: add /auto slash command and --auto CLI flag
2026-05-28 20:24:50 +08:00
liruifengv
f3c1015b67
feat(cli): add clickable changelog link to update prompt (#162)
Render a clickable OSC 8 hyperlink pointing to the release notes page
in the update available prompt, so users can easily review changes
before deciding to install.
2026-05-28 20:02:43 +08:00
qer
c88b7bf0ef
fix: preserve datasource response files (#159)
* fix: preserve datasource response files

* fix: restrict datasource response file writes

* test: avoid async http handler
2026-05-28 19:30:35 +08:00
caiji
d1f9a83d7a
fix(tui): cap session-title length at 32 chars (#158)
Resolve #128
2026-05-28 18:57:25 +08:00
liruifengv
56053f4ff5
chore: reduce default thinking preview lines to 2 (#152) 2026-05-28 17:14:29 +08:00
_Kerman
a6d379b2ce
feat: offload large base64 media payloads to external blob files (#117) 2026-05-28 16:47:57 +08:00