Skip to content

feat: fork a session from an earlier prompt - #4017

Open
liruifengv wants to merge 7 commits into
mainfrom
feat/fork-at-turn
Open

liruifengv wants to merge 7 commits into
mainfrom
feat/fork-at-turn

Conversation

@liruifengv

@liruifengv liruifengv commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Requirement or Bug

Support forking a session from an earlier prompt: the :fork endpoint accepts a prompt_id, and the copy keeps only the conversation through that prompt's turn segment. Forking is idle-only — it is rejected with session.fork_active_turn while any operation is in progress. (Forking while a turn is running was split out to a follow-up PR built on loop-reported fork boundaries.)

Bug Reproduction Steps

N/A

Root Cause

N/A

Code Changes

REST (POST /sessions/{id}:fork)

  • Accept prompt_id (the persistent id of a user prompt) to fork from an earlier point instead of the latest state. Validated at both schema layers: the route-boundary sessionActionRequestSchema and the action body sessionForkSchema, then passed through to ISessionManager.fork.

Engine (SessionLifecycleService.fork)

 fork(opts)
   flush source agents + append log store (unchanged)
-  any agent loop running → throw session.fork_active_turn
+  tryAcquireQuiescence per agent; any failure → throw session.fork_active_turn
+  resolve prompt_id → user-visible turn index (resolveForkPromptIndex)
   slice main-agent wire at the turn (existing turn-slice path)
  • resolveForkPromptIndex maps a prompt id to its user-visible turn: merged steers are resolved through turn.steer.promptIds, and an id matching more than one visible turn is rejected as ambiguous instead of silently picking the first.
  • Subagent wires are capped at the main cutoff time, so no agent in the fork knows more than the main agent.
  • The forked session's last_turn_reason is derived from the slice: a slice cut before its terminal record (a steer-split turn) reports no outcome instead of inheriting an earlier turn's reason.

Turn-close quiescence fix (AgentLoopService)

  • Quiescence now opens as soon as the turn.ended event is published (pendingTurnCloses), while settled() keeps waiting for the turn result to resolve (pendingFinalizations). Undo, manual compaction, and fork called from turn-completion continuations no longer fail with a spurious busy error.

Docs: server-api.md (en/zh) documents prompt_id and the idle-only fork semantics.

Impact Scope

  • agent-core-v2: sessionLifecycle fork, forkTurnSlice, loop quiescence.
  • kap-server: :fork route schema and contract tests.
  • node-sdk: ForkSessionInput.promptId pass-through; the engine keeps turnIndex for the vscode consumer.
  • Tests: forkTurnSlice unit tests, node-sdk fork e2e (prompt addressing, idle-only rejection), kap-server fork contract tests, loop quiescence tests.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (external PRs: issue must have a maintainer's /approve).
  • 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.

The session fork REST action now accepts turn_index to fork from an
earlier user-visible turn, and a live session no longer rejects fork
while a turn is running or queued: when quiescence cannot be acquired
the fork is capped at the last completed turn (explicit indices beyond
it fail request.invalid; only a session with no completed turn yet
still fails session.fork_active_turn).
Copilot AI balanced review requested due to automatic review settings September 24, 2026 07:31
@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8bc5e8e

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

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

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

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@liruifengv
liruifengv marked this pull request as ready for review October 8, 2026 06:18
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T11:10:56.098721Z 8bc5e8e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026 •

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

commit: 8bc5e8e

@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: b594e07378

ℹ️ 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 thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
… holds, and fork outcome

- clamp busy forks to the last completed user-visible turn index instead
  of the completed-turn count, so a steer-split turn keeps the steered
  input and its answer, and explicit indexes into it are accepted
- reject forking while quiescence is held by a non-turn operation such
  as manual compaction, undo, or another fork, instead of treating it
  as turn activity and copying a wire in an inconsistent state
- derive the fork's lastTurnReason from the retained wire's terminal
  turn.ended rather than copying the source metadata, which an active
  turn may have cleared
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@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: f72cd67205

ℹ️ 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 thread packages/agent-core-v2/src/workspace/sessionLifecycle/internal/forkTurnSlice.ts Outdated
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
The REST :fork body now takes prompt_id (a durable id persisted in the
wire as turn.prompt/turn.steer records) and the engine resolves it to
the user-visible turn index against the full wire history. Addressing
by a client-computed index drifted whenever the client could not see
the whole conversation — paginated transcripts silently forked at a
turn near the session start, and turns the client does not render
(e.g. shell-command turns) shifted every later index.

- resolve promptId to a visible turn index via the append message id;
  promptId and turnIndex are mutually exclusive, an unknown id or one
  past the last completed turn fails request.invalid
- kap-server fork schema, action boundary schema, docs, and the
  node-sdk fork input speak prompt_id; the engine and SDK keep the
  turnIndex option for full-history consumers (e.g. the VSCode bridge)

@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: 5f9d80d99f

ℹ️ 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 thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
Comment thread packages/agent-core-v2/test/workspace/sessionLifecycle/forkTurnSlice.test.ts Outdated
…and contention

- count a shell command as a completed turn once its output append
  lands, so busy forks stop clamping past (or rejecting on) a finished
  shell command
- cap busy-fork slices at the start of the currently active engine
  turn, so a running non-visible turn's records never leak into the
  fork
- keep the loop non-quiescent while a turn is finalizing (endTurn
  covers persistence of turn.ended), so a fork can never copy a wire
  whose terminal record is still in flight; settled() waits for the
  same point
- classify quiescence contention per agent: any agent holding the
  guard without turn activity (compaction, undo, another fork) rejects
  the fork instead of being masked by another agent's active turn
- resolve fork prompt ids through turn.steer promptIds (merged steer)
  and reject ids matching more than one visible turn (duplicated after
  a restart) instead of silently picking the first
@liruifengv

Copy link
Copy Markdown
Collaborator Author

@codex

@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: 24770b152a

ℹ️ 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 thread packages/agent-core-v2/src/workspace/sessionLifecycle/internal/forkTurnSlice.ts Outdated
Comment thread packages/agent-core-v2/src/agent/loop/loopService.ts
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/internal/forkTurnSlice.ts Outdated
- cap busy-fork slices at the first unterminated turn instead of the
  first turn after the last completed visible one, so completed
  continuation turns before the active turn stay in the fork
- stop the fork outcome scan at the last turn's start boundary, so a
  slice cut before its terminal record no longer inherits an earlier
  turn's last_turn_reason
- gate quiescence on the turn-close window (pendingTurnCloses, released
  once turn.ended is published) while settled() keeps waiting for the
  turn result to resolve (pendingFinalizations), so undo, compaction,
  and fork called from turn-completion continuations no longer fail
  with a spurious busy error

@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: 950b501236

ℹ️ 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 thread packages/agent-core-v2/src/workspace/sessionLifecycle/internal/forkTurnSlice.ts Outdated
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/sessionLifecycleService.ts Outdated
Comment thread packages/agent-core-v2/src/workspace/sessionLifecycle/internal/forkTurnSlice.ts Outdated
- fork now requires quiescence from every agent of the source session
  and rejects with session.fork_active_turn while any operation is in
  progress; the busy-fork slicing path (cap at the last completed turn)
  is removed and will return in a follow-up built on loop-reported fork
  boundaries
- keep the prompt_id addressing (merged-steer resolution, ambiguous
  rejection), the turn-sliced fork, and the turn-close quiescence fix
  that lets undo, compaction, and fork run right after turn.ended
@liruifengv liruifengv changed the title feat: allow turn-level session fork, including while a turn is running feat: fork a session from an earlier prompt Oct 8, 2026
@liruifengv

Copy link
Copy Markdown
Collaborator Author

Scope update: this PR is now limited to forking from an earlier prompt (idle-only). The busy-fork path (forking while a turn is running, capped at the last completed turn) has been removed and will return in a follow-up PR built on loop-reported fork boundaries instead of record-stream inference. The three latest review threads (shell interleaving, subagent active turns, gate-blocked completions) are addressed by that removal and resolved accordingly.

@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: e5e316d61b

ℹ️ 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".

if (reason === 'failed' || reason === 'blocked') return 'failed';
return undefined;
}
if (isEngineTurnStartRecord(record) || isUserVisibleTurnRecord(record)) return undefined;

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 Preserve outcomes across gate-blocked prompt slices

When a completed turn is followed by a prompt rejected by onBeforeSubmitPrompt, the wire contains a visible user message and prompt.completed with reason blocked, but no turn.ended. Forking at that prompt therefore reaches this early return and clears lastTurnReason, even though the source still reports the preceding completed outcome. Recognize terminal prompt.completed records before treating the visible message as an unterminated turn so the fork does not silently lose its session outcome.

Useful? React with 👍 / 👎.

Its only production caller was the removed busy-fork contention
classification; the interface method stays out until the busy-fork
follow-up needs it again

@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

const subagentRecords = sliceSubagentRecordsAtTime(
flattenChain(await this.readSourceWireRecords(sourceHandle, sourceId, agentId)),
turnSlice.cutoffTime,
);

P1 Badge Cut subagent wires only at terminal boundaries

When an earlier fork point falls in the middle of a background subagent turn, slicing solely by the main agent's timestamp retains the subagent's prompt and partial output while discarding its later turn.ended. This can happen even after the source becomes fully idle—for example, a subagent launched during prompt 1 finishes after prompt 2 begins, and the user later forks at prompt 1—so quiescence does not prevent the fork from containing an unterminated subagent wire. Fresh evidence after the busy-fork removal is that completed subagent turns can still straddle a historical cutoff; truncate each subagent at its last terminal boundary before the cutoff, or omit a turn that overlaps it.


const end = turnStarts[turnIndex + 1] ?? records.length;

P1 Badge Exclude prompts admitted after the fork lease is acquired

When a concurrent prompt submission reaches loop.submit() after the fork has acquired quiescence, the paused engine accepts and durably journals it as agent.input.submitted; the REST prompt and fork routes are not serialized. If prompt_id selects the last visible prompt, this fallback extends the slice to records.length, so readSourceWireRecords() flushes and copies that newly queued engine input even though its visible prompt has not started. Resuming the fork can then execute a prompt submitted concurrently to the source, violating the requested historical boundary; either prevent admissions while the fork lease is held or establish a fixed wire cutoff before reading the slice.

ℹ️ 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".

This branch has not been deployed

No deployments
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.

2 participants