Skip to content

fix(approval): scope pending requests to turn lifecycle - #2086

Closed
wbxl2000 wants to merge 1 commit into
mainfrom
codex/approval-lifecycle-cleanup
Closed

fix(approval): scope pending requests to turn lifecycle#2086
wbxl2000 wants to merge 1 commit into
mainfrom
codex/approval-lifecycle-cleanup

Conversation

@wbxl2000

@wbxl2000 wbxl2000 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Make approval waits indefinite by default so active foreground/subagent approvals are not auto-rejected after 5 minutes.
  • Cancel foreground-turn approvals through the existing ApprovalSource/cancel_by_source lifecycle when the owning KimiSoul run exits.
  • Cancel abandoned KimiCLI.run streams with an internal run cancel event, without mutating the caller-owned cancel_event.
  • Keep explicit timeout behavior covered for future policy/config use.

Context

This separates lifecycle correctness from timeout policy. The old 300s fallback prevented some hangs, but it also caused active approvals to be rejected as if the user rejected them. This PR uses source-scoped lifecycle cleanup as the primary completion path instead.

Tests

  • uv run pytest tests/core/test_approval_runtime.py tests/core/test_notifications.py
  • uv run ruff check src/kimi_cli/app.py src/kimi_cli/approval_runtime/runtime.py src/kimi_cli/soul/kimisoul.py tests/core/test_approval_runtime.py tests/core/test_notifications.py
  • uv run ruff format --check src/kimi_cli/app.py src/kimi_cli/approval_runtime/runtime.py src/kimi_cli/soul/kimisoul.py tests/core/test_approval_runtime.py tests/core/test_notifications.py

Open in Devin Review

Copilot AI review requested due to automatic review settings April 27, 2026 11:10
@wbxl2000

Copy link
Copy Markdown
Collaborator Author

Superseded by #2087 using the requested branch naming convention.

@wbxl2000 wbxl2000 closed this Apr 27, 2026
@wbxl2000
wbxl2000 deleted the codex/approval-lifecycle-cleanup branch April 27, 2026 11:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts approval lifecycle handling so that approvals are no longer auto-rejected by a default timeout, and instead are cleaned up via explicit source-scoped cancellation when the owning run/stream ends.

Changes:

  • Make ApprovalRuntime.wait_for_response() wait indefinitely by default (timeout=None), while keeping explicit timeout support.
  • Cancel foreground-turn approval requests via cancel_by_source when a KimiSoul.run()-created approval source exits.
  • In KimiCLI.run(), introduce an internal cancel event to cleanly cancel abandoned runs/streams without mutating the caller’s cancel_event, and propagate external cancellations correctly.
  • Add/adjust tests and update changelogs to reflect the new behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/core/test_notifications.py Adds coverage for abandoning KimiCLI.run() streams and for external cancellation propagation.
tests/core/test_approval_runtime.py Adds tests for indefinite default waiting, explicit timeout, and run-scoped cancellation behavior.
src/kimi_cli/soul/kimisoul.py Tracks whether a foreground approval source was created and cancels by source in finally.
src/kimi_cli/approval_runtime/runtime.py Changes default approval wait timeout from 300s to indefinite (None).
src/kimi_cli/app.py Uses an internal run cancel event to cancel abandoned runs without mutating the external cancel event; selectively swallows internal RunCancelled.
docs/zh/release-notes/changelog.md Documents removal of the 5-minute auto-timeout behavior in Core approvals.
docs/en/release-notes/changelog.md Same as above (English release notes).
CHANGELOG.md Same as above (root changelog).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 99 to 103
self._waiters[request_id] = waiter
if timeout is None:
return await asyncio.shield(waiter)
try:
return await asyncio.wait_for(asyncio.shield(waiter), timeout=timeout)

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

wait_for_response() now awaits the underlying waiter indefinitely when timeout is None. If the calling task is cancelled (e.g., run cancellation), asyncio.shield(waiter) will raise CancelledError to the caller while leaving the stored waiter future pending; later lifecycle cleanup (e.g., cancel_by_source) sets an exception on that future, which can trigger noisy "Future exception was never retrieved" warnings because nothing is awaiting it anymore. Consider ensuring waiter exceptions are always consumed (e.g., add a done-callback that calls future.exception()), or otherwise avoid setting un-retrieved exceptions on orphaned waiters during cancellation cleanup.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +129
@pytest.mark.asyncio
async def test_approval_runtime_wait_for_response_explicit_timeout() -> None:
runtime = ApprovalRuntime()
request = runtime.create_request(
request_id="req-timeout",
tool_call_id="call-timeout",
sender="WriteFile",
action="edit file",
description="Write file /tmp/test.txt",
display=[],
source=ApprovalSource(kind="foreground_turn", id="turn-timeout"),
)

with pytest.raises(ApprovalCancelledError):
await runtime.wait_for_response(request.id, timeout=0.05)

record = runtime.get_request(request.id)
assert record is not None
assert record.status == "cancelled"
assert record.feedback == "approval timed out"

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The explicit-timeout behavior is still tested at ApprovalRuntime.wait_for_response(), but this no longer verifies that the timeout feedback (e.g. "approval timed out") is propagated through the higher-level Approval.request()/ApprovalResult.rejection_error() surface. Since the PR description calls out keeping explicit timeout behavior covered for future policy/config use, consider reintroducing an integration-style test that exercises Approval.request() and asserts the resulting rejection includes the timeout feedback (to prevent regressions back to the generic Rejected by user).

Copilot uses AI. Check for mistakes.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

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