Skip to content

Background subagent writes are silently dropped from file history after the parent turn ends #3587

Description

@cursor

What version of Kimi Code is running?

0.41.0 (kimi --version), also current main (ccf3d5d6)

Which open platform/subscription were you using?

Not relevant (engine-local file-history / rewind path)

Which model were you using?

Not relevant

What platform is your computer?

Linux x86_64 (reproduced with a focused unit test; the defect is host-independent)

What issue are you seeing?

Turn-level file history silently drops Write/Edit snapshots from a background subagent once the parent (main) turn has ended.

Subagent file mutations are forwarded to the main agent's captureForActiveTurn(). That method returns immediately when activeTurnId is undefined:

captureForActiveTurn(path: string): Promise<void> {
  const turnId = this.activeTurnId;
  if (turnId === undefined) return Promise.resolve();
  return this.enqueue(() => this.capture(path, turnId));
}

Agent with run_in_background: true is the intended path for long-running workers. Those workers commonly keep writing after the parent turn has already completed. File history / rewind then reports success (no error) while the files never appear in any checkpoint.

Related (same API, different timing): if the user has already started a new main turn when the background write arrives, captureForActiveTurn pins the snapshot to that new turn, not the turn that spawned the subagent. Rewind of the original turn misses the files; rewind of the later turn restores unrelated worker output.

What steps can reproduce the bug?

Deterministic unit reproduction (fails on unpatched main, passes with the last-ended fallback):

  1. Construct AgentFileHistoryService for main.
  2. Publish TurnStarted(turnId=1) then TurnEnded(turnId=1) with no edits.
  3. Call captureForActiveTurn('/ws/late.txt') (this is what onSubagentWillExecuteTool does).
  4. Observe: history().checkpoints stays []. The promise resolves. No error is logged.

The same drop happens through the real subagent hook: a second service for agent-1 whose IAgentLifecycleService.handleOf('main') returns the main service, then onWillExecuteTool for a Write after the parent turn has ended — main still has no checkpoint for that path.

Interactive sketch:

  1. Start a session, send a prompt that launches Agent with run_in_background: true and asks it to write a file after some work.
  2. Let the parent turn finish (the background worker is still running).
  3. After the worker writes the file, inspect turn-level file history / rewind for the parent turn.
  4. The worker's write is missing. Creating a brand-new session is the only way to get a clean history going forward.

What is the expected behavior?

  1. A background subagent Write/Edit that lands after the parent turn ends must still produce a file-history snapshot (at least a start checkpoint on the last ended main turn), not Promise.resolve() with no record.
  2. Failures to snapshot should be visible (log / error), not silent success.
  3. Ideally, captures from a given subagent stay bound to the spawn turn, not whichever main turn happens to be active when the write arrives.

Additional information

Root cause

packages/agent-core-v2/src/features/fileHistory/fileHistoryService.ts

  • Subagents do not keep their own history (agentId !== MAIN_AGENT_ID registers only onSubagentWillExecuteTool and returns). Existing test: stays inactive on subagents.
  • onSubagentWillExecuteTool always forwards to main:
event.waitUntil(main.accessor.get(IAgentFileHistoryService).captureForActiveTurn(path));
  • Main only sets activeTurnId from its own TurnStarted / TurnEnded. After TurnEnded, activeTurnId is cleared.
  • captureForActiveTurn treats "no active turn" as success and records nothing.

This is a state-machine / identity bug: subagent writes have no durable turn to attach to after the parent goes idle, and the API swallows that.

Suggested fix

Minimum (unblocks the idle / run_in_background case):

  • Remember lastEndedTurnId on main TurnEnded.
  • captureForActiveTurn uses activeTurnId ?? lastEndedTurnId instead of no-op'ing.

That is enough for the unit tests above. Remaining follow-up (not required for the idle drop):

  • Bind each subagent to the main turn that spawned it, so a later user prompt cannot steal those snapshots.
  • After a late capture on an already-ended turn, also record an end-of-turn after-image (today endCheckpoint has already run and will not see the new path).

Environment

Contribution

  • I am willing to submit a PR for this bug fix myself (please wait for maintainer approval in this issue first)

Please /approve if this looks right and I will open the fix PR against this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions