Skip to content

fix(executor): clean up task state after cancel to prevent 'Request interrupted' error - #10

Merged
qdaxb merged 4 commits into
mainfrom
weagent/fix-cancel-task-state-cleanup
Jan 12, 2026
Merged

qdaxb merged 4 commits into
mainfrom
weagent/fix-cancel-task-state-cleanup

Conversation

@Micro66

@Micro66 Micro66 commented Jan 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • 修复取消任务后发送下一条消息报错 "Request interrupted by user" 的问题
  • 在 send_cancel_callback_async() 方法中添加任务状态清理逻辑

Problem

当用户取消 Claude 任务后,TaskStateManager 中的任务状态被设置为 CANCELLED。但是由于没有清理该状态,当用户发送下一条消息时:

  1. Executor 复用已存在的 agent session
  2. _async_execute() 方法检查 task_state_manager.is_cancelled(task_id) 返回 True
  3. 任务立即返回,导致 "Request interrupted by user" 错误

Solution

在 send_cancel_callback_async() 方法中,取消回调发送后调用 task_state_manager.cleanup(task_id) 清理任务状态,确保后续消息可以正常处理。

Changes

  • 在 executor/services/agent_service.py 中导入 TaskStateManager
  • 在取消回调发送后调用 task_state_manager.cleanup(task_id)
  • 在异常处理中也添加状态清理,确保状态始终被清理

Test plan

  • 取消正在执行的 Claude 任务
  • 取消后发送新消息,验证消息正常发送和处理
  • 验证不会出现 "Request interrupted by user" 错误

Summary by CodeRabbit

  • Bug Fixes

    • Improved robustness of agent creation, cancellation and callback flows with consistent cleanup and clearer error handling.
    • Fixed cases where task state could be left inconsistent after failures or cancel attempts.
  • Improvements

    • Clearer, more consistent logging and status/result formatting for agent operations.
    • Safer parsing of bot configuration values and standardized handling of missing values.
    • Standardized attachment workspace paths to a task-specific workspace.
  • Chores

    • Updated startup environment to reference the local network address for manager connectivity.

✏️ Tip: You can customize this high-level summary in your review settings.

Change attachment download path from project_path to WORKSPACE_ROOT/task_id to ensure attachments are saved to the correct location.

Expected: /workspace/{task_id}/{task_id}:executor:attachments/{subtask_id}/
Previous: /workspace/{task_id}/{repo_name}/{task_id}:executor:attachments/{subtask_id}/
…nterrupted" error

When a task is cancelled, TaskStateManager stores the CANCELLED state.
If the state is not cleaned up, the next message sent by the user will
check is_cancelled() which returns true, causing the agent to return
immediately with "Request interrupted by user" error.

This fix adds task state cleanup in send_cancel_callback_async() after
the cancel callback is sent, ensuring subsequent messages can be
processed normally.

Changes:
- Import TaskStateManager in agent_service.py
- Call task_state_manager.cleanup(task_id) after cancel callback is sent
- Add cleanup in error handler as well to ensure state is cleaned up
@coderabbitai

coderabbitai Bot commented Jan 11, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Refined AgentService behavior and logs: clarified agent creation vs reuse, safer bot_config parsing, stronger cancellation and cleanup with TaskStateManager, and standardized status/result logging. Claude code agent now stores attachments under WORKSPACE_ROOT/{task_id}.

Changes

Cohort / File(s) Summary
Agent service core
executor/services/agent_service.py
Standardized logging and status/result formatting; clarified agent creation vs reuse; safer bot_config parsing (handles list case, normalizes shell_type); expanded multi-line object construction for readability; strengthened cancellation flow with explicit logging and TaskStateManager cleanup; added defensive error handling around callback sending; minor type-annotation tweak for _instance.
Attachment workspace path
executor/agents/claude_code/claude_code_agent.py
_download_attachments now consistently uses WORKSPACE_ROOT/{task_id} for workspace paths (removes dependence on project_path/repo subdir); comments updated to reflect uniform task-specific workspace layout.
Startup env
start.sh
Executor manager startup environment updated: TASK_API_DOMAIN changed from http://localhost:8000 to http://<local_ip>:8000 and DOCKER_HOST_ADDR=localhost added.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant AgentService
  participant AgentStore
  participant AgentSession
  participant TaskStateManager
  participant CallbackSink

  Client->>AgentService: Start task/run request
  AgentService->>AgentStore: find existing agent/session by id
  alt agent exists
    AgentStore-->>AgentService: AgentSession
    AgentService->>AgentSession: update prompt / prepare
  else agent missing
    AgentService->>AgentStore: create AgentSession / Agent
    AgentStore-->>AgentService: new AgentSession
  end
  AgentService->>AgentSession: initialize & run
  AgentSession->>TaskStateManager: update state (running)
  AgentSession->>CallbackSink: send progress/results
  CallbackSink-->>AgentService: ack / error
  AgentService->>TaskStateManager: cleanup state on completion/failure
  AgentService-->>Client: final status/result
Loading
sequenceDiagram
  autonumber
  participant Client
  participant AgentService
  participant AgentSession
  participant TaskStateManager
  participant CallbackSink

  Client->>AgentService: Cancel request
  AgentService->>AgentSession: locate session / agent
  alt agent supports cancel
    AgentSession->>AgentService: cancel initiated
    AgentService->>TaskStateManager: mark cancelled / cleanup
    AgentService->>CallbackSink: send cancellation update
    CallbackSink-->>AgentService: ack / error
    AgentService-->>Client: cancellation confirmed
  else cancel unsupported or fails
    AgentService->>TaskStateManager: ensure cleanup fallback
    AgentService-->>Client: cancellation failed / not supported
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hop through logs with nimble paws,
I tidy prompts and mend the flaws,
Tasks get cancelled, workspaces neat,
Attachments stored in tidy seat,
Hooray — this rabbit's left code sweet! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the primary fix: cleaning up task state after cancel to prevent the 'Request interrupted' error, which aligns with the main problem and solution described in the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fefc698 and 80ded67.

📒 Files selected for processing (1)
  • start.sh
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: wecode-ai/Wegent PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T02:09:09.776Z
Learning: Applies to executor/agents/claude_code/**/*.py : ClaudeCode agent MUST support Claude Code SDK, Git clone, Skills, MCP servers, and custom instructions (.cursorrules, .windsurfrules)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test wegent CLI Integration
  • GitHub Check: E2E Tests (Shard 1/3)
  • GitHub Check: E2E Tests (Shard 2/3)
  • GitHub Check: E2E Tests (Shard 3/3)
🔇 Additional comments (1)
start.sh (1)

773-776: LGTM! Network configuration for docker container access is correct.

Using the host's local IP instead of localhost is the right approach for docker containers to reach host services. The added comments clearly explain the purpose of each variable.

One edge case to be aware of: if get_local_ip() falls back to "localhost" (which occurs when the ip route, hostname -I, and ifconfig methods all fail), docker containers will still be unable to reach the backend. In practice, this fallback is unlikely on production Linux systems where the ip route method works reliably, but it's worth noting for unusual network configurations.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @executor/services/agent_service.py:
- Around line 288-294: The cancel-callback currently does task_data =
getattr(agent, "task_data", {}) and then calls task_data.get(...), which will
crash if agent.task_data exists but is None or a non-dict; update the logic
around agent.task_data to ensure it's a dict before using .get: retrieve
agent.task_data, check isinstance(task_data, dict) (or coerce None/other types
to an empty dict), then read subtask_id, task_title, and subtask_title from that
safe dict so accesses in the cancel callback (references to agent.task_data,
subtask_id, task_title, subtask_title) never call .get on a non-dict.
🧹 Nitpick comments (1)
executor/services/agent_service.py (1)

118-120: Drop redundant {e} in logger.exception(...) messages (keeps stack trace, reduces noise).
Ruff’s TRY401 is right here: logger.exception(...) already records exception info.

Proposed fix
-            logger.exception(
-                f"[{_format_task_log(task_id, subtask_id)}] Exception during agent creation: {e}"
-            )
+            logger.exception(
+                f"[{_format_task_log(task_id, subtask_id)}] Exception during agent creation"
+            )
@@
-            logger.exception(
-                f"[{agent.get_name()}][{_format_task_log(agent.task_id, agent.subtask_id)}] Execution error: {e}"
-            )
+            logger.exception(
+                f"[{agent.get_name()}][{_format_task_log(agent.task_id, agent.subtask_id)}] Execution error"
+            )
@@
-            logger.exception(
-                f"[{_format_task_log(task_id, subtask_id)}] Task execution error: {e}"
-            )
+            logger.exception(
+                f"[{_format_task_log(task_id, subtask_id)}] Task execution error"
+            )
@@
-            logger.exception(
-                f"[{_format_task_log(task_id, -1)}] Error closing agent: {e}"
-            )
+            logger.exception(
+                f"[{_format_task_log(task_id, -1)}] Error closing agent"
+            )
@@
-            logger.exception(f"[{task_id}] Error sending cancel callback: {e}")
+            logger.exception(f"[{task_id}] Error sending cancel callback")

Also applies to: 132-135, 162-165, 180-183, 328-339

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7b32c0 and 1e6127d.

📒 Files selected for processing (1)
  • executor/services/agent_service.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-18T02:09:09.776Z
Learnt from: CR
Repo: wecode-ai/Wegent PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T02:09:09.776Z
Learning: Applies to executor/agents/**/*.py : Executor agents MUST support multiple types: ClaudeCode (local_engine), Agno (local_engine), Dify (external_api), Chat (direct_chat), ImageValidator (validator)

Applied to files:

  • executor/services/agent_service.py
📚 Learning: 2025-12-18T02:09:09.776Z
Learnt from: CR
Repo: wecode-ai/Wegent PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-18T02:09:09.776Z
Learning: Applies to executor/agents/claude_code/**/*.py : ClaudeCode agent MUST support Claude Code SDK, Git clone, Skills, MCP servers, and custom instructions (.cursorrules, .windsurfrules)

Applied to files:

  • executor/services/agent_service.py
🪛 Ruff (0.14.10)
executor/services/agent_service.py

115-115: Consider moving this statement to an else block

(TRY300)


119-119: Redundant exception object included in logging.exception call

(TRY401)


133-133: Redundant exception object included in logging.exception call

(TRY401)


163-163: Redundant exception object included in logging.exception call

(TRY401)


181-181: Redundant exception object included in logging.exception call

(TRY401)


328-328: Redundant exception object included in logging.exception call

(TRY401)


336-336: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: E2E Tests (Shard 1/3)
  • GitHub Check: E2E Tests (Shard 3/3)
  • GitHub Check: E2E Tests (Shard 2/3)
  • GitHub Check: Test Frontend
  • GitHub Check: Test wegent CLI Integration
🔇 Additional comments (3)
executor/services/agent_service.py (3)

123-135: Behavior-preserving improvements look good (logging clarity + cancel callback status + structured returns).

Also applies to: 146-152, 243-266, 295-317, 341-350, 398-399


319-339: Singleton pattern confirmed, but race condition concern is mitigated by agent-level cancellation.

The TaskStateManager() instances do share underlying state—it's a proper singleton using the double-checked locking pattern (lines 42–50 in task_state_manager.py). However, cleanup timing is not a blocker:

  • cancel_task() calls agent.cancel_run() synchronously before returning, which cancels execution at the agent level.
  • is_cancelled() checks in streaming loops (dify_agent.py:356, 469, 587; response_processor.py:75) rely on agent-internal state during execution.
  • The cleanup in send_cancel_callback_async() is defensive cleanup after the callback—not the primary cancellation mechanism.

The async gap between callback dispatch and state cleanup is acceptable because agents stop processing based on their own cancellation flags, not just TaskStateManager state.


49-56: Actual issue is opposite: Lines 63 and 143 incorrectly convert to str when lookups should use raw int keys.

The review's diagnosis is reversed. When json.loads() parses task_data, numeric values become Python int, not str. Storage at line 111 uses raw task_id (an int), making it consistent with cancel_task (line 232) and send_cancel_callback_async (line 280), which both correctly use raw task_id: int keys. However, create_agent (line 63) and execute_task (line 143) incorrectly call self.get_agent(f"{task_id}"), converting to str keys that won't match the stored int keys—causing session reuse to fail.

The correct fix is to remove the string conversions at lines 63 and 143, not add them elsewhere. Additionally, delete_session_async takes a str parameter (line 186) while the execute path uses int—this API inconsistency should also be resolved.

Likely an incorrect or invalid review comment.

Comment on lines +288 to +294
task_data = getattr(agent, "task_data", {})

# Get task information
subtask_id = task_data.get("subtask_id", -1)
task_title = task_data.get("task_title", "")
subtask_title = task_data.get("subtask_title", "")

logger.info(f"[{_format_task_log(task_id, subtask_id)}] Sending cancel callback asynchronously")


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.

⚠️ Potential issue | 🟡 Minor

Harden cancel-callback task_data access (can be None / non-dict).
getattr(agent, "task_data", {}) won’t protect you if task_data exists but is None (or another type), and .get(...) will raise.

Proposed fix
-            task_data = getattr(agent, "task_data", {})
+            task_data = getattr(agent, "task_data", None) or {}
+            if not isinstance(task_data, dict):
+                task_data = {}
🤖 Prompt for AI Agents
In @executor/services/agent_service.py around lines 288 - 294, The
cancel-callback currently does task_data = getattr(agent, "task_data", {}) and
then calls task_data.get(...), which will crash if agent.task_data exists but is
None or a non-dict; update the logic around agent.task_data to ensure it's a
dict before using .get: retrieve agent.task_data, check isinstance(task_data,
dict) (or coerce None/other types to an empty dict), then read subtask_id,
task_title, and subtask_title from that safe dict so accesses in the cancel
callback (references to agent.task_data, subtask_id, task_title, subtask_title)
never call .get on a non-dict.

Micro66 and others added 2 commits January 12, 2026 10:45
…path

fix(executor): use correct workspace path for attachment downloads
@qdaxb
qdaxb merged commit c945167 into main Jan 12, 2026
14 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jan 30, 2026
@qdaxb
qdaxb deleted the weagent/fix-cancel-task-state-cleanup branch February 4, 2026 09:33
qdaxb pushed a commit that referenced this pull request Jun 10, 2026
* feat(chat): add context status metrics and toolbar indicator

* feat(chat): finalize Phase 1 chat status toolbar and reconnect recovery

- Replace QuotaUsage with ChatToolbarStatus driven by chat:status_updated
- Wire useChatStatusIndicator to derive a fallback snapshot from the
  latest persisted AI message so toolbar status survives cold reloads
  before any live socket event arrives
- Type UnifiedMessage.result.context_metrics so persisted snapshots
  flow through the state machine without runtime casts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(chat_shell): add TruncationPolicy and GuardSource Protocol (T1)

Introduce the shared types module for Phase 2 context governance:
- TruncationPolicy: frozen dataclass with kind (bytes/tokens) and limit
- GuardSource: runtime_checkable Protocol for guard source adapters
- DEFAULT_EMERGENCY_RATIO = 0.3 and default_emergency_policy helper

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(chat_shell): add ToolOutputGuardAdapter and compact tool string format (T2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(chat_shell): tighten compact footer detection and dedupe encoding (T2 review)

* feat(chat_shell): add UnifiedContextGuard with source + compression pipeline (T3)

Phase 2 unified context guard. Runs as LangGraph pre_model_hook before every
model call (pre-turn and mid-turn alike) and applies:

1. Source-level pass: convert raw tool messages to compact form via the
   registered GuardSource adapters; mark `compacted=true` so subsequent
   passes treat them as already-handled.
2. Request-level compression: when live state exceeds the model trigger
   limit, run MessageCompressor and translate its diff into LangGraph
   state updates (RemoveMessage for dropped ids, fresh BaseMessage for
   synthesized summaries; compacted=true marker propagates).
3. Emergency re-truncation: stub for T7; logs a warning when compression
   alone is insufficient.

Skips messages without a stable id since add_messages cannot upsert them
without doubling content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(chat_shell): wire UnifiedContextGuard as pre_model_hook (T4)

Phase 2 wiring task:

- chat_service constructs UnifiedContextGuard with ToolOutputGuardAdapter
  as the only source. The guard is chained AFTER guidance_consumer's hook
  via chain_pre_model_hooks so guidance injection observes already-budgeted
  state and writes its llm_input_messages on top of the compacted view.
- agent.build_messages no longer invokes MessageCompressor — budget
  enforcement now lives entirely in the pre_model_hook, which fires on
  pre-turn AND every mid-turn follow-up. Cache-breakpoint application
  remains in build_messages on the assembled layout.
- Drops the now-unused MessageCompressor import from agent.py.
- Adds guard/composition.py with chain_pre_model_hooks, supporting mixed
  sync/async hooks; messages updates concatenate, llm_input_messages from
  the last hook wins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(chat_shell): finalize Phase 2 context governance (T7+T8+T9 + review)

Completes Phase 2 by adding emergency re-truncation, removing legacy
serialization-time truncation, refactoring the metrics tracker to
emit-only, and addressing the review findings on the resulting code.

T7 — Emergency re-truncation
- UnifiedContextGuard stage 3 now re-renders source-owned messages
  under each source's emergency policy, biggest-first with early stop
  once usage drops below trigger.
- ToolOutputGuardAdapter accepts an emergency_ratio (defaults to
  DEFAULT_EMERGENCY_RATIO); chat_service passes settings.EMERGENCY_TOOL_OUTPUT_RATIO.
- Synthesized messages from stage 2 are excluded from the candidate set
  so the upsert-by-id contract holds.

T8 — Delete legacy _truncate_tool_result_for_storage
- graph_builder._serialize_messages_chain no longer chops tool results
  at serialization time; budget enforcement is owned by the guard.
- Removed MAX_TOOL_RESULT_LENGTH from config and its tests.

T9 — Tracker → emit-only wrapper
- ContextMetricsTracker no longer keeps its own messages duplicate.
  Single API: capture(messages, phase). Snapshot computation is
  delegated to an injected metrics_fn so accounting flows from one
  source.
- chat_service captures BUILD_MESSAGES at turn start and FINAL at end
  with messages + agent_builder._last_messages_chain.
- tools/events drops the per-tool tracker calls.

Review fixes
- #1: extract_raw now parses already-compact strings via the new
  _split_compact helper, so stage 3 emergency re-render no longer
  produces nested headers / fabricated total_tokens counts.
- #2: UnifiedContextGuard.__call__ is async and emits a metrics
  snapshot via the injected tracker on every invocation. Phase is
  PHASE_AFTER_COMPACTION when compaction happened, PHASE_AFTER_TOOL_END
  otherwise; bucket-throttling in should_emit_status_update suppresses
  redundant emits. Tests converted to async.
- #3: ToolResultTruncationStrategy now skips messages flagged
  additional_kwargs.compacted=True. Without this, stage 2 compression
  would corrupt the guard's fixed-format compact strings, leaving a
  misleading total_tokens count and mixing two truncation notices.
- #5: Documented the source-disjointness assumption in
  _apply_source_pass — registered GuardSources must have non-overlapping
  applies_to predicates.
- #6: calculate_context_metrics accepts pre-built TokenCounter and
  ModelContextConfig; guard.metrics() reuses its instance state
  instead of constructing fresh ones on every snapshot.
- #8: Kept default_emergency_policy as a documented public reference
  helper for future GuardSource impls.
- #9: _dict_to_base_message forwards d.get('id') so the compressor's
  synthesized ids survive the round-trip.
- #10: New test verifies cache_control markers in additional_kwargs
  survive guard compaction (Anthropic prompt-cache compatibility).

Tests
- 14 new tests across guard composition, tracker emit, compact-string
  roundtrip, id propagation, cache-breakpoint preservation, and
  compressor skip behavior. Total chat_shell suite: 549 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(chat-shell): finalize phase2 context guard

* fix(chat-shell): tune tool guard controls and diagnostics

* feat(knowledge): add listing pagination and guard fixes

* fix(frontend): improve chat toolbar status sections

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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