Skip to content

fix(agent-core-v2): carry session state across fork and ignite toolDedupe - #1629

Merged
sailist merged 2 commits into
MoonshotAI:mainfrom
sailist:fix/v2-fork-and-tool-dedupe
Jul 13, 2026
Merged

fix(agent-core-v2): carry session state across fork and ignite toolDedupe#1629
sailist merged 2 commits into
MoonshotAI:mainfrom
sailist:fix/v2-fork-and-tool-dedupe

Conversation

@sailist

@sailist sailist commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problems are explained below.

Problem

Two independent v2 engine regressions relative to v1:

  1. Session fork loses everything except the conversation log. Forking a session only copied the wire logs and metadata; per-agent blobs/ and plans/, background-task output, and media-originals/ were left behind, so the fork's blob refs resolve to [media missing] and its active plan file is gone. Cron tasks were lost too: v1 kept them inside the session dir so its cp -r fork carried them, while v2 persists them at workspace level. And a fork that failed midway left a broken half-copy in the registry and on disk, tripping SESSION_ALREADY_EXISTS on retry.
  2. Tool-call deduplication never activated. AgentToolDedupeService is self-wiring (its constructor registers the loop step hooks and the executor's onBefore/onDidExecuteTool handlers) but nothing injects it, so it was never instantiated — identical tool calls issued in the same step executed multiple times, and repeated identical calls across steps never received escalating reminders.

What changed

Fork state carry-over (SessionLifecycleService.fork)

  • Copy the source session's on-disk state into the target session dir — per-agent blobs/ and plans/, background-task output, and session media originals — excluding state.json (rewritten with fork provenance), the wire logs (copied with a fork boundary record), and logs/. Symlinks are never followed out of the session dir.
  • Duplicate the source session's cron tasks with fresh ids retagged to the target session, via a shared CRON_SESSION_TAG exported from app/cron/cronTask.ts.
  • Roll back a half-fork on failure: drop the materialized handle from the live registry, dispose it, and remove the target session dir, so a retry with the same ids works.

toolDedupe ignition (AgentLifecycleService)

  • Ignite IAgentToolDedupeService during agent creation, before the external hooks service, so toolDedupe sits ahead of permission on onBeforeExecuteTool and same-step duplicates are suppressed before authorization runs (matching v1 ordering).
  • Mirror the ignition order in the test harness (AgentTestContext).

Tests: added a lifecycle test asserting the dedupe hooks are registered, and fork tests covering file copy/exclusions, rollback + retry, and cron task duplication.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

sailist added 2 commits July 13, 2026 22:43
- ignite IAgentToolDedupeService in AgentLifecycleService before external
  hooks so it sits ahead of permission on onBeforeExecuteTool
- mirror the ignition in the test harness AgentTestContext
- add lifecycle test asserting the dedupe hooks are registered
- copy blobs, plans, background-task output, and media originals into the
  forked session dir (excluding state.json, wire logs, and logs/)
- duplicate the source session's cron tasks with fresh ids retagged to the
  target session via the shared CRON_SESSION_TAG
- roll back the half-fork on failure: drop the materialized handle from the
  live registry and delete the target session dir
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 595fa94

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@moonshot-ai/agent-core-v2 Patch
@moonshot-ai/kimi-code Patch
@moonshot-ai/kap-server Patch
@moonshot-ai/klient Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@595fa94
npx https://pkg.pr.new/@moonshot-ai/kimi-code@595fa94

commit: 595fa94

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 595fa94864

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +479 to +480
if (targetId !== undefined) {
this.sessions.delete(targetId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid deleting existing sessions on fork collisions

When newSessionId already belongs to a live session, the collision check throws after targetId has been assigned, but before this fork materializes a target. This catch then deletes this.sessions[targetId], so a rejected fork can make the pre-existing session disappear from get/list/close. Only remove the handle when it is the target created by this fork.

Useful? React with 👍 / 👎.

id: ulid(),
tags: { ...task.tags, [CRON_SESSION_TAG]: targetId },
};
await this.cronStore.save(workspaceId, clone);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Delete cloned cron tasks on fork rollback

If the fork fails after one or more cron clones are saved here, for example during agent restore/replay or while appending the session index, the catch only removes the live handle and session directory. The cloned workspace-level cron docs remain tagged to the target id; retrying the same fork id can create another set of clones, so the successful fork later sees duplicated scheduled jobs. Track the cloned ids and delete them on rollback, or defer saving them until the fork can no longer fail.

Useful? React with 👍 / 👎.

Comment on lines +409 to +413
// 7. Copy the source session's on-disk state into the target — per-agent
// `blobs/` and `plans/`, background-task output, and media originals.
// v1 achieved this with `cp -r` of the whole session dir; the wire logs
// (step 8) and `state.json` (step 9) are rewritten by the fork flow
// itself, and `logs/` is the source's debug log, so those are excluded.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep implementation comments out of method bodies

packages/agent-core-v2/AGENTS.md says comments in this package must live only in the top-of-file /** */ block and never beside functions, methods, or statements. This newly added method-body narration violates that directory rule, and the same pattern appears in the new rollback/copy helper code; remove it or fold any necessary role-level context into the file header.

Useful? React with 👍 / 👎.

@sailist
sailist merged commit 0527ca2 into MoonshotAI:main Jul 13, 2026
15 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant