kimi-code/packages/kap-server
Kexin Liu 650d12f6c5
feat: add experimental turn-level file history snapshots (#3339)
* feat(agent-core-v2): add experimental turn-level file history snapshots

Back up the original content of every file the Edit/Write tools touch
(whole-file blob on first touch, captured through the tool executor's
will-execute hook so the read races nothing) and version all tracked
files at each main-agent turn boundary — a checkpoint at turn start and
one at turn end, so edits the user makes between turns attribute to no
turn. Unchanged files deduplicate by content hash; files past the 4 MiB
cap record an oversize sentinel fingerprinted by size and mtime; durable
checkpoint events are delta-encoded and fold back into replayable agent
state; reads (per-turn changes, checkpoint content) are computed from the
blobs on demand and serialize with writes. Gated behind
KIMI_CODE_EXPERIMENTAL_FILE_HISTORY (default off).

* feat(kap-server): serve turn-level file history over REST

GET /sessions/{session_id}/file-history/changes returns one turn's exact
per-file additions/deletions from the daemon's whole-file checkpoints;
/file-history/content returns a file's full content at either checkpoint
boundary. Persisted sessions resume on demand, matching the transcript
routes' cold-session behavior.

* fix(agent-core-v2): drain queued snapshots on agent teardown, single-sentence changeset

Register the snapshot queue with the unit ledger so agent-scope disposal
waits for an in-flight end checkpoint instead of racing runtime and log
teardown, and fold the changeset's enablement note into one sentence per
the changeset format rule.

* fix(agent-core-v2): bound aggregate LCS work per changes() call

Each file's line-count diff had its own 4M-cell LCS budget, so one
changes() request over a turn that rewrote hundreds of files could sum
to hundreds of millions of synchronous DP cells on the server event
loop (and on the snapshot queue). A per-call aggregate budget (16M
cells) is now threaded through diffChange/countLineDiff; files past the
budget fall back to the existing oversize no-counts marker. Files over
the per-file cap short-circuit without consuming aggregate budget.

* refactor(agent-core-v2): store per-turn checkpoint deltas instead of cumulative maps

Each checkpoint previously materialized the full session-wide entry
table, so changes() walked every file the session ever touched --
blob-reading and diffing files the turn never modified. Checkpoints now
keep only their own delta (entryAt already resolves by walking back);
when the cap evicts old checkpoints their entries fold into the new
head so resolution, maxVersion, and blob retention stay correct.
changes() derives its path set from the end checkpoint's delta, which
is exactly the files the turn touched.

* fix(agent-core-v2): gate live reads on the actually active turn

A start checkpoint without a matching end previously counted as live
whenever it was the latest checkpoint, so a session reloaded after a
crash or a lost end write compared that checkpoint against the current
workspace and attributed unrelated edits to the interrupted turn. The
service now tracks the active turn from TurnStarted/TurnEnded and takes
the live-disk path only while that turn is genuinely running; a cold
session reports no changes for an unpaired start instead.

* fix(agent-core-v2): reset file history in turn-sliced forks

A turn-indexed fork slices the journal at the next turn's first user
record, and the asynchronously written end checkpoint of the forked-at
turn can land after that boundary, leaving the fork with partial file
history. Instead of teaching the slicer to chase late records, the
slice now drops all file_history records: a turn-sliced fork starts
with an empty snapshot store and re-captures baselines on first touch,
while full forks copy the journal unchanged and keep complete history.

* fix(agent-core-v2): settle snapshots before fork copy, prune sliced-fork blobs

A fork accepted right after a turn completes could copy the blob
directory before the queued end checkpoint wrote its blobs, then copy a
journal that references them; fork() now awaits each live agent's file
history settling before copying. Turn-sliced forks drop file_history
records but previously kept every copied snapshot blob unreachable on
disk; pruneTruncatedForkFiles removes the file-history directory
alongside tasks and cron.

* fix(agent-core-v2): capture subagent edits into the main agent's file history

Non-main agents previously registered no tool hook, so a file first
touched by a delegated subagent had no baseline backup and never
appeared in the turn's changes. Subagent services now route their
file_io captures to the main agent's service, which records the
pre-image against its currently active turn; the existing end-of-turn
checkpoint then picks up the file's final state from disk.

* fix(agent-core-v2): hold loop quiescence on source agents while forking

The active-turn precheck and snapshot drain did not stop a prompt that
arrives mid-fork from starting a turn between the file copy and the
journal copy, letting the fork pick up journal records whose blobs were
written after the directory copy. fork() now acquires loop quiescence
on every live source agent (the undo/compaction idiom) and holds it
through both copy phases; prompts submitted meanwhile are held by the
loop and re-admitted on release.

* feat(agent-core-v2): retain file history for a five-turn sliding window

The 400-checkpoint cap with fold-on-eviction kept every touched file's
resolution (and its blobs, v1 forever) for the session's lifetime, so
disk and the per-boundary tracked-file scan both grew without bound.
Retention becomes a queue over the last five turns: checkpoints older
than the window are dropped outright, the tracked set shrinks to files
appearing in the remaining deltas, and their blobs -- v1 included --
are evicted. A file edited again after leaving the window re-captures
a fresh v1 baseline. Reads outside the window return empty and the
client already degrades to its local stats for those turns; the fold
machinery and the v1-forever rule are deleted.

* feat(agent-core-v2): capture a per-turn baseline so turns are self-contained

capture() previously fired only on a file's first touch in the session,
so a later turn's before-image resolved by walking back into older
turns' entries -- the cross-turn dependency that made window pruning
able to misreport an edge turn's modification as an addition. Every
turn now records its own baseline at first touch: when the pre-image
hash matches the latest known entry the entry is copied with its
existing blob key (no new content written), otherwise a new version is
backed up. changes()/contentAt() resolve within the turn's own start
checkpoint, and the prune-time fold remains only as the backstop for
files changed out-of-band mid-turn.

* refactor(agent-core-v2): turns record only their own tool snapshots, reads never walk back

Simplify the model to exactly what the feature displays: a turn records
the before-image of each file its tools touch (at first touch) and the
after-image of those same files at turn end, compared against the
turn's own baseline. changes() and contentAt() are direct lookups in
the turn's two checkpoints -- no cross-turn resolution, so the
prune-time fold and the walk-back helpers are deleted, the start
boundary scan is gone, and the live path iterates the turn's touched
files instead of every tracked file. An empty end record still lands on
turns without edits so the retention window keeps advancing.

Semantics change: files modified only by external means during a turn
(no tool touch) no longer appear in that turn's changes, and deletions
are recorded only for tool-touched files -- the turn diff now covers
exactly the tool activity, matching the client's row set.

* feat(kap-server): report file-history availability on the changes response

An empty changes list is ambiguous for clients: the flag may be off, or
the turn's checkpoint may have left the retention window. Carry the
service's enabled state so clients can tell the two apart.

* fix(agent-core-v2): retain the last five recorded turns, not five turn ids

Cancelled queued turns leave gaps in the ordinal sequence, so id
arithmetic evicted more history than the advertised window; retention
and blob eviction now keep the newest five distinct recorded turns. The
changeset sentence also catches up with the per-turn snapshot scope.

* fix(agent-core-v2): count only completed turns in the file-history window

A crash between a turn's first capture and its end checkpoint leaves a
start-only turn that used to occupy a retention slot and shrink the
usable window; retention and blob eviction now count turns with an end
record, dropping stale start-only leftovers while preserving anything
newer than the latest completed turn. The flag catalog description also
catches up with the per-turn snapshot scope.

* fix(agent-core-v2): flush the end checkpoint before evicting displaced blobs

dispatch queues the durable append without waiting for it, so a crash
between the window advancing and the journal flush could leave the old
checkpoints on disk with their blobs already deleted; the journal is now
flushed before any displaced blob is removed.

* fix(agent-core-v2): size snapshot reads to the statted file

Every snapshot read requested the 4 MiB limit, and the local fs backend
allocates the requested length up front — a turn touching many small
files paid hundreds of megabytes of zero-fill. Request the statted size
plus one byte instead; growth past the stat still surfaces as a length
mismatch, and a file already at the limit keeps the oversize path.

* fix(agent-core-v2): keep file-history blobs out of session exports, sweep crash orphans

Session export packed the whole session directory, shipping up to five
turns of whole-file snapshots inside the zip; the blob directory is now
excluded. A crash between the checkpoint flush and blob eviction could
also strand blobs no surviving checkpoint references — the first end
checkpoint of a session now sweeps unreferenced file-history blobs
once.

* feat(agent-core-v2): retain file history for the thirty most recently active sessions

Snapshots previously accumulated across every session a workspace ever
ran. A per-workspace LRU (persisted beside the sessions) is touched on
each end checkpoint; when it exceeds thirty sessions the least recently
active one's snapshot blobs are removed. An evicted session simply has
no snapshots — resuming it re-enters the window and rebuilds baselines
on the next edit, and a session evicted while idle self-heals because
blob writes recreate their directories.

* fix(agent-core-v2): keep the retention ledger out of the session listing

The per-workspace LRU document lived inside sessions/<workspace>, where
listSessionIds enumerates directory entries as session-id candidates;
it now lives in its own top-level file-history scope keyed by workspace
id.

* fix(agent-core-v2): review round — editing-turn window, full blob hashes, tests in-tree

Editless turns no longer emit end checkpoints, so pure chat neither
grows the wire journal nor evicts real edit history — the window now
keeps the last five editing turns. Blob keys use the full path hash
(truncation could collide and interleave histories), a lost blob
degrades its row to a countless marker instead of misreporting status,
and the window rule lives in one shared helper used by both the reducer
and blob eviction. The fork guard gets an honest message about
concurrent holds plus its own changeset, and the service/route/window
suites move into the tree with new unit coverage for the window helper
and diff budget.

* feat(agent-core-v2): report whether a turn's history is recorded

The changes response gains a recorded flag — true when the turn has a
start checkpoint and either its end checkpoint or a live turn — so
clients can tell an authoritative empty result from a turn whose
records were never made, lost to a crash, or displaced by retention.

* fix(agent-core-v2): recorded probes blob availability, content falls back for unchanged files

turnRecorded now verifies a keyed blob still exists, so a session whose
snapshots were displaced by the session window stops claiming authority
and clients keep their local rows. contentAt(end) resolves a file the
turn left unchanged from its start entry instead of returning nothing,
and session eviction deletes blobs before updating the ledger so a
crash retries the deletion instead of stranding it.

* fix(agent-core-v2): satisfy lint on the recorded probe

* fix(agent-core-v2): keep the retention ledger honest across eviction failure, fork and delete

A victim whose blob removal hits an IO error stays in the ledger for a
later retry instead of being delisted with its blobs stranded. A full
fork that copied snapshot blobs now enters the target session into the
ledger, so repeated forks cannot hold history outside the session
window, and deleting a session drops its ledger entry so ghost rows no
longer displace live sessions.

* chore(agent-core-v2): regenerate the wire manifest after the merge

* fix(agent-core-v2): count a trailing-newline-only change in authoritative stats

The daemon's line splitter adopts the client's last-line sentinel so a
file differing only in its trailing newline reports 1/1 instead of 0/0,
keeping authoritative counts in agreement with the client. Closes the
follow-up tracked in #3449.

---------

Co-authored-by: user <user@userdeMacBook-Pro.local>
2026-09-02 14:04:56 +08:00
..
src feat: add experimental turn-level file history snapshots (#3339) 2026-09-02 14:04:56 +08:00
test feat: add experimental turn-level file history snapshots (#3339) 2026-09-02 14:04:56 +08:00
AGENTS.md feat(auth): split model readiness from sign-in state in /api/v1/auth (#3293) 2026-08-28 15:53:56 +08:00
CHANGELOG.md ci: release packages (#3140) 2026-08-27 19:33:51 +08:00
package.json ci: release packages (#3140) 2026-08-27 19:33:51 +08:00
tsconfig.dev.json feat(v2): land agent-core-v2 engine and kap-server behind experimental flag (#1441) 2026-07-12 21:44:04 +08:00
tsconfig.json feat(v2): land agent-core-v2 engine and kap-server behind experimental flag (#1441) 2026-07-12 21:44:04 +08:00
tsdown.config.ts feat(kap-server): add provider write endpoints and models.dev/registry import (#2110) 2026-07-24 14:00:16 +08:00
vitest.config.ts feat(minidb): persistent index generations and lifecycle hardening (#2604) 2026-08-04 20:38:42 +08:00