Skip to content

fix(cli): lifecycle guards for engine-opened turns (fork/resend, exit cleanup, Stop routing, replay window) - #654

Open
tommy0103 wants to merge 13 commits into
LodyAI:mainfrom
tommy0103:fix/engine-turn-origin-divider
Open

tommy0103 wants to merge 13 commits into
LodyAI:mainfrom
tommy0103:fix/engine-turn-origin-divider

Conversation

@tommy0103

@tommy0103 tommy0103 commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Related issue

Refs #640

Stacking note: this PR is stacked on #653 and the two must land together.
It carries the lifecycle guards from the codex review rounds (fork/edit-resend
suppression of auto: entries, ACP exit-path marker cleanup, engine-turn
Stop routing + failed-stop semantics, replay-window bypass, monitor-state
coverage). The rest of the diff shown here is #653's content; the policy
check's 1000-line finding measures the stacked diff and clears once #653
merges and this branch is rebased. Review as one change; intended order:
#653 first, then this immediately after.

Problem / pressure

#653 separates engine-opened turns into their own history entries. That new entry type then flowed through existing lifecycle predicates that were written for client-dispatched turns only: fork and edit-and-resend treated a deliberately non-fork-position auto: id as a fork boundary; the engine-turn activity marker leaked when the ACP process died via exit (never terminated); Stop matched no client turn and reported success while the work continued, and a failed stop would have cleared state anyway; a live engine turn firing during the loadSession replay-suppression window was dropped as replay; the machine monitor and the disconnected-session idle reset could not see the turn.

Summary

  • Fork/edit-resend hardening: autonomous (auto:) entries are skipped while walking to the real provider boundary in both the CLI and renderer selection paths, cloneHistoryThroughTurn rejects an engine-opened fork source, and the renderer no longer offers Fork on such entries.
  • Lifecycle: the engine-turn activity marker also clears on the ACP process exit event; machine monitor and resetMachineDisconnectedSessionsToIdle read the marker.
  • Stop routing: a stop request matching no client turn cancels the active engine-opened turn through plain ACP session/cancel; the marker clears only after a delivered cancel, and a failed or undeliverable cancel keeps it and returns success: false.
  • Replay window: enqueueACPUpdate resolves the autonomous identity before replay suppression, so a live engine turn firing during the loadSession window is never dropped as replay (replay carries no autonomous marker).

(The origin-divider UI that briefly lived here was reverted: a separate stream item breaks the history index == chat items index coordinate contract that handleScrollToMessage and search navigation rely on — see #660 for the properly constrained design.)

Visual explanation

Simple change: predicate-level guards and one cancel-path branch, each mirrored by unit tests; no control-flow restructuring.

Before / after

Before After
Fork/edit-resend forwards a non-position auto: id; fails or forks wrong Autonomous entries skipped/rejected in all selection paths
Crashed agent leaves session busy+GC-exempt forever Marker clears on exit as well as terminated
Stop reports success, cron turn keeps running; failed stop would clear state Stop cancels the engine turn; failures keep state and return success: false
Engine turn during loadSession suppression dropped permanently Autonomous-identity updates bypass replay suppression
Monitor / idle-reset can't see the engine turn Both read the activity marker

Test plan

  • apps/cli: 148 targeted tests passing (session-fork, edit-and-resend, execution-service incl. new cancel-path cases, machine-runtime engine-turn, live-status predicate, transient store).
  • packages/components: 15 stream-item tests passing; tsc clean everywhere; prettier clean.
  • Re-verified after the divider revert: remaining diff is guard-only.

Context handoff

Instructions for reviewing agents

  • Review focus: session-edit-and-resend-service.ts and session-chat-interface.tsx backward-walk skip conditions, session-fork-service.ts cloneHistoryThroughTurn rejection, session-execution-service.ts engine-turn cancel branch (success ordering), message-handler.ts enqueueACPUpdate suppression ordering, machine-runtime.ts exit wiring.
  • Decisions to challenge: clearing the marker immediately after a delivered cancel (self-heals via turnEnded/re-mark); treating any stale stop with an active engine turn as targeting it; replay-bypass keyed on autonomous identity as the replay-vs-live discriminator.
  • Plausible failures / evidence gaps: the replay-window reorder (3 lines) has no dedicated harness test — MessageHandler construction is disproportionate; both predicates it orders are independently covered. Dispatching a user message during an engine turn still relies on engine-side queue/reject semantics (pre-existing).

Authoring context

  • User goal / directives: land the lifecycle guards as a companion to fix(cli,shared): render engine-opened agent turns as their own history entries #653; revert the divider UI after the index-contract finding and track the UI design in a follow-up issue.
  • Constraints / non-goals: no stream-index contract changes; no replay/import classification rework; no cron-prompt surfacing.
  • Risk-bearing decisions: engine-turn cancels go through plain ACP session/cancel (cancels the agent's current turn, whatever it is); replay bypass trusts the autonomous marker as proof of liveness.
  • Destructive or irreversible behavior: none — all paths fail closed (marker kept on failure; fork rejected).
  • Deliberately not done or tested: origin-label UI (needs a contract-safe design); end-to-end Stop click in a live app (covered at service level).
  • Unknowns / confidence: high confidence from unit coverage of every branch; the two no-harness spots are noted above and bounded to thin wiring over tested predicates.

Original user prompt

Show original prompt
对,所以你要不要看一下「新条目会进入多少既有生命周期判定」这方面的东西,看看你还有没有遗漏

@github-actions

github-actions Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

@tommy0103, this pull request needs updates before review.

It is marked status:needs-pr-attention. Address the findings below by 2026-09-19 16:00:04 UTC. The label and this comment are removed automatically after the PR passes validation.

If the PR remains invalid for 7 days, it will be closed and marked status:pr-policy-expired. Continue afterward by opening a new pull request with the current template.

Policy findings
PR does not meet Lody contribution requirements:

- ## Visual explanation must include a structural view because this PR changes 2262 lines, above the 200-line complexity floor.
- PR changes 2262 lines; community PRs over 1000 lines require a maintainer assignment on the linked Issue before review.

See `CONTRIBUTING.md` and `.github/PULL_REQUEST_TEMPLATE.md`.

@tommy0103
tommy0103 force-pushed the fix/engine-turn-origin-divider branch from 0253ff4 to d27b19e Compare September 12, 2026 16:30
@tommy0103 tommy0103 changed the title feat(components): label engine-opened agent turns with an origin divider feat(components): label engine-opened turns and suppress them in fork/edit-resend paths Sep 12, 2026
@tommy0103
tommy0103 marked this pull request as ready for review September 12, 2026 17:23

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

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

);
// A dying ACP process ends any engine-opened turn it hosted: drop the
// activity marker so busy status and the idle-GC guard release the session.
this.onSessionTerminated((sessionId) => this.handler?.clearEngineTurnActivity(sessionId));

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 Clear engine activity when the ACP session exits

When an autonomous turn's ACP process exits without emitting turnEnded, this callback never runs because onSessionTerminated is fed only by the terminated listener, while SessionManager also reports agent death through exit. The stale marker then makes live status remain running and causes hasActiveTurn() to exempt the session from idle GC indefinitely; clear it on the exit lifecycle path as well.

AGENTS.md reference: apps/cli/AGENTS.md:L59-L63

Useful? React with 👍 / 👎.

@tommy0103
tommy0103 marked this pull request as draft September 12, 2026 17:33
@tommy0103

Copy link
Copy Markdown
Contributor Author

Verified and fixed: Session.emit('terminated') fires only on the explicit terminate() path, while unexpected agent death reports solely through exit — exactly the marker-leak case described. machine-runtime now also clears the engine-turn marker on the manager's exit event (commit ea4b53a), covered by tests/machine-runtime-engine-turn.test.ts which asserts the exit lifecycle path clears the marker.

@tommy0103
tommy0103 marked this pull request as ready for review September 12, 2026 17:44

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

ℹ️ 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 +9659 to +9660
state.turn.phase !== 'idle' ||
state.engineTurn !== 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.

P1 Badge Include engine turns in the fork busy predicate

When a cron/task turn is active, this method now considers the session busy, but the isSourceBusy callback passed to SessionForkService still independently calls resolveSessionLiveStatus() without consulting engineTurn. Because autonomous turns have neither normal execution ownership nor presence, a fork during such a turn is treated as an idle-source fork and bypasses the supportsActiveTurnFork() and resident ACP identity checks in forkInner, allowing session/fork to run against an active source without the required advertised capability. Route this activity state into the fork predicate as well.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L97-L102

Useful? React with 👍 / 👎.

@tommy0103
tommy0103 marked this pull request as draft September 12, 2026 18:01
@tommy0103

Copy link
Copy Markdown
Contributor Author

Valid — the fork busy predicate (isSourceBusy) resolved through resolveSessionLiveStatus, which didn't know about the engine-turn marker, so a fork during an engine turn would take the idle-source path and skip supportsActiveTurnFork / resident-ACP checks in forkInner. The marker is now an input to the shared predicate itself (commit 420280b): the live-status RPC, isSourceBusy, and any future caller read it from one place, with presence still outranking. Covered by two new predicate tests (engine turn → running; presence outranks).

@tommy0103
tommy0103 force-pushed the fix/engine-turn-origin-divider branch from 420280b to 80330df Compare September 12, 2026 18:09
@tommy0103

Copy link
Copy Markdown
Contributor Author

Confirmed against the cancel path: the unfinished autonomous entry flows through resolveActiveAssistantTurnId into the Stop control, while cancelSession exact-matches only client-turn registries — the request hit the stale branch, returned success: true, and the engine turn kept running. Fixed in 885f371: when a stop request matches no client turn and an engine-opened turn is active, it is cancelled through the plain ACP session/cancel (the only live work on the agent) and the activity marker clears; if the agent is already gone, the exit path from ea4b53a covers cleanup. Two new tests: stop routes to the ACP cancel + marker clears; no engine turn → stale no-op preserved.

@tommy0103
tommy0103 marked this pull request as ready for review September 12, 2026 18:31

@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: 519310d1b9

ℹ️ 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 +1788 to +1790
!(
row.item.message.acpTurnId && isAutonomousTurnId(row.item.message.acpTurnId)
) &&

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 Exclude autonomous turns from the global fork target

When a scheduled or background turn finishes, buildChatStreamItems still publishes that autonomous entry as lastCompletedAssistantMessageId. This guard hides only the per-row fork action; SessionChatInterface.handleForkFromMenu and SessionDetail's getLastAssistantTurnId() path still submit the autonomous ID, which cloneHistoryThroughTurn rejects, so header and side-panel forks fail until another normal assistant turn completes. Track the last forkable assistant separately.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L97-L103

Useful? React with 👍 / 👎.

Comment on lines +5347 to +5351
} catch (error) {
this.deps.logger.warn(
`[${sessionId}] Failed to cancel engine-opened turn: ${formatErrorMessage(error)}`
);
}

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 Preserve engine activity when cancellation fails

When agentClient.cancel() rejects for an active engine-opened turn, this catch only logs the error; the marker was already cleared and the function subsequently returns { success: true }. The agent can therefore continue background work while live status reports idle and the UI reports that Stop succeeded. Return the failure and retain or restore the activity marker until cancellation or termination is confirmed.

Useful? React with 👍 / 👎.

// turn (a cron fire, a task wake) — the only live work on the agent.
// Cancel it through the plain ACP session cancel instead of reporting
// a stale no-op while the work continues.
if (this.deps.isEngineTurnActive(sessionId)) {

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 Match stop requests to the active autonomous turn

When a delayed stop request for an older client turn arrives after that turn ends but while an engine-opened turn is active, this boolean check treats the stale request as targeting the engine turn and cancels the newer work. This bypasses the exact-match guarantee documented immediately above the branch; expose the current engine turn ID and require the request to match its synthesized assistant entry before canceling.

Useful? React with 👍 / 👎.

@tommy0103

Copy link
Copy Markdown
Contributor Author

Both fixed in b6471c6:

  1. Failed engine-turn stop: the cancel path now clears the activity marker only after a delivered ACP cancel. A failed cancel (or an unavailable ACP session) keeps the marker and returns success: false — status and the GC guard keep seeing the work, and "cannot send" is distinguished from "cancelled". Two new tests cover cancel-throws and agent-unavailable.
  2. Replay window: enqueueACPUpdate now resolves the autonomous identity before recordSuppressedAcpReplay, so a live engine turn firing during the loadSession suppression window is never dropped as replay — replay notifications carry no autonomous marker, only live engine turns do. A turnEnded falling in the window likewise lands, so the turn can't go permanently missing.

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

ℹ️ 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 +201 to +202
const divider = engineTurnDividerFor(entry, sessionId);
if (divider) items.push(divider);

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 Preserve message indices when adding divider rows

Whenever an engine turn precedes a search or pinned-message target, inserting this extra item shifts the chat-stream index away from the raw history index: SessionChatInterface still passes activeSearchResult.messageIndex and history.findIndex(...) directly to scrollToIndex, while that method indexes this expanded items array. Pinned-message navigation therefore lands one message early per preceding engine turn, and search can fail to reveal distant targets once enough dividers put the intended row outside Virtua's mounted buffer; translate history indices to stream indices or scroll by message ID instead.

AGENTS.md reference: packages/components/src/components/ai-gui/AGENTS.md:L80-L83

Useful? React with 👍 / 👎.

@tommy0103 tommy0103 changed the title feat(components): label engine-opened turns and suppress them in fork/edit-resend paths fix(cli): lifecycle guards for engine-opened turns (fork/resend, exit cleanup, Stop routing, replay window) Sep 12, 2026
@tommy0103
tommy0103 force-pushed the fix/engine-turn-origin-divider branch from b6471c6 to 05282b8 Compare September 12, 2026 19:41

@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: 05282b858d

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

return { success: false, error: 'The agent is no longer connected.' };
}
try {
await session.agentClient.cancel(session.acpSessionId);

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 Fence the engine cancel against a new client turn

When a user dispatch starts after the initial ownership snapshot but before this awaited session-wide cancel completes, the cancel can hit the new client prompt rather than the engine turn; MessageProcessor.extractQueueKey deliberately puts session/cancel on an unkeyed lane, so dispatch is not serialized behind it. Afterward, this branch also clears the newly claimed turn's presence. Hold the session conflict lease across the cancel or recheck ownership before clearing state so Stop cannot interrupt a turn that started during the await.

Useful? React with 👍 / 👎.

@tommy0103

Copy link
Copy Markdown
Contributor Author

Fenced in 19e7176: the check-cancel-clear sequence now runs inside the session rewrite barrier (the same primitive dispatch/steer respects), and the activity marker is rechecked inside it. A client turn can no longer start between the ownership snapshot and the awaited session/cancel, so the cancel can't land on a fresh client prompt, and presence can't be cleared out from under it; an engine turn that ended in the gap is not cancelled twice. New test: stale-snapshot recheck skips the cancel entirely.

@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: 19e71766f7

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

presence: this.sessionActivePresence.getStatus(sessionId),
execution: this.executionService.getExecutionSnapshot(sessionId),
hasPendingDispatch: this.sessionDispatchWatcher.hasPendingDispatch(sessionId),
engineTurnActive: this.store.isEngineTurnActive(sessionId),

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 Hold queued dispatches while engine turns are active

When an autonomous turn is producing updates and the user submits another message, this status makes the renderer treat the session as busy and enqueue the message. However, SessionDispatchWatcher builds both dispatch snapshots solely from executionService.getExecutionSnapshot() and durable session metadata, neither of which includes engineTurn; the queue mutation therefore wakes the watcher, which can immediately promote and dispatch the queued prompt against the same ACP agent while the cron/task turn is still running. Thread this activity predicate into daemon dispatch admission so queued work remains queued until the autonomous turn ends.

Useful? React with 👍 / 👎.

@Leeeon233 Leeeon233 self-assigned this Sep 13, 2026
@tommy0103

Copy link
Copy Markdown
Contributor Author

Fixed in 35a8bc7: getExecutionSnapshot now includes the engine-turn activity marker in hasActiveTurn (with an explicit comment that this is live work on the same agent). SessionDispatchWatcher's admission sees the session as active (active-session noop) while an engine turn runs, so a queued user message stays queued; when the turnEnded marker clears the activity, the next check chain dispatches it. Two snapshot tests added (engine turn → hasActiveTurn: true with no activeTurnId; cleared → false). This also removes the reliance on engine-side another turn is already in progress rejections as the only backstop.

@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: 35a8bc7056

ℹ️ 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 +1130 to +1131
const hasActiveTurn =
(typeof activeTurnId === 'string' && activeTurnId.length > 0) || engineTurnActive;

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 goal starts queued behind engine turns

When an engine-opened turn is active, this reports hasActiveTurn: true while leaving activeTurnId undefined. queueGoalTurn() only waits when both fields are present (session-execution-service.ts:1303-1306), so a Set/Resume action falls through to startGoalTurn() and opens a second ACP prompt against the same agent while the autonomous turn is still running, violating the goal contract and risking interleaved output. Add a waitable engine-turn owner or make the goal queue wait directly for engine activity to clear.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L50-L52

Useful? React with 👍 / 👎.

@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: 95c24c8575

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

!('title' in update.update) &&
!('updatedAt' in update.update)
) {
return;

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 Preserve a cancel identity for engine start markers

When a Kimi cron/task turn has emitted only its metadata-only turn.started marker, this return records the engine as busy but creates no unfinished assistant entry. The frontend derives activeAssistantTurnId solely from history, and canStopAgentEnabled requires that ID, so Stop is unavailable throughout the potentially long or hung pre-first-output period. Persist an owner entry that rendering may hide, or expose the autonomous cancel ID through live status so the turn can be stopped immediately.

Useful? React with 👍 / 👎.

Comment on lines +52 to +55
const isForkableAssistantMessage = (message: SessionHistoryParsed): boolean =>
message.role === 'assistant' &&
message.finished === true &&
(!message.acpTurnId || !isAutonomousTurnId(message.acpTurnId));

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 Match legacy fork targets to the CLI eligibility rule

When a completed legacy assistant without acpTurnId is followed by a completed autonomous entry, this predicate selects the legacy entry as lastForkableAssistantMessageId, so the header and side-panel actions submit it. However, cloneHistoryThroughTurn rejects any ID-less source that has a later finished assistant, returning SOURCE_TURN_NOT_FORKABLE; the newly added selector therefore exposes an action guaranteed to fail in the exact legacy-before-autonomous case. Fresh evidence beyond the earlier fork-target comment is that this new selector explicitly classifies that incompatible case as forkable; either keep legacy entries eligible only while they satisfy the CLI rule or align the CLI rule with this selection.

Useful? React with 👍 / 👎.

…y entries

An agent engine can open turns of its own behind the client's back (a
kimi cron fire, a task wake). Their ACP updates carried no turn identity,
so the history layer appended them to the last client turn's assistant
entry: the real reply was folded away and the interrupting turn's text
became the visible answer, and after a daemon restart such updates were
dropped outright.

- history-apply: route turn-stamped updates by ownership — scoped to
  kimi-stamped turns only (auto: id or turnOrigin marker): adopt only a
  delta-accepting entry or create assistant:autonomous-<turnId>. Every
  other id keeps the exact legacy last-wins restamping (claude per-message
  uuids, codex collab child turns are boundaries inside one client turn).
  turnEnded finalizes the owning entry once; acpTurnOrigin persists.
- session-transient-store: route marked updates to a synthesized
  autonomous target even while a client turn owns the session; maintain an
  engine-turn activity marker (set on first marked update, cleared on end
  marker or ACP process termination) feeding hasActiveTurn (idle-GC guard)
  and live status unknown -> running.
- Bump the kimi submodule for the matching stamping (turnId auto:<n> +
  turnOrigin + turnEnded on engine-opened turns).

Model: kimi-code/kimi-for-coding
…-resend paths

A finished engine-opened entry carries a deliberately non-fork-position
auto: id, but both selection paths treated any finished entry with an
acpTurnId as a fork boundary: Fork on such an entry forwarded the id to
session/fork, and edit-and-resend on the next user turn picked it as the
nearest preceding boundary. Skip autonomous entries while walking to the
real provider boundary in both paths, and reject them as fork sources in
cloneHistoryThroughTurn.

Model: kimi-code/kimi-for-coding
SessionManager reports unexpected agent death only through exit (the
terminated event fires solely on the explicit terminate path), so an
engine turn whose process crashed without a turnEnded marker kept the
session permanently busy and exempt from the idle GC. Clear the marker
on the manager's exit event as well.

Model: kimi-code/kimi-for-coding
The unfinished autonomous entry made resolveActiveAssistantTurnId hand
its id to the Stop control, but cancelSession only interrupts ids owned
by the client-turn registries, so the request fell through to the stale
path, reported success, and the engine turn kept running. In the stale
branch, an active engine turn now gets cancelled through the plain ACP
session cancel (the only live work on the agent) and the marker clears.

Model: kimi-code/kimi-for-coding
getSessionMonitorState read only presence and client-turn phase, so the
machine monitor showed an engine-turn session as idle mid-turn; and
resetMachineDisconnectedSessionsToIdle would reset a non-idle meta status
on a session whose only live work is an engine-opened turn. Read the
activity marker in both.

Model: kimi-code/kimi-for-coding
…ession for engine turns

Two review findings:

- The engine-turn cancel path cleared the activity marker before the ACP
  cancel, logged failures, and still returned success: a failed stop left
  the cron/task turn running while status went idle and the GC guard
  released the session. Now the marker clears only after a delivered
  cancel; failures (or an unavailable ACP session) keep it and return
  success: false, distinguishing an undeliverable cancel from a
  cancelled turn.
- enqueueACPUpdate ran recordSuppressedAcpReplay unconditionally, so a
  live engine turn firing during the loadSession replay-suppression
  window was dropped as replay (and its end marker with it, losing the
  turn permanently). Autonomous-identity updates resolve first and bypass
  suppression — replay carries no autonomous marker.

Model: kimi-code/kimi-for-coding
The ACP session cancel is session-wide and dispatch is not serialized
behind a stop request: a client turn starting between the ownership
snapshot and the awaited cancel would take the bullet meant for the
engine turn, and the branch would then clear the new turn's presence.
Hold the rewrite barrier across the check-cancel-clear sequence and
recheck the marker inside it, so no turn can start mid-flight and an
already-ended engine turn is not cancelled twice.

Model: kimi-code/kimi-for-coding
The dispatch snapshot read only client-turn ownership, so a user message
arriving during an engine-opened turn was promoted and dispatched against
the busy agent immediately (engine-side reject or a hidden engine queue).
Include the activity marker in getExecutionSnapshot hasActiveTurn, so
dispatch admission holds queued work until the engine turn's end marker
clears it.

Model: kimi-code/kimi-for-coding
@tommy0103
tommy0103 force-pushed the fix/engine-turn-origin-divider branch from 95c24c8 to 5f3797d Compare September 14, 2026 17:24

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

ℹ️ 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 +1103 to +1105
const engineTurnActive = this.deps.isEngineTurnActive(sessionId);
const hasActiveTurn =
(typeof activeTurnId === 'string' && activeTurnId.length > 0) || engineTurnActive;

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 Reject edit-and-resend while an engine turn is active

When a cron/task turn is active, this snapshot deliberately sets hasActiveTurn while omitting activeTurnId, but SessionEditAndResendService checks only freshExecution.activeTurnId after calling prepareReplacementSession. It therefore skips cancellation/waiting and can adopt the replacement ACP session and close the old one while the autonomous prompt is still executing, causing output loss or concurrent session mutation. Reject or wait on ownerless engine activity before preparing the replacement.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L50-L52

Useful? React with 👍 / 👎.

Separate engine prompt occupancy from client turn ownership and fail closed across provider preparation and persistence. Document the admission limitation and future architecture follow-ups.

Model: DimAgent

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

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

// Engine updates can replace the marker while ACP cancel is
// awaited. Never clear a replacement owner's activity.
if (this.deps.getEngineTurnOwnerForCancel(sessionId, turnId) === liveOwner) {
this.deps.clearEngineTurnActivity(sessionId, liveOwner);

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 engine activity until cancellation completes

When ACP acknowledges cancel() before the autonomous prompt has actually completed, clearing the marker here immediately makes getExecutionSnapshot() report the prompt slot as free and wakes engine-turn waiters, so a queued goal or new dispatch can start a second prompt while the cancelled work is still unwinding. The ordinary-turn path in this same method explicitly treats cancel acknowledgement as distinct from prompt completion; retain engine activity until turnEnded, raw prompt completion, or process termination confirms release.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L50-L51

Useful? React with 👍 / 👎.

Keep upstream completed-reply fork gating and context copying alongside the branch forkable-target logic that skips autonomous turns; union the session-history-writes evidence lists; rebase the kimi submodule branch onto upstream kimi main (usage accounting deltas) and re-point the gitlink; re-compress the session AGENTS rules to fit the 8 KiB gate while keeping both engine-turn boundaries; give the upstream steer-provenance test the engine-activity dependency.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants