* 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
* 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
* 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
* 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
* feat(export): record install source and shell environment in manifest
Capture how the CLI was installed (npm-global, native, etc.) and the
user's terminal environment (TERM, TERM_PROGRAM, multiplexer, SHELL)
so exported session archives carry richer diagnostic context.
* chore: add changeset for export manifest enhancements
* chore: simplify changeset description
* fix(agent-core): always pair tool.call with tool.result on malformed returns
A tool returning undefined, a primitive, or an object without a valid
output field crashed normalization, exited the batch loop without
dispatching the matching tool.result, and left the next provider
request to fail with a missing tool_call_id response.
- Validate the tool return at the boundary in runRunnableToolCall via a
new coerceToolResult helper; malformed returns become an isError
result.
- Harden normalizeToolResult and toolResultStopsTurn against
non-conformant input from synthetic results or finalize hooks.
- Track unpaired tool.call ids in runToolCallBatch and emit
compensating error results in finally for any that did not receive a
result.
* refactor(agent-core): simplify tool result pairing
Drop the unpairedCallIds tracking and finally-compensation wrapper from
runToolCallBatch. The set was updated before dispatchEvent, so a failure
between the two left it inconsistent; the per-call try/catch already
produces a paired error result, making the outer wrapper redundant.
Coerce hook returns at the synthetic and finalizeToolResult boundaries
so a malformed hook output is normalized into a paired error result the
same way a malformed tool return already is.
* fix(kosong): make OpenAI-compatible thinking work without reasoning_key
Reasoning field names (reasoning_content / reasoning_details / reasoning)
are protocol facts, not user preferences. Treating reasoning_key as a
required user-set field meant any path that didn't go through the catalog
— hand-written config.toml in particular — silently lost thinking content
and broke strict gateways like DeepSeek.
Demote reasoning_key to an internal protocol constant with an explicit
override:
- Inbound (stream + non-stream): scan reasoning_content,
reasoning_details, reasoning in order; first string value wins. An
explicit reasoning_key restricts the scan to that one field.
- Outbound: serialize ThinkPart back as reasoning_content by default.
An explicit reasoning_key writes to that field instead.
- reasoning_effort auto-injection no longer requires reasoning_key;
presence of ThinkPart in history is enough.
Catalog plumbing is unchanged — explicit values from the catalog still
win, the default just stops being undefined.
Manually verified end-to-end against the real DeepSeek API with a
hand-written config.toml that does not set reasoning_key: thinking
content renders, no 400, multi-turn conversations work.
* fix(kosong): normalize blank reasoning_key to unset
ModelAliasSchema accepts `reasoning_key = ""` (z.string().optional()).
A blank value used to disable the default field scan and route both
inbound reads and outbound writes through an empty property name.
Trim and treat empty as undefined at the provider boundary so the
default protocol behavior applies.
* fix(kosong): preserve caller-pinned reasoning_effort during auto-inject
When the history contains ThinkPart, generate() injects
reasoning_effort='medium' and then assigns it onto createParams,
which used to silently overwrite a value the caller set via
withGenerationKwargs({ reasoning_effort: 'high' }). Skip auto-inject
when an explicit reasoning_effort already lives in kwargs.
* feat(agent-core): re-export wire record types for in-monorepo consumers
* chore(vis): purge legacy wire protocol code
* feat(vis): introduce single-source agent-record types
* test(vis): add fixture session and builder helper
* feat(vis): implement new session store reader
* feat(vis): wire new session list/detail routes
* refactor(vis): drop legacy path config
* feat(vis): adapt session list page to new DTO
* feat(vis): implement per-agent wire reader
* feat(vis): rewrite wire route for new protocol
* feat(vis): rewrite wire type metadata for new protocol
* feat(vis): rewrite wire row + headline for new record union
* feat(vis): wire tab detail panel + multi-agent selector
* feat(vis): rebuild wire issues detection for new protocol
* feat(vis): implement context projector
* feat(vis): rewrite context route on projector
* feat(vis): rebuild context tab for new ContextMessage shape
* feat(vis): implement agent tree builder
* feat(vis): rewrite agents route
* feat(vis): rebuild subagents tab around state.json.agents
* feat(vis): rebuild state tab on raw state.json
* chore(vis): purge residual legacy field references
* vis: rewrite complete on new agent-core protocol
* fix(vis): adapt to wire protocol 1.1 with flattened tool calls
* fix(vis): populate workDir from session index
* fix(vis): return broken-state sessions from detail lookup
* fix(vis): tolerate per-session wire read failures during listing
* fix(vis): read wire files from canonical session path
* feat(vis): restore session detail page with full tab layout
* feat(vis): wire subagent context tab to real ContextTab
* fix(vis): sync wire and context tab agentId with prop changes
* feat(vis): auto-pick a free dev port when 3001 is busy
* feat(vis): accept v1.0 wire files via agent-core migration chain
* refactor(vis): default API to 5174 and pick a non-colliding vite port
* feat(vis): make long strings expandable with copy in JsonViewer
* fix(vis): stop gating session health on protocol version
* refactor(vis): split wire records into raw + projected; best-effort unknown protocol
* feat(vis): pair tool.call with tool.result via inline cross-reference and hover highlight
* feat(vis): open session folder and copy its path from the detail header
* fix(vis): reconstruct assistant and tool messages from loop events
* fix(vis): keep system prompt bubble within the message column width
* feat(vis): collapse tool result bubble by default in context view
* feat(vis): expose broken_main_wire in the session health filter
* fix(vis): reset wire and context tab agent when navigating sessions
* fix(vis): fall back to a generic headline for unknown wire record types
* fix(vis): emit compaction summary as an assistant message with origin
* fix(vis): harden session-store reads for broken wires, broken state, and path-traversal agent ids
* feat(vis): inline image previews for image_url content parts
* fix(agent-core): resolve user skills from OS home directory, not kimi home
KimiCore incorrectly used the kimi home directory (e.g. ~/.kimi-code) as
userHomeDir, causing the skill scanner to look for user skills under
~/.kimi-code/.agents/skills/ instead of ~/.agents/skills/. Only the
builtin mcp-config skill was found.
Fix by always setting userHomeDir to homedir() (the actual OS home),
independent of the homeDir / KIMI_CODE_HOME options that control where
session data is stored.
* chore: add changeset for skill user home dir fix
* test(node-sdk): align SDK skill test with OS home resolution fix
* Apply suggestion from @liruifengv
Signed-off-by: liruifengv <liruifeng1024@gmail.com>
---------
Signed-off-by: liruifengv <liruifeng1024@gmail.com>
Co-authored-by: liruifengv <liruifeng1024@gmail.com>
* fix(catalog): preserve reasoning fields
* fix(catalog): treat interleaved=true as reasoning_content
models.dev documents `interleaved` as `boolean | { field }`, where the
bare boolean means "general support" without an explicit field name.
The previous branch returned undefined for `true`, leaving openai-compat
gateways that publish `interleaved: true` without a round-tripped
reasoning field. Map `true` to the default `reasoning_content` so those
models still surface and replay thinking content.
* fix(catalog): preserve interleaved field in built-in catalog snapshot
`update-catalog.mjs` drives the bundled catalog that ships with release
builds and is the default source for `/connect`. The allowlist dropped
`interleaved`, so even after the runtime learned to read the field, the
default offline path never sees it — reasoning round-tripping silently
stayed off for openai-compat models in release builds. Keep
`interleaved` so the bundled snapshot carries the same metadata as the
live models.dev catalog.
---------
Co-authored-by: 7Sageer <7sageer@djwcb.cn>
* feat: add /connect command with models.dev catalog support
Add a /connect slash command that configures a provider and model
from a models.dev-style catalog. Users no longer need to hand-write
model metadata (context window, output limit, capabilities).
Architecture (3 layers):
- kosong: pure data layer — Catalog schema, inferWireType,
catalogModelToCapability
- node-sdk: IO + config write — fetchCatalog, applyCatalogProvider,
catalogModelToAlias
- app: TUI flow — /connect command, provider/model selection,
credential input, config persistence
UI improvements in this PR:
- ChoicePickerComponent: add searchable (fuzzy filter + search bar)
- ModelSelectorComponent: add searchable (same)
- Extract reusable paging.ts for list pagination
Changesets included for kosong, kimi-code-sdk, and kimi-code.
* feat: bundle pruned models.dev catalog for offline /connect
When the network is unavailable, /connect now falls back to a built-in
snapshot of the models.dev catalog.
- `scripts/update-catalog.mjs`: fetches models.dev/api.json, strips
unnecessary fields, and writes `src/built-in-catalog.ts` with the
JSON string as a TS constant.
- `loadBuiltInCatalog(text?)` in node-sdk: parses the JSON string safely;
returns undefined on any failure.
- `handleConnectCommand`: on fetch failure, shows an informative offline
message and tries the built-in snapshot.
- The snapshot file is a placeholder (`undefined`) in source control;
`update-catalog.mjs` populates it before release builds so the actual
catalog is inlined into the bundle by rolldown.
* refactor(tui): share search and pagination across list pickers
ChoicePicker and ModelSelector each carried their own copy of the cursor +
fuzzy-search + pagination state machine. Extract it into a reusable
SearchableList so both pickers share one implementation; behavior is unchanged.
* fix(tui): filter unsupported catalog providers
* docs(tui): clarify /connect stale-alias cleanup depends on removeProvider
* fix(kimi-code): inject built-in catalog at release time
* feat(tui): hint at /login and /connect when /model has no models
Replace the bare "No models configured." error with a notice that
points users to /login for Kimi and /connect for other providers.
* fix(tui): tighten /connect error reporting for edge cases
Two silent-failure cases in /connect could leave users without any
feedback to act on:
- Reject `--url` when its value is missing (e.g. `/connect --url` or
`/connect --url=`). Previously the argument parser silently fell
back to the default catalog, so a malformed flag still appeared to
succeed but with the wrong source.
- Show an explicit error when the resolved catalog yields no providers
with supported wire types. Previously the picker resolved with no
selection and the command returned without any UI feedback.
* chore(tui): restore slash invalid intent type
* fix(tui): support /logout for /connect-configured providers
After /connect writes a non-managed provider (e.g. openai), /logout
fell through to "Nothing to logout." because the handler only matched
the managed default and isOpenPlatformId branches, leaving users no
in-app way to drop the API key and model aliases they just configured.
Collapse the OpenPlatform branch into a generic "provider is present
in config" check so any non-managed provider in config — OpenPlatform
OAuth targets and /connect catalog providers — goes through the same
removeProvider path.
* fix(tui): reject --url values that are not http(s) URLs
`resolveConnectCatalogRequest` previously matched any non-space token
after `--url` as the URL, so `/connect --url --refresh` parsed
`--refresh` as the value and bypassed the missing-value error path.
Bare non-URL tokens (`/connect --url not-a-url`) and non-http(s)
schemes were also silently accepted.
Constrain the captured value to `https?://...` so flag-like and
non-URL tokens fall through to the existing `URL_FLAG_PRESENT_RE`
check and surface a clear error.
* ci(native): scope built-in catalog generation to signed macOS jobs
The catalog-generation step ran whenever `inputs.sign-macos` was true,
including Linux and Windows targets that take the local-profile build
path and never consume the generated catalog. A transient models.dev
outage would therefore fail unrelated artifact builds.
Match the condition to the macOS signed release-profile build that
actually consumes the bundled catalog.
* fix(ci): embed built-in catalog in non-macOS native artifacts
The earlier narrowing to `runner.os == 'macOS' && inputs.sign-macos`
relied on a misread of build.mjs: its `profile === 'release'` guard
only auto-fetches the catalog as a dev fallback. Whether the binary
actually embeds the catalog is decided by tsdown's define at bundle
time, which reads KIMI_CODE_BUILT_IN_CATALOG_FILE regardless of
profile.
Linux and Windows release artifacts therefore lost their bundled
catalog and silently regressed offline /connect on those targets.
Restore generation for all OS jobs when sign-macos is true.
* chore(tui): mention /connect in welcome panel hints
Align the welcome panel with the /model picker so the empty-state copy
points users to both /login and /connect.
* feat(migration-legacy): migrate user skills from kimi-cli
The first-launch migration previously left ~/.kimi/skills/ behind: the
new scanner only reads ~/.kimi-code/skills/ and ~/.agents/skills/, so any
custom skills authored against kimi-cli silently disappeared after the
upgrade. Adds a skills step that copies top-level entries from
~/.kimi/skills/ into ~/.kimi-code/skills/ with skip-existing semantics,
wires it into the existing run-migration pipeline and result screen, and
surfaces the count alongside config/mcp/REPL-history.
* refactor(migration): hide OAuth from migration UX
OAuth credentials are deliberately never migrated (refresh tokens
rotate server-side, so two installs sharing one token would fight over
who gets refreshed). The previous UX framed this as a limitation: the
result screen carried a yellow ⚠ "kimi-cli login not migrated — run
/login" line, and the pre-migration summary listed "kimi-cli login
(needs /login)" alongside real migratable data classes, making users
think a login was about to be transferred and only the last step had
failed.
Drops both surfaces and short-circuits the pre-migration screen when
the only legacy data is `credentials/*.json`. kimi-code's own /login
flow handles re-auth on first use, so a dedicated migration notice is
redundant. The `report.notices.oauthLoginsRequiringRelogin` JSON field
is left intact for debugging.
* chore: changeset for skills migration and OAuth UX cleanup
* feat(agent-core): add agent record migrations
* test(agent-core): include wire metadata in subagent fixtures
* docs(agent-core): document wire version format
- destroy stdout/stderr on abort to release stdio pipes held by detached daemons\n- use `exit` instead of `close` event to resolve exit promise
Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
Replace the single threshold of 7 with tiered reminders triggered at streak counts 3, 5, and 8.\nThe first reminder is a generic nudge, while the second and third include\nthe tool name, repeat count, and arguments for stronger guidance.
Co-authored-by: haozhe.yang <yanghaozhe@moonshot.ai>
Co-authored-by: Kai <me@kaiyi.cool>