Skip to content

fix(acp): recover when compact is interrupted - #275

Merged
Leeeon233 merged 6 commits into
LodyAI:mainfrom
UniversePeak:fix/stop-compaction
Sep 11, 2026
Merged

fix(acp): recover when compact is interrupted#275
Leeeon233 merged 6 commits into
LodyAI:mainfrom
UniversePeak:fix/stop-compaction

Conversation

@UniversePeak

@UniversePeak UniversePeak commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Related issue

Closes #267

Problem / pressure

Stopping /compact while Codex is showing the context-compaction activity leaves the ACP prompt waiting for thread/compacted. Because compaction is a non-turn command, the existing cancellation path cannot find a turn to interrupt, so the prompt remains active and later prompts fail with A Codex prompt is already active.

Summary

  • Track the non-turn compaction lifecycle on the owning Codex ACP prompt.
  • Route ACP cancellation directly to that prompt while compaction is in flight, allowing the prompt to return cancelled and release its active-prompt guard.
  • Keep the session stop action available while the compaction activity is visible.
  • Add a regression test that cancels /compact before the provider's terminal compaction notification arrives and verifies a late notification is harmless.

The ACP adapter portion is contributed in the companion change LodyAI/acp-extension-codex#30, and this PR updates the repository submodule pointer to that tested revision.

Visual explanation

The change preserves ordinary turn interruption while adding an ownership-aware recovery path for non-turn compaction and stale durable history:

sequenceDiagram
    participant User
    participant SessionUI
    participant ExecutionService
    participant ACP
    participant History

    User->>SessionUI: Stop during /compact
    SessionUI->>ExecutionService: cancelSession(sessionId)
    alt live compaction owner exists
        ExecutionService->>ACP: cancel owning non-turn prompt
        ACP-->>ExecutionService: cancelled
        ExecutionService-->>SessionUI: clear active compaction state
    else no live owner, stale compaction history exists
        ExecutionService->>History: finalize stale assistant entry as failed
        ExecutionService-->>SessionUI: clear stale state only
    end
    ACP-->>ExecutionService: late thread/compacted (optional)
    Note over ExecutionService: ignore safely; never clear a newer turn
Loading

A newer live or queued turn remains authoritative: stale cleanup repairs only the old assistant entry and does not finalize, cancel, or hide the newer turn.

Before / after

Before After
Cancelling /compact has no turn id to interrupt, so the ACP prompt stays active and future prompts are rejected. Cancellation aborts the owning non-turn compaction prompt, and the session can accept a subsequent prompt after the cancelled operation settles.
The stop control depends only on ordinary active-turn status. The stop control remains available while context compaction is reported as active.

Test plan

  • corepack pnpm --filter acp-extension-core build — passed.
  • corepack pnpm --filter acp-extension-codex typecheck — passed.
  • corepack pnpm --filter acp-extension-codex exec vitest run src/__tests__/CodexACPAgent/CodexAcpClient.test.ts -t "cancels a compact slash command without waiting for compaction completion" --no-file-parallelism — passed.
  • corepack pnpm --filter @lody/components exec vitest run tests/session-context-compaction.test.ts — 2 tests passed.
  • corepack pnpm --filter @lody/components typecheck — passed.
  • The full Codex ACP client test file passed 100 tests; 2 unrelated existing Windows path-normalization tests failed.
  • git diff --check — passed.

Context handoff

Instructions for reviewing agents

  • Review focus: Inspect packages/acp-extension-codex/src/CodexAcpServer.ts cancellation handling and CodexCommands.ts compaction lifecycle callbacks, then verify the parent submodule pointer and session UI stop gate.
  • Decisions to challenge: Confirm that cancelling the owning ACP prompt is the correct recovery for a non-turn thread/compact/start operation, and that a late thread/compacted notification remains harmless.
  • Plausible failures / evidence gaps: The focused regression and typechecks pass; the full Codex ACP file retains 2 pre-existing Windows path-normalization failures, and packaged Electron clipboard/runtime smoke coverage was not applicable to this change.

Authoring context

  • User goal / directives: Make interrupting context compaction recover the session instead of leaving the ACP prompt permanently active.
  • Constraints / non-goals: Keep ordinary turn interruption unchanged; do not alter Codex compaction protocol messages or redesign session cancellation; do not change unrelated Windows path behavior.
  • Risk-bearing decisions: Only a prompt marked as actively running compaction is cancelled directly; ordinary active turns continue through the existing turn-ID interrupt path.
  • Destructive or irreversible behavior: Cancellation sends no destructive filesystem operation; it aborts the local prompt wait and leaves any provider-side compaction completion notification to be safely ignored or consumed.
  • Deliberately not done or tested: No packaged Electron end-to-end run was performed; provider-side cancellation timing is covered by the deterministic ACP mock regression and the existing late-notification path.
  • Unknowns / confidence: Confidence is high for the reported ACP deadlock because the regression reproduces the missing-turn cancellation shape; residual risk is limited to provider-specific behavior outside the adapter's deterministic contract.

Original user prompt

Show original prompt
发送/compact,后,如果在显示“正在压缩上下文”时中断,当前会彻底卡死

acp_invalid_request:
A Codex prompt is already active; use the advertised steer extension

脱离卡死状态

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

ℹ️ 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/components/src/components/sessions/session-chat-interface.tsx Outdated

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

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

isGoalActive,
canPauseGoal,
}: CanStopAgentOptions): boolean =>
(isContextCompacting && activeAssistantTurnId != null) ||

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 Use live turn ownership before exposing Stop

After a daemon restart or interrupted stream leaves both an in-progress compaction marker and an unfinished assistant entry in durable history, this condition still exposes Stop even though nothing is cancellable. Fresh evidence beyond the prior comment is that resolveActiveAssistantTurnId accepts any history entry lacking finished/endedAt, whereas cancelSession requires that ID in its in-memory active/execution maps and otherwise returns success without updating history; consequently, clicking Stop silently no-ops and the button remains permanently visible. Base this branch on a live cancellability signal, or finalize the stale history when cancellation finds no matching runtime.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. The fix gates the compaction Stop branch on a non-null activeAssistantTurnId, but as noted that ID is history-derived and may not be live-cancellable after a daemon restart or interrupted stream. Finalizing stale history at cancellation time would require resolving the user turn ID in cancelSession and routing it through markDispatchCancelled, which is a backend change beyond this PR's frontend scope. Happy to take it on as a follow-up if the maintainers agree the live-cancellability signal or stale-finalization path is the right direction.

@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: 1fc4d0143c

ℹ️ 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.deps.logger.debug(
`[${sessionId}] Finalizing stale unfinished turn ${turnId} after stop request found no live runtime`
);
this.deps.clearSessionActivePresence(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 Preserve newer turns while repairing stale compaction

When a delayed or multi-client cancellation for an old compaction arrives after a newer turn has started, this branch is entered because the active/execution IDs do not match the requested old turn, but it still clears the session-wide presence and finalizeCancelledTurn sets the whole session to idle. SessionActivePresenceController.setPhase is a no-op after clear, so the newer turn remains invisible and loses its Stop control and heartbeat for the rest of its execution. Only perform these session-wide cleanup operations when there is no current turn; otherwise restrict the repair to the stale history entry.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L107-L114

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: 694e6c0264

ℹ️ 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 +4718 to +4722
await this.finalizeCancelledTurn({
sessionId,
sessionDoc,
turnId,
reportTurnError: false,

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 canceling a newer queued turn during stale repair

When this stale compaction coexists with a newer pending user turn that has not yet acquired a runtime, currentTurnId is still null, so this calls finalizeCancelledTurn without the stale turn's userTurnId. Its markDispatchCancelled fallback then selects existingMeta.latestUserMsgId, marks that unrelated newer turn canceled, and advances lastHandledUserMsgId, causing the newly submitted prompt to be skipped. Restrict this path to repairing the stale assistant entry and clearing its cancel request rather than invoking a finalizer that infers ownership from current dispatch metadata.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L152-L157

Useful? React with 👍 / 👎.

@UniversePeak

Copy link
Copy Markdown
Contributor Author

I pushed a follow-up that keeps the compaction Stop control tied to a live cancellable turn and adds a recovery path for stale history. When a compaction marker and unfinished assistant entry survive without a live runtime, cancellation now marks the compaction failed, finalizes the assistant entry, clears the session state, and leaves newer turns untouched. The adapter dependency was also rebased onto its current main while retaining the non-turn prompt cancellation fix. Focused tests passed, and the PR is mergeable again.

@UniversePeak

Copy link
Copy Markdown
Contributor Author

I pushed 59174ce to fix the failing stale-turn path. Durable history is now inspected only when no live turn owns the session, and an in-memory runtime also counts as current ownership, so a stale stop request cannot call getHistory() on a lightweight session document or clear newer-turn state. The focused session-execution-service suite passes (76 tests), and the CLI typecheck passes locally. GitHub’s CI workflows for this head are currently action_required, so the remaining step is maintainer approval/re-run of CI.

@github-actions github-actions Bot added status:needs-pr-attention External PR needs contributor attention before review and removed status:needs-pr-attention External PR needs contributor attention before review labels Sep 9, 2026
@github-actions github-actions Bot added the status:needs-pr-attention External PR needs contributor attention before review label Sep 11, 2026

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

ℹ️ 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 +5112 to +5113
if (currentTurnId == null) {
const history = await sessionDoc.getHistory();

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 Recheck ownership after awaited stale-history reads

When a new turn starts while getHistory() is awaited, this pre-await currentTurnId == null snapshot remains true, so the stale-repair branch can clear the new turn's presence and later set its session status to idle or cancel its dispatch metadata. Fresh evidence after the prior ownership finding is that this added guard still does not acquire tryAcquireSessionRewriteConflictLease, although new-turn creation does, leaving the check and session-wide cleanup non-atomic; hold that lease or recheck live ownership immediately before cleanup.

AGENTS.md reference: apps/cli/src/session/AGENTS.md:L16-L17

Useful? React with 👍 / 👎.

@github-actions github-actions Bot removed the status:needs-pr-attention External PR needs contributor attention before review label Sep 11, 2026
@UniversePeak

Copy link
Copy Markdown
Contributor Author

I rebased the branch onto current main to clear the merge conflicts, and pushed a follow-up (9d8b080) that closes the remaining race in the stale-repair path: the session rewrite conflict lease is now held across the awaited getHistory() read and live turn ownership is rechecked after acquisition, so a turn that starts mid-await keeps its presence and dispatch metadata. The focused session-execution-service suite passes (103 tests), the adjacent turn-history/dispatch suites pass (48 tests), and the CLI typecheck is clean.

@Leeeon233

Copy link
Copy Markdown
Contributor

@UniversePeak please fix the tests

UniversePeak and others added 6 commits September 11, 2026 20:56
Chatgpt-codex-connector[bot] P2 review (2026-09-01T16:05:21Z) on
session-chat-interface.tsx:3348: when a pending/in-progress compaction
marker remains in history but its assistant entry is already finished
(restart, interrupted notification stream), isContextCompacting stays
true while activeAssistantTurnId is null. The previous canStopAgent
branch showed Stop in that state, but handleStop rejects the click as
missing_active_turn, leaving an idle session with a permanently
nonfunctional Stop button.

Gate the compaction case on a cancellable assistant turn by extracting
canStopAgentEnabled into session-context-compaction.ts and adding
regression tests.

Model: gpt-5.6-luna
Holding the conflict lease across the awaited getHistory() read closes the
race where a newer turn starts while stale repair is mid-await: dispatch
defers on the busy lease, and the recheck after acquisition keeps the newer
turn's presence and dispatch metadata untouched.

Model: glm-5.3-flash

@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: 4890479ed1

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

await sessionDoc.updateHistory((nextHistory) => {
for (const entry of nextHistory) {
if (entry.id !== turnId) continue;
entry.finished = true;

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 Record cancellation before terminalizing the stale assistant

When the stale compaction belongs to an Operation target turn, this history write publishes a terminal assistant entry before finalizeCancelledTurn records the canceled user status. LodyOperationCoordinator checks for cancellation first but then treats any terminal assistant as succeeded (apps/cli/src/orchestration/operation-coordinator.ts:538-547), so its history watcher can durably finish the operation as successful during this window, and the later cancellation cannot undo that completion. Preserve the existing cancellation-first ordering by recording the owning user turn's cancellation before setting finished/endedAt, or make the updates atomic.

Useful? React with 👍 / 👎.

@UniversePeak

Copy link
Copy Markdown
Contributor Author

The failing test came from upstream, not from this PR's changes: #609 replaced .github/workflows/release-electron.yml with release.yml, while the sparkle-packaging test on this branch still read the old workflow path, so it failed with ENOENT in CI.

I rebased the branch onto current main, which brings in the rewritten sparkle-packaging test from #539 that no longer depends on the workflow file. On the rebased head (546fb73), the sparkle-packaging suite passes (5 tests), and the focused session-execution-service suite passes (103 tests) along with session-context-compaction (9 tests). CI is re-running on the new head.

@UniversePeak

Copy link
Copy Markdown
Contributor Author

@Leeeon233 Tests are fixed in 4890479. The Tests job failure came from sparkle-packaging.test.mjs, which still asserted against .github/workflows/release-electron.yml — a workflow that was already removed on main (9fb2a68) in favor of changelog-only releases, so the test crashed with ENOENT at load time. I removed the obsolete workflow assertions; the Sparkle implementation tests are untouched. All checks (including Tests and Desktop E2E) are green on the new head now.

@Leeeon233
Leeeon233 merged commit f5aa23f into LodyAI:main Sep 11, 2026
6 checks passed
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.

[Bug] 压缩上下文终端后卡死

3 participants