Commit graph

207 commits

Author SHA1 Message Date
_Kerman
e280f33daf
fix: recover from model token limit errors (#207) 2026-05-29 17:27:59 +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
_Kerman
07d51e4add
chore(agent-core): move tool services type (#206) 2026-05-29 17:20:57 +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
qer
3da4daeade
fix(kosong): retry when a response stream is terminated mid-flight (#201)
A mid-stream SSE drop surfaces as a raw undici `TypeError: terminated`, which was classified as a non-retryable generic error and failed the turn on the first attempt. Route raw transport-layer errors through the connection-error heuristic so a dropped stream becomes a retryable APIConnectionError and is retried transparently. User aborts (ESC) are unaffected — the retry loop checks the abort signal before retrying.

Related to #149.
2026-05-29 15:18:55 +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
_Kerman
3a0e06031a
fix: project persisted context messages (#195) 2026-05-29 14:43:17 +08:00
_Kerman
8c77cfab62
fix(agent-core): handle ripgrep cross-device install (#198) 2026-05-29 14:42:56 +08:00
_Kerman
8de720434f
chore: add docs to pnpm-workspace (#196) 2026-05-29 14:34:05 +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
qer
1873859b0e
refactor(agent-core): slim llm request log line (#190)
Merge turnId/step into a single `turnStep` field ("0.1") and
attempt/maxAttempts into `attempt` ("2/3"), and drop the
messageCount/toolCallCount fields. The per-request `llm request`
line goes from up to 8 fields down to ~3; the `llm config` line
(including thinkingEffort, logged for all providers) is unchanged.
2026-05-29 13:36:34 +08:00
_Kerman
564721fe16
fix: clarify subagent and background task stop messages as user-initiated (#189) 2026-05-29 13:22:34 +08:00
_Kerman
537cf20d18
feat: remove default per-turn step limit of 1000 (#186) 2026-05-29 13:11:45 +08:00
Haozhe
c2bd60fce4
ci(release): reorder Nix installation before setup-node (#188)
Nix's profile prepends its bin directory to PATH, shadowing the Node.js version installed by setup-node. Moving Nix installation before setup-node ensures the Node >=24 PATH entry remains first, fixing `ERR_PNPM_UNSUPPORTED_ENGINE` during release.
2026-05-29 12:51:33 +08:00
Haozhe
092a9a8c8d
test(agent-core): use deterministic jitter id in cron pending-jitter test (#187) 2026-05-29 12:42:58 +08:00
_Kerman
114777e859
refactor(agent-core): split RuntimeConfig into Kaos and ToolServices (#185) 2026-05-29 12:28:47 +08:00
Haozhe
3c18987c0b
ci(release): sync Nix pnpmDeps hash during release PR generation (#184)
- Add version:release script combining changeset version with Nix hash update
- Update release workflow to install Nix and use version:release
- Document updated hash refresh workflow in for-agents/workflows.md
- Include changeset for the fix
2026-05-29 12:25:45 +08:00
qer
68df4b8b84
docs: simplify plugins documentation (#169)
* docs: simplify plugins documentation

* docs: restore plugin caveats lost during simplification

Re-add three behaviors that were dropped from the simplified plugins
docs: the stdio MCP `cwd` must start with `./`, local-path installs run
from the managed copy (so editing the source after install requires a
reinstall), and `/plugins remove` only deletes the install record while
leaving files on disk. Mirror the changes in both en and zh.
2026-05-29 02:21:19 +08:00
liruifengv
681ccc5b85
chore(changelog): sync 0.5.0 and reword 'assistant' to 'agent' (#171) 2026-05-28 22:46:50 +08:00
_Kerman
b5981a523b
feat(agent-core): ModelProvider interface and SingleModelProvider (#167) 2026-05-28 22:27:09 +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
liruifengv
850aad461b
docs: document scheduled task tools (#168) 2026-05-28 22:12:54 +08:00
qer
92e1d8c72b
fix: count discovered plugin skills (#166) 2026-05-28 22:05:53 +08:00
_Kerman
74e867a300
refactor(agent-core): move HookEngine to sessions (#165) 2026-05-28 21:35:42 +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
happy wang
3e72f25ad9
fix(migration): map default_yolo to default_permission_mode instead of dead yolo field (#124)
* fix(migration): map default_yolo to default_permission_mode instead of dead yolo field

The kimi-cli config key `default_yolo` was being migrated to `yolo` in
kimi-code config.toml, but `yolo` is a dead field that no code reads.
The real config key for yolo mode is `default_permission_mode = "yolo"`.

* chore: add changeset for migration yolo mapping fix
2026-05-28 19:32:08 +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
36add70d57
refactor(kaos): move Environment into kaos, slim package API (#147) 2026-05-28 16:50:10 +08:00
_Kerman
a6d379b2ce
feat: offload large base64 media payloads to external blob files (#117) 2026-05-28 16:47:57 +08:00
_Kerman
8515472476
fix(agent-core): compaction edge cases, error handling, and truncated output (#120) 2026-05-28 16:47:38 +08:00
liruifengv
8b5a25161c
feat(tui): show full text for long bash commands and AskUserQuestion content (#150)
* feat(tui): reveal full bash command on ctrl+o expand

The Bash tool header truncates long commands at 60 chars and the
result renderer never showed the command body, so the full command
was nowhere to be found in the UI. When the user expands the card
with ctrl+o, render the complete multi-line command above the
output.

* feat(tui): wrap long text in AskUserQuestion dialog instead of truncating

Replace single-line truncation with hanging-indent word wrap for the
question prompt, body description, option label, option description,
and submit-tab review entries. Long content now flows onto multiple
rows instead of being cut off with an ellipsis.
2026-05-28 15:54:00 +08:00
liruifengv
76cbf86e20
fix(tui): cap todo panel at 5 rows with overflow indicator (#146)
* fix(tui): cap todo panel at 5 rows with overflow indicator

Pick visible todos around in_progress (recent done + upcoming pending),
append `+N more` when truncated.

* fix(tui): make todo selector order-agnostic

The previous selector assumed todos were grouped as done...pending
when no in_progress existed. On interleaved orderings (the TodoList
tool keeps the model-provided order without normalizing) the before
and after windows skipped most candidates and produced fewer than
five rows. Pick directly from status buckets instead.
2026-05-28 15:17:29 +08:00
_Kerman
52bdc6fced
perf(tui): cache Text instance in ThinkingComponent to avoid per-frame re-wrap (#144) 2026-05-28 14:31:24 +08:00
liruifengv
dad2b87cee
refactor(tui): split kimi-tui God-class into controllers and command modules (#142)
* refactor(tui): extract TasksBrowserController from KimiTUI

将 /tasks 浏览器相关的 16 个方法(~450 行)从 KimiTUI 提取到独立的
TasksBrowserController。KimiTUI 通过 Host 接口传 this 给 controller,
保留 4 个一行委托方法供外部调用点使用。

kimi-tui.ts: 6160 → 5711 行 (-449)

* refactor(tui): extract StreamingUIController from KimiTUI

将 Streaming Flush(15 个方法)和 Live Render Hooks(15 个方法)
共 ~475 行代码从 KimiTUI 提取到 StreamingUIController。这两个区域
之间紧密耦合(flush 方法直接调用 render hooks),合并提取避免循环依赖。

同时迁移了 5 个实例变量:flushTimer, lastFlushAt, pendingAssistantFlush,
pendingThinkingFlush, pendingToolCallFlushIds。

kimi-tui.ts: 5711 → 5268 行 (-443)

* refactor(tui): extract SessionEventHandler from KimiTUI

将 Session Events(39 个方法)和 Background Task Lifecycle(4 个方法)
共 ~960 行代码从 KimiTUI 提取到 SessionEventHandler。包含事件分发、
turn/step 生命周期、thinking/assistant delta 处理、tool call 管理、
compaction 流程、subagent 管理、MCP 状态渲染、background task 事件。

kimi-tui.ts: 5268 → 4316 行 (-952)

* refactor(tui): extract SessionReplayRenderer from KimiTUI

将 Session Replay 的 21 个方法(~410 行)从 KimiTUI 提取到
SessionReplayRenderer。包含会话历史回放水合(snapshot hydration)、
replay record 渲染、thinking/assistant/tool call 回放、skill 激活、
权限变更、审批结果、background task 通知等逻辑。

kimi-tui.ts: 4316 → 3911 行 (-405)

* refactor(tui): extract slash command handlers from KimiTUI

将 Slash Command Handlers 区域(~765 行)从 KimiTUI 提取到两个文件:
- slash-commands.ts (684 行):命令逻辑(plan/yolo/compact/editor/theme/
  model/title/fork/init/login/connect/logout/feedback)
- slash-command-prompts.ts (183 行):UI prompt 函数(platform selection、
  logout provider、feedback input、API key、model selector 等)

prompt 函数拆为独立模块避免 ESM 同模块内部调用 mock 不生效的问题。
测试改为 vi.mock 对应的 prompt 模块。

kimi-tui.ts: 3911 → 3163 行 (-748)

* refactor(tui): extract AuthFlowController from KimiTUI

将 Auth / Model Bootstrap 的 6 个方法(~115 行)从 KimiTUI 提取到
AuthFlowController。包含 refreshAvailableModels、
enterLoginRequiredStartupState、activateModelAfterLogin、
clearActiveSessionAfterLogout、refreshConfigAfterLogin、
refreshConfigAfterLogout。

kimi-tui.ts: 3163 → 3063 行 (-100)

* refactor(tui): clean up dead imports after controller extraction

移除 kimi-tui.ts 和 controller 文件中因代码提取而残留的 55+ 个
无用 import(SDK event types、OAuth 工具函数、catalog 类型等)。

kimi-tui.ts: 3063 → 3005 行 (-58)

* refactor(tui): remove delegate methods and direct controller references

Phase A: 删除 14 个零调用者的纯委托方法
Phase B: 内联 8 个少量调用者委托(调用处直接引用 controller)
Phase C: controller 之间通过 host 上的 controller 引用直接协作,
         不再绕 kimi-tui 中转(SessionEventHandler → tasksBrowserController,
         SlashCommands → authFlow)

kimi-tui.ts: 3005 → 2956 行 (-49)

* refactor(tui): move pickers, config apply, info commands to slash-commands controller

Phase D: 将 pickers(editor/model/theme/permission/settings)、
config apply(applyEditorChoice/applyThemeChoice/applyPermissionChoice/
performModelSwitch/persistModelSelection)、info commands
(showUsage/showStatusReport/showMcpServers + load* helpers)共 ~400 行
从 kimi-tui.ts 搬到 slash-commands controller。

Phase E: handleBuiltInSlashCommand 中所有 case 直接调用 slashCommands.*,
删除全部 13 个 slash command 委托方法。测试改为直接调用 controller 函数。

kimi-tui.ts: 2956 → 2566 行 (-390)

* refactor(tui): final cleanup — remove last delegates and dead imports

删除 finalizeTurn 和 activateModelAfterLogin 委托方法,
slash-commands 直接通过 host.streamingUI / host.authFlow 调用。
清理 15 个因搬运产生的 dead import。

kimi-tui.ts: 2566 → 2545 行 (-21)

* refactor(tui): clean up dead imports and empty import blocks

清理第二轮搬运后残留的 19 个 dead import 和 3 个空 import 块
(api-key-input-dialog、feedback-input-dialog、settings-selector、
feedback constants 等已搬到 slash-commands controller)。

kimi-tui.ts: 2545 → 2517 行 (-28)

* refactor(tui): inline last auth delegate methods

将 refreshAvailableModels 和 enterLoginRequiredStartupState 的
调用方改为直接访问 this.authFlow.*,删除最后两个委托方法。

kimi-tui.ts: 2517 → 2510 行 (-7)

* refactor(tui): move slash command dispatch to slash-commands controller

将 executeSlashCommand(~45 行)和 handleBuiltInSlashCommand(~80 行)
从 kimi-tui.ts 搬到 slash-commands controller。kimi-tui.ts 的
handleUserInput 现在只做空检查、replay guard、历史持久化,
然后调 slashCommands.dispatchInput(this, text)。

kimi-tui.ts: 2510 → 2367 行 (-143)

* refactor(tui): clean up 38 dead imports and 3 dead interfaces

移除因前几轮搬运残留的 import(message-replay、mcp-server-status、
background-*-status、hook-result-format、event-payload utils、
streaming/theme constants 等)和 SessionUsageResult / RuntimeStatusResult /
ManagedUsageResult 接口定义。

kimi-tui.ts: 2367 → 2311 行 (-56)

* refactor(tui): deduplicate combineStartupNotice and isOAuthLoginRequiredError

将 combineStartupNotice(kimi-tui.ts + auth-flow.ts 各一份)和
isOAuthLoginRequiredError 统一到 constant/kimi-tui.ts,两边改为 import。

kimi-tui.ts: 2311 → 2296 行 (-15)

* refactor(tui): move startup utils out of constant/kimi-tui

combineStartupNotice 和 isOAuthLoginRequiredError 是工具函数不是常量,
从 constant/kimi-tui.ts 移到 utils/startup.ts。

* refactor(tui): remove last session event/replay delegate methods

删除 handleEvent、startSessionEventSubscription、
hydrateTranscriptFromReplay 三个委托方法。
kimi-tui 内部和 auth-flow controller 改为直接访问
sessionEventHandler.startSubscription() 和
sessionReplay.hydrateFromReplay()。
测试改为通过 driver.sessionEventHandler 直接调用。

kimi-tui.ts: 2296 → 2278 行 (-18)

* refactor(tui): reduce TUIState fields by removing redundancy and merging pairs

Delete 4 redundant fields (AppState.yolo, AppState.isStreaming,
TUIState.backgroundAgents, TUIState.assistantStreamActive) that were
derivable from existing state. Merge 4 pairs of always-coupled fields
into single objects (streamingBlock, subagentInfo, activitySpinner,
activeDialog). Demote 3 fields only used by KimiTUI itself to private
class fields. Net reduction: AppState 22→20, TUIState 55→48 fields.

* refactor(tui): extract TUIState types and move streaming state into StreamingUIController

Break the type-level circular dependency where controllers imported
TUIState from kimi-tui.ts while kimi-tui.ts imported runtime values
from controllers. Shared types now live in tui-state.ts (TUIState,
createTUIState) and types.ts (KimiTUIOptions, LoginProgressSpinnerHandle,
etc.) so the import graph is strictly unidirectional.

Move 12 streaming-related fields (currentTurnId, currentStep,
assistantDraft, thinkingDraft, streamingBlock, activeThinkingComponent,
activeCompactionBlock, activeToolCalls, streamingToolCallArguments,
pendingToolComponents, pendingAgentGroup, pendingReadGroup) from the
flat TUIState bag into StreamingUIController as instance properties,
along with 3 dispose methods. TUIState shrinks from 40+ to 28 fields.

* refactor(tui): split slash-commands.ts by domain into 4 focused modules

Extract the 1223-line slash-commands.ts into domain-specific files:

- auth-commands.ts (~350 lines): login, logout, connect, OAuth flows
- config-commands.ts (~380 lines): plan, yolo, compact, model/theme/
  editor/permission pickers and apply logic
- info-commands.ts (~185 lines): feedback, usage, status, mcp reports
- session-commands.ts (~105 lines): title, fork, init

slash-commands.ts is now a slim routing hub (~210 lines) that owns
SlashCommandHost, dispatchInput, and the builtin command switch. All
public handlers are re-exported so existing consumers are unaffected.

* refactor(tui): move command handlers from controllers/ to commands/

Relocate the 6 command-related files into the existing commands/
directory where parsing and routing logic already lives:

  controllers/slash-commands.ts       → commands/dispatch.ts
  controllers/slash-command-prompts.ts → commands/prompts.ts
  controllers/auth-commands.ts        → commands/auth.ts
  controllers/config-commands.ts      → commands/config.ts
  controllers/info-commands.ts        → commands/info.ts
  controllers/session-commands.ts     → commands/session.ts

controllers/ now contains only state/lifecycle controllers
(auth-flow, session-event-handler, session-replay, streaming-ui,
tasks-browser). commands/index.ts re-exports all public symbols.

* refactor(tui): move background/render-dedup state into SessionEventHandler

Move 7 fields from TUIState into SessionEventHandler as instance
properties: backgroundAgentMetadata, backgroundTasks,
backgroundTaskTranscriptedTerminal, subagentInfo,
renderedSkillActivationIds, renderedMcpServerStatusKeys,
mcpServerStatusSpinners.

Add resetRuntimeState() to SessionEventHandler that clears all 7
fields in one call. TUIState shrinks from 28 to 21 fields.

TasksBrowserHost.backgroundTasks is now a top-level ReadonlyMap
property instead of being embedded in the state subset, with a
getter on KimiTUI that delegates to sessionEventHandler.

* refactor(tui): encapsulate StreamingUIController internal state behind semantic methods

Make 12 fields private and expose 18 semantic methods instead of letting
SessionEventHandler, SessionReplayRenderer, and KimiTUI directly
manipulate internal maps and counters. Key methods: registerToolCall(),
accumulateToolCallDelta(), completeToolResult(), markStepTruncated(),
appendThinkingDelta(), appendAssistantDelta(), cleanupAfterReplay().

* refactor(tui): extract EditorKeyboardController from KimiTUI

Move editor callback wiring, pending-exit state, clipboard media
handling, and external editor logic into a dedicated controller.
KimiTUI drops from 2070 to 1814 lines.

* refactor(tui): clean up kimi-tui.ts — strip noise comments, reorganize sections

Remove ~70 single-line comments that merely restated the method name.
Condense multi-paragraph inline comments (signal handlers, start()) to
one-liners that capture the WHY. Reorganize sections: merge the one-
method "Layout" section into Lifecycle, rename "Startup Helpers" to
"Autocomplete & Skill Commands", move stray accessors into "State &
Accessors", delete the empty trailing section, relocate input-history
helpers next to each other. 1814 → 1639 lines.

* refactor(tui): route TUIState field mutations through host methods

Controllers may still read host.state freely, but all direct field
assignments now go through setter methods on the host: setStartupReady,
clearQueuedMessages, shiftQueuedMessage, pushTranscriptEntry,
setExternalEditorRunning, setTasksBrowser. This prevents controllers
from silently mutating shared state without KimiTUI's knowledge.

* chore(tui): drop merge analysis docs, add changeset, fix 2 lint errors

- delete docs/refactor-kimi-tui-{analysis,merge-plan}.md (working notes)
- add changeset for the kimi-tui split refactor
- session-event-handler.ts: drop unused `state` destructure in finishCompaction
- editor-keyboard.ts: void-mark fire-and-forget session.cancel() promise

* chore(changeset): simplify wording
2026-05-28 14:14:20 +08:00
liruifengv
50251a1360
fix(approval): show file content/diff and open full-screen preview on ctrl+e (#139)
* fix(approval): include file content and diff in approval display

After #26 the WriteTool/EditTool input display was reduced to
`{kind: 'file_io', operation, path}`, dropping the args carried by the
previous generic fallback. The approval panel then only had a path to
show — no file content for Write, no diff hunk for Edit — and ctrl+e
expanded to the same one-liner.

Extend the file_io display with optional `content` / `before` / `after`
fields so Write can attach its full content and Edit can attach its
old_string/new_string hunk. The adapter promotes file_io+content to a
file_content block and file_io+before/after to a diff block, matching
what the panel renders for the legacy generic-fallback path.

* feat(tui): open full-screen viewer for approval previews

Inline ctrl+e expand-in-place inflated the approval panel past one
viewport for any non-trivial Edit / Write, which collided with pi-tui's
inline differential renderer and the terminal's "snap to bottom on
stdout" reflex: scrolling back glitched and the screen flickered. On
top of that, the diff renderer's O(m·n) LCS DP ran every frame the
panel was visible, so each spinner tick re-paid the cost.

Make ctrl+e hand off to a dedicated full-screen viewer instead. The
viewer renders all body lines once at construction and slices them on
scroll, so per-frame cost is O(viewport) regardless of payload size.
It uses the same nested-takeover pattern as TaskOutputViewer; the
approval panel instance is preserved and refocused on close so the
selection / feedback state survives.

The panel itself drops its local `expanded` toggle and always renders
the compact cluster view; ctrl+e now exclusively forwards to the host
when there is something to preview, and falls through to the existing
plan-expand toggle otherwise.

* chore(changeset): restore approval previews
2026-05-28 13:22:33 +08:00
liruifengv
16e881ed64
docs(changelog): sync 0.4.0 from apps/kimi-code/CHANGELOG.md (#125) 2026-05-27 23:31:03 +08:00
github-actions[bot]
fa114c150d
ci: release packages (#93)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-27 22:50:19 +08:00
qer
ebf6e8181e
feat: add plugin manager and official plugins (#119)
* feat: add plugin manager and official plugins

* fix(agent-core): honor plugin capability overrides

* fix: restrict plugin zip root detection

* Update apps/kimi-code/src/constant/app.ts

Co-authored-by: liruifengv <liruifeng1024@gmail.com>
Signed-off-by: qer <wbxl2000@outlook.com>

---------

Signed-off-by: qer <wbxl2000@outlook.com>
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
2026-05-27 22:47:33 +08:00
liruifengv
2c7a8cc010
feat(tui): expand paste markers on second paste (#116)
* feat(tui): expand paste markers on second paste

When the cursor sits on a folded paste marker (e.g. `[paste #1 +15 lines]`)
and the user pastes again (Ctrl-V or bracketed paste), the marker is expanded
back to its original content instead of inserting new clipboard data.

* chore: add changeset for paste marker expansion

* fix(tui): preserve paste content after marker expansion for undo

Stop deleting the paste entry from the Map after expansion so that
undo → re-expand still works.

* fix(tui): buffer consumed paste data to handle split end sequences

Accumulate chunks while consuming discarded paste data so a
split ESC[201~ across chunks still resets consumingPaste.
2026-05-27 20:19:18 +08:00
_Kerman
2b74025302
feat: rework permission decision policies (#26) 2026-05-27 20:07:24 +08:00
liruifengv
b7e7404d90
docs: document /export-md and /export-debug-zip slash commands (#114)
Add reference table entries for the new in-TUI export commands, and
extend the Sessions guide so users can discover them alongside the
existing `kimi export` CLI subcommand. Mirrored across en and zh.
2026-05-27 19:15:14 +08:00
liruifengv
028d069b12
feat(tui): add /export-md slash command (#113)
* 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
2026-05-27 18:51:44 +08:00