The Write tool previously failed when a parent directory was missing, forcing a manual mkdir round trip. It now creates missing parents recursively before writing.
* feat(tui): detach foreground tasks to background with Ctrl+B
- Add Ctrl+B shortcut to detach all foreground Bash/subagent tasks at once
- Show "Press Ctrl+B to run in background" hint in tool cards (Agent immediately, Bash after 10s) and in the agent group panel
- Mark detached foreground subagents as ◐ backgrounded instead of ✓ Completed
- Filter foreground tasks out of the /tasks panel (they appear after detach)
- Steer the model away from blocking on TaskOutput after a detach
* fix(tui): address Codex review on Ctrl+B detach
- Preserve `◐ backgrounded` for detached subagents inside AgentGroupComponent by reusing getDerivedSubagentPhase in getSubagentSnapshot
- Distinguish detached-from-foreground subagents from started-in-background ones so the latter still read as `done`
- Let Ctrl+B fall through to readline backward-char at the idle prompt instead of always consuming the key
* fix(tui): address follow-up Codex review on detach hints
- Auto-clear the "No foreground task running." hint via showDetachHint so it doesn't stick on the footer
- Don't clobber a newer transient hint (e.g. exit confirmation) when the detach hint timer fires
- Add FooterComponent.getTransientHint()
* fix(tui): address Codex review on backgrounded gating and /tasks counts
- Only mark foreground-running subagent cards as backgrounded (skip done/backgrounded cards so background resumes don't mutate older rows)
- Count /tasks header from the filtered (background-only) task set so foreground-only sessions don't read misleading counts
* feat: add workspace add-dir support
Add multi-directory /add-dir management with session-only or project-remembered persistence, directory completion, confirmation UI, and runtime workspace/permission wiring.
* fix: honor --add-dir for resumed sessions
Pass CLI additional directories through shell and prompt resume paths, resolve caller-relative dirs against workDir, and add regression coverage.
* fix: keep additional dirs AGENTS.md out of default context
Load only user-level and cwd AGENTS.md by default, while preserving additional directory listings in the prompt context.
* feat: append /add-dir result as user message
Add a session appendUserMessage RPC and use it after /add-dir so the command result is recorded as a normal user message and surfaced in the transcript.
* docs: add add-dir research and follow-up todos
Document the add-dir / local-command-stdout research findings and the follow-up tasks for stdout wrapping, slash file completion, and hints.
* feat: wrap /add-dir output as local-command-stdout
Insert the /add-dir result as a user-role <local-command-stdout> record with an injection origin directly inside Session.addAdditionalDir. It enters the model context on the next turn but does not start a turn, and stays out of the live and resumed transcript; the transient status toast is kept for immediate feedback. --add-dir is unaffected since it bypasses addAdditionalDir.
Remove the now-unused appendUserMessage RPC and SDK method.
* feat: reopen /add-dir completion after accepting a directory
Generalize the slash-argument completion reopen so it fires whenever the text before the cursor ends with '/', not only when the literal '/' key is typed. After Tab-accepting a directory (or auto-applying a single-child dir), the next level's completion list reappears automatically, so repeated Tab keeps drilling down into subdirectories. '@' file mention is unaffected.
* feat: reopen file mention completion after accepting a directory
Extend the path completion reopen so it also fires for '@' file mentions. After Tab-accepting a directory in an '@' mention, the next level's completion list reappears automatically, matching the '/add-dir' continuous-Tab behavior.
* feat: show inline argument hints for slash commands
Render a dim ghost-text argument hint inside the input box after a slash command that takes arguments, replacing the popup-only hint that was easy to miss. The hint appears once the command is typed and disappears as soon as an argument is entered, and is truncated to fit the box width. Add argument hints for /compact, /swarm, /goal and /title; /add-dir already had one.
* test: remove stale additional-dirs AGENTS.md assertion
The subagent-host test still asserted that an additional directory's AGENTS.md content appears in the agent system prompt, but additional-dirs AGENTS.md has been intentionally excluded from the default context since an earlier commit (covered by context.test.ts). Drop the stale assertion.
* fix: resolve /add-dir paths against workdir and persist via kaos
Resolve user-supplied /add-dir paths against the current workdir instead of the project root, so launching from a subdirectory behaves like the CLI --add-dir flag. Also route the local.toml read/write through the kaos abstraction instead of host fs, so the remember path works for non-local sessions.
* fix: expand ~ in /add-dir paths before resolving
The /add-dir completer emits ~/... values, but the core treated ~/foo as a relative path because pathe isAbsolute('~/foo') is false, producing <workDir>/~/foo. Expand ~ and ~/ to the home directory (via kaos.gethome()) before resolving.
* chore: remove add-dir dev docs from the branch
These were working notes (research and follow-up todos) that don't belong in the PR.
* chore: clarify add-dir changeset for users
* docs: document /add-dir, --add-dir, and local.toml
* test: flush records before reading wire in add-dir runtime tests
FileSystemAgentRecordPersistence.append buffers records and flushes asynchronously, so readMainWire can read the wire before the local-command-stdout record lands. Flush the main agent's records explicitly in the two add-dir runtime tests to make them deterministic.
Merge the two duplicate image-extension guards in detectFileType into a single mode-independent rule: an image extension without confirming magic is not an image in any mode. The video extension fallback stays before the NUL check so video containers with no magic still win. No behavior change.
Add and align tests: Read rejects an image-extension file with non-image bytes as not readable instead of redirecting to ReadMediaFile; file-type asserts the sniff-failed image case in both media and text modes.
When ReadMediaFile reads a file whose bytes have no recognisable image magic (for example a `.png` that is actually plain text), detectFileType previously fell back to the extension MIME type and built a data URL whose bytes did not match the declared format, which the model API rejected.
Every image format the model accepts (PNG/JPEG/GIF/WebP) has a reliable magic signature, so a failed sniff means the bytes are not a supported image. Report such files as `unknown` so ReadMediaFile returns a clear error instead of a bad data URL. The extension fallback is kept for video containers with no magic signature (for example MPEG-PS `.mpg`); format acceptance beyond that is left to the provider.
* feat(agent-core): collapse hidden dirs in prompt
Keep hidden directory entries visible in the cwd snapshot while omitting their contents to reduce prompt noise, and document the tools models should use to inspect hidden paths.
* feat(agent-core): clarify prompt AGENTS context
Add a marker when AGENTS.md content is truncated by the prompt budget and move dynamic system prompt context after the static guidance.
* chore: add prompt refinement changesets
- Add terminating-state guard to skip ERR_STREAM_PREMATURE_CLOSE when a process is killed or times out
- Ensure timeout/kill reason is reported instead of the internal stream error
- Add changeset for agent-core and kimi-code
* feat(acp-adapter): support embedded resource prompts
- advertise embeddedContext support in ACP capabilities and docs
- convert file:// resource_link blocks into decoded paths with optional line ranges
- keep XML wrappers for non-file or unparseable resource_link URIs
- update adapter tests for the new resource link behavior
* feat(acp-adapter): add ACP built-in slash command routing and UNC path support
- add local execution for /compact, /status, /usage, /mcp, /tasks, /help in ACP sessions
- surface unknown slash commands as local errors instead of forwarding to model\n- export ACP_BUILTIN_SLASH_COMMANDS from acp-adapter for CLI reuse
- fix file:// URI conversion for Windows UNC paths
- rebuild agent builtin tools on session tool kaos rebind
* fix(glob): normalize pattern before brace expansion
* fix(glob): preserve backslash-escaped metacharacters and normalize braces correctly
- expand braces before normalizing to prevent `..` from collapsing across brace groups
- skip normalization for patterns containing backslash-escaped glob metacharacters
- add test coverage for escaped braces and nested `..` inside alternatives
* 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
* 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
When the user interrupts running tools or parallel subagents, the tool_result
fed back to the model was a neutral `Tool "X" was aborted` or a weak "stopped by
the user", so the model treated it as a system fault and speculated about
capacity/concurrency limits instead of recognizing a deliberate stop.
Carry a UserCancellationError as the AbortSignal reason from the cancel sites
(Turn.cancel/abortTurn, SessionSubagentHost.cancelAll) through to the message
sites (tool-call settle paths and the AgentTool catches), which now emit an
explicit "deliberate user action, not a system error/timeout/capacity limit"
message. Aborts propagated from another signal (e.g. a subagent's deadline via
waitForCurrentTurn) carry their original reason, so a timeout is not mislabeled
as a user interruption. The telemetry outcome classifier matches the new
"manually interrupted" phrase to keep counting these as cancelled.
* 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.
* 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(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