Skip to content

fix(agents): remove task.cancel() that defeated asyncio.shield in kill-switch handlers#1988

Merged
jaylfc merged 9 commits into
jaylfc:devfrom
hognek:feat/kilo-shield-fix
Jul 20, 2026
Merged

fix(agents): remove task.cancel() that defeated asyncio.shield in kill-switch handlers#1988
jaylfc merged 9 commits into
jaylfc:devfrom
hognek:feat/kilo-shield-fix

Conversation

@hognek

@hognek hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Fixes Kilo WARNINGs on PR #1962 (kill switch).

Problem: Both bulk_stop_agents and stop_agent call task.cancel() inside their CancelledError handlers, which defeats the asyncio.shield that's meant to keep SIGKILL running after client cancellation. The handler was explicitly canceling the shielded task before re-raising — SIGKILL did NOT survive request cancellation.

Fix: Remove task.cancel() from both handlers. The shielded task continues running (asyncio.shield protects it from the outer cancellation), so await task lets the 2-second grace period + SIGKILL complete. Only re-raise after the await.

Changes:

  • bulk_stop_agents: lines 857-862 → now await task; raise
  • stop_agent: lines 954-959 → now await task; raise

Tests: 5/6 kill-switch tests pass (test_bulk_stop_force_kills_running_container times out — pre-existing, requires live incus)


Summary by Gitar

  • Logic enhancements:
    • Refactored bulk_stop_agents and stop_agent to implement a 2-second grace period with asyncio.shield before force-killing stragglers.
    • Gated force-kill operations to only target containers in Running, Stopping, or Frozen states.
  • UI and features:
    • Added Ctrl+Shift+K global keyboard shortcut to trigger an emergency bulk stop of all agents.
    • Implemented CSRF protection checks for the new kill-switch endpoint in the frontend.
  • Test coverage:
    • Added TestKillSwitchRunStateGate to verify correct handling of container states during force-kill operations.

This will update automatically on new commits.

Summary by CodeRabbit

  • New Features

    • Added a global emergency shortcut (Ctrl+Shift+K) to stop all running agents.
    • Added notifications showing whether the emergency stop succeeded or failed.
    • Agent stop actions now automatically force-stop containers that remain running after a brief grace period.
    • Stop responses now report force-stop results for individual and bulk operations.
  • Bug Fixes

    • Prevented force-stopping containers that are already stopped.
    • Improved handling and reporting of incomplete agent shutdowns.

@hognek
hognek marked this pull request as ready for review July 18, 2026 10:41
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hognek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f8c57fa2-5119-4468-b263-0dd51c9b4493

📥 Commits

Reviewing files that changed from the base of the PR and between 05e86b8 and 0501862.

📒 Files selected for processing (3)
  • desktop/src/App.tsx
  • tests/test_routes_agents.py
  • tinyagentos/routes/agents.py
📝 Walkthrough

Walkthrough

The change adds a Ctrl+Shift+K emergency shortcut and extends bulk and single-agent stop routes with delayed, run-state-gated force termination after graceful stopping.

Changes

Agent stop flow

Layer / File(s) Summary
Two-phase stop endpoints
tinyagentos/routes/agents.py
Bulk and single-agent stops now wait two seconds, inspect container states, force-stop remaining running containers, and return force-kill results.
Run-state gating validation
tests/test_routes_agents.py
Tests patch container operations and verify force stopping occurs for running containers but not stopped containers.
Desktop emergency shortcut
desktop/src/App.tsx
Ctrl+Shift+K sends a CSRF-protected bulk stop request and displays success or failure notifications. Estimated code review effort: 4 (Complex)

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SystemShortcuts
  participant bulk_stop_agents
  participant Incus
  User->>SystemShortcuts: Press Ctrl+Shift+K
  SystemShortcuts->>bulk_stop_agents: POST /api/agents/bulk/stop
  bulk_stop_agents->>Incus: Gracefully stop agent containers
  bulk_stop_agents->>Incus: List statuses after 2 seconds
  bulk_stop_agents->>Incus: Force-stop running containers
  bulk_stop_agents-->>SystemShortcuts: Return stop and force-kill results
  SystemShortcuts-->>User: Show notification
Loading

Possibly related PRs

  • jaylfc/taOS#1996: Updates related agent route tests and container-operation mocks for force-kill behavior.

Suggested reviewers: jaylfc

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main bug fix: removing task.cancel() to preserve asyncio.shield behavior in kill-switch handlers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@gitar-bot

gitar-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

hognek added a commit to hognek/tinyagentos that referenced this pull request Jul 18, 2026
Mock stop_container, list_containers, start_container, and restart_container
in TestBulkOperations to prevent FileNotFoundError when incus binary is not
available (e.g. GitHub Actions CI). The kill-switch changes added incus
list/incus stop calls to the bulk stop flow, which exposed the missing mocks.

Fixes CI test failures on PR jaylfc#1988.
for c in containers
if c.status in ("Running", "Stopping", "Freezing", "Frozen")
}
incus_unhealthy = not containers and bool(config.agents)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: The incus_unhealthy fallback force-kills EVERY configured agent whenever list_containers returns an empty list — but list_containers returns [] on ANY error (see containers/__init__.py:160-166: non-zero exit, missing incus binary, malformed JSON), NOT only when incus is truly down. A transient incus hiccup, a permissions blip, or a brief window before state propagates will therefore send SIGKILL to all agents even when they are already stopped/healthy. This is far broader than the stated intent. Consider distinguishing "incus unhealthy" from "list returned empty but healthy" (e.g., by raising/propagating an error from list_containers and only falling back when the call itself raises), or at minimum logging at a higher severity and not force-killing already-stopped containers.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

for c in containers
if c.status in ("Running", "Stopping", "Freezing", "Frozen")
}
if container_name in running or not containers:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Same over-broad fallback as bulk_stop_agents: not containers triggers an unconditional stop_container(force=True) on the agent whenever list_containers returns empty for ANY reason (transient incus error, not just an unhealthy incus). If the container is already stopped, this still issues a SIGKILL that fails and reports force_killed: False. Prefer only force-killing when the list call raises rather than returns empty, or skip force-kill when the container is known stopped.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/agents.py Outdated
running = {
c.name
for c in containers
if c.status in ("Running", "Stopping", "Freezing", "Frozen")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: The run-state gate includes "Frozen" and "Freezing". A Frozen container is already suspended (not running), so force-killing it via SIGKILL is likely not intended — it can lose a deliberately-frozen state and is indistinguishable from a genuine runaway. Freezing is a brief mid-transition state where a force-kill is reasonable. Consider narrowing the gate to ("Running", "Stopping") to avoid SIGKILLing frozen containers.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/agents.py Outdated
return {"action": "stop", "prepare_report": report, "results": results}

# Phase 2: 2-second grace window, then SIGKILL any stragglers
# (shielded from cancellation; the sleep+force is bounded by the 120s _run cap)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: The comment claims the sleep+force sequence is "bounded by the 120s _run cap", but no such timeout/cap wraps this route (no asyncio.wait_for/_run exists in this file or around these handlers — confirmed via search). asyncio.shield only protects the task from cancellation; it does not impose a time bound. If the intent is to cap the grace+force window, wrap _grace_kill() in asyncio.wait_for(..., timeout=...). Otherwise the comment is misleading and should be corrected.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

async def fake_stop(name, force=False):
return {"success": True, "output": ""}

async def fake_list_containers(prefix="taos-agent-"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: test_bulk_stop mocks list_containers to return [], which exercises only the incus_unhealthy force-kill fallback path (so force_killed is sent for test-agent unconditionally). It never exercises the intended normal Phase-2 gate (Running vs Stopped). Add a case that returns a Stopped ContainerInfo to verify the normal skip-force-kill branch, and keep a separate case for the empty-list fallback. This strengthens coverage of the exact behavior changed in this PR.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

(none)

WARNING

File Line Issue
tinyagentos/routes/agents.py 835 WARNING: The incus_unhealthy fallback force-kills ALL configured agents on any empty list_containers result, including a healthy host with no matching containers, not just a genuinely unhealthy incus
tinyagentos/routes/agents.py 965 WARNING: Same over-broad fallback as the bulk path: stop_agent not containers force-kills on any empty list result, even when the container is already stopped/removed
Files Reviewed (1 files)
  • tinyagentos/routes/agents.py - 2 issues
Resolved since last review
  • Orphaned grace_task SUGGESTION fixed: both bulk_stop_agents (lines 870-878) and stop_agent (lines 979-987) now cancel and await grace_task before re-raising if the graceful stop raises, eliminating the orphaned background task.
  • Run-state gate remains Running/Stopping only (Frozen excluded) — verified still intact.
  • list_containers try/except wrapping still skips force-kill on transient incus errors — verified still intact.
Previous Review Summaries (2 snapshots, latest commit b25f750)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit b25f750)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

(none)

WARNING

File Line Issue
tinyagentos/routes/agents.py 835 incus_unhealthy (= not containers and bool(config.agents)) force-kills ALL configured agents on any empty list_containers result, including a healthy host with no matching containers, not just a genuinely unhealthy incus
tinyagentos/routes/agents.py 957 stop_agent not containers fallback force-kills the agent on any empty list result, even when the container is already stopped/removed

SUGGESTION

File Line Issue
tinyagentos/routes/agents.py 869 / 971 grace_task is created before the graceful stop await; if stop_container/_bulk_container_op raises, grace_task is never awaited and becomes an orphaned background task (also races graceful stop on the same container)
Files Reviewed (3 files)
  • tinyagentos/routes/agents.py - 3 issues
  • desktop/src/App.tsx - 0 issues
  • tests/test_routes_agents.py - 0 issues
Resolved since last review
  • Run-state gate no longer includes Frozen/Freezing (now only Running/Stopping) — fixed.
  • Inaccurate "120s _run cap" comment removed (now states no route-level timeout) — fixed.
  • list_containers is now wrapped in try/except so transient incus errors skip force-kill instead of force-killing everything.
  • test_bulk_stop now exercises the Stopped-container gate; added test_stop_agent_force_kills_running_container.

Fix these issues in Kilo Cloud

Previous review (commit 05e86b8)

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 3
Issue Details (click to expand)

CRITICAL

(none)

WARNING

File Line Issue
tinyagentos/routes/agents.py 832 incus_unhealthy fallback force-kills ALL configured agents on any list_containers empty result, which includes transient incus errors, not just unhealthy incus
tinyagentos/routes/agents.py 935 stop_agent not containers fallback force-kills the agent on any list error, even when already stopped

SUGGESTION

File Line Issue
tinyagentos/routes/agents.py 830 Run-state gate includes Frozen/Freezing; freezing a frozen container via SIGKILL is likely unintended
tinyagentos/routes/agents.py 818 "120s _run cap" comment is inaccurate — no timeout wraps this route; asyncio.shield only blocks cancellation
tests/test_routes_agents.py 86 test_bulk_stop only exercises the empty-list fallback path, not the normal Running/Stopped gate
Files Reviewed (3 files)
  • tinyagentos/routes/agents.py - 4 issues
  • desktop/src/App.tsx - 0 issues
  • tests/test_routes_agents.py - 1 issue

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 37.1K · Output: 2.4K · Cached: 142.7K

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/test_routes_agents.py (1)

189-224: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the corresponding running-container test for stop_agent.

The single-agent handler has separate force-kill and response logic, but only its stopped branch is exercised. Verify that it calls force=True and returns force_killed: true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_routes_agents.py` around lines 189 - 224, Add a test for the
single-agent stop handler, alongside the existing stopped-branch coverage, using
a running ContainerInfo and monkeypatched list/stop operations. Invoke
stop_agent for that container, assert the stop call receives force=True, and
verify the response reports force_killed: true.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/src/App.tsx`:
- Around line 133-142: Update the bulk stop response handling around the success
notification and the `bulk_stop_agents` flow to inspect per-agent failures in
both `results` and `force_kill_results`, even when the HTTP response is
successful. Only dispatch the agents-changed event and show the “Agents stopped”
success notification when no individual stop failures are reported; otherwise
surface the failure outcome instead of claiming all agents stopped.

In `@tests/test_routes_agents.py`:
- Around line 143-147: Update the stopped-container assertion in the relevant
test to compare force_calls against the actual container name, including its
taos-agent prefix, rather than the partial "test-agent" value. Preserve the
assertion that the stopped container must not receive a force kill.

In `@tinyagentos/routes/agents.py`:
- Around line 833-851: The force-kill loop around stop_container is serial and
delays later agents when Incus is unhealthy. Add a per-agent async helper that
performs the existing force=True call and preserves each agent’s success,
output, warning, and exception handling, then launch all helpers concurrently
with asyncio.gather and map their results back into force_results.
- Around line 814-820: The _grace_kill task currently starts only after the
graceful stop completes, leaving no concurrent shielded SIGKILL path during a
long shutdown or cancellation. In both tinyagentos/routes/agents.py:814-820 and
tinyagentos/routes/agents.py:917-923, create/start _grace_kill before awaiting
_bulk_container_op(config, stop_container), then preserve the existing shielded
grace-and-force-kill behavior and task cleanup.

---

Nitpick comments:
In `@tests/test_routes_agents.py`:
- Around line 189-224: Add a test for the single-agent stop handler, alongside
the existing stopped-branch coverage, using a running ContainerInfo and
monkeypatched list/stop operations. Invoke stop_agent for that container, assert
the stop call receives force=True, and verify the response reports force_killed:
true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fde7133-1f9a-4cc5-8421-c298574e01f8

📥 Commits

Reviewing files that changed from the base of the PR and between 0e6e6fa and 05e86b8.

📒 Files selected for processing (3)
  • desktop/src/App.tsx
  • tests/test_routes_agents.py
  • tinyagentos/routes/agents.py

Comment thread desktop/src/App.tsx Outdated
Comment thread tests/test_routes_agents.py
Comment thread tinyagentos/routes/agents.py Outdated
Comment thread tinyagentos/routes/agents.py Outdated
@hognek

hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Bot-fix: Kilo + CodeRabbit findings addressed

Branch: fix/kill-switch-asyncio-shield on fork
Commit: bf56666a

Changes

  • asyncio.gather: Serial force-kills → concurrent in bulk_stop_agents
  • Concurrent _grace_kill: Start grace-kill task BEFORE awaiting graceful stop in both bulk and single-agent
  • Run-state gate: Removed Frozen/Freezing — only Running/Stopping trigger force-kill
  • Comment fix: Removed inaccurate "120s _run cap" (no route timeout exists)
  • Test assertion: Fixed "test-agent""taos-agent-test-agent" in stopped-container check
  • Test update: test_bulk_stop now exercises Running container path (not empty-list fallback)
  • New test: test_stop_agent_force_kills_running_container for single-agent running path
  • App.tsx: Parse bulk stop response — detect individual agent failures before reporting success

Test results

  • Targeted: 5/5 kill-switch tests pass
  • TypeScript: tsc --noEmit clean

Please merge or cherry-pick at your convenience.

jaylfc pushed a commit that referenced this pull request Jul 18, 2026
…ce window (#1962)

* feat(agents): add Ctrl+Shift+K kill-switch shortcut + SIGTERM→SIGKILL grace window

- Add Ctrl+Shift+K system shortcut in desktop SPA that triggers
  POST /api/agents/bulk/stop to immediately stop all running agents
- Enhance bulk_stop_agents and stop_agent backend routes with
  SIGTERM (incus stop) → 2s grace → SIGKILL (incus stop --force)
  to guarantee termination of runaway agent containers
- Desktop component AgentKillSwitch + TopBar integration already
  existed; this adds the keyboard shortcut and hardens the backend

Fixes #132

* fix(agents): shield sleep+force-kill sequence from cancellation

Wrap the entire 2-second grace window + SIGKILL sequence in
asyncio.shield() for both stop_agent and bulk_stop_agents.
Previously only stop_container calls were shielded; the preceding
await asyncio.sleep(2) was not, so a request cancelled during the
grace window would skip SIGKILL entirely.

* fix(agents): gate force-kill on container run-state, add bounded-hang comment, fix shield orphan

- Replace container_exists() with list_containers() status=='Running' check
  in both bulk_stop_agents and stop_agent force-kill paths.  container_exists()
  only resolves project membership — it returns True for Stopped containers,
  causing misleading force_killed:false reports after incus stop --force
  failures on already-dead containers.

- Add one-line comment noting that the sleep+force sequence inside
  asyncio.shield is bounded by the 120s _run cap.

- Fix asyncio.shield() orphan: create a Task, catch CancelledError, cancel
  the detached grace-kill task, and await its cleanup before re-raising.

- Add TestKillSwitchRunStateGate with 3 tests covering:
  * bulk_stop skips force-kill for Stopped containers
  * stop_agent skips force-kill for Stopped containers
  * bulk_stop still force-kills Running containers

* fix(agents): surface CSRF failure in kill-switch, fallback force-kill on incus-down, rename force_error->force_output

- desktop/App.tsx: check CSRF token before kill-switch POST, surface
  missing-token and server errors via notification store instead of
  silent catch{}; remove no-op ?. on withCsrf return.
- agents.py bulk_stop: when list_containers returns empty but agents
  are configured (incus unhealthy), attempt force-kill on all agents
  anyway instead of silently skipping SIGKILL.
- agents.py stop_agent: same incus-down fallback; rename force_error
  to force_output (it carries stdout even on success).

* fix(agents): surface CSRF failure in kill-switch, fallback force-kill on incus-down, rename force_error->force_output

- desktop/App.tsx: check CSRF token before kill-switch POST, surface
  missing-token and server errors via notification store instead of
  silent catch{}; use non-null assertion on withCsrf return (tsc).
- agents.py bulk_stop: when list_containers returns empty but agents
  are configured (incus unhealthy), attempt force-kill on all agents
  anyway instead of silently skipping SIGKILL.
- agents.py stop_agent: same incus-down fallback; rename force_error
  to force_output (it carries stdout even on success).

* fix(agents): remove task.cancel() that defeated asyncio.shield in kill-switch CancelledError handlers

Removed explicit task.cancel() calls from the CancelledError exception
handlers in both bulk_stop_agents (line ~858) and stop_agent (line ~955).
Calling task.cancel() on a shielded task in its CancelledError handler
aborts the shielded operation before it can send SIGKILL — defeating the
'shielded from cancellation' guarantee.

Now the handlers simply await the shielded task (which continues running
because asyncio.shield protects it), allowing the 2-second grace period
and SIGKILL to complete before re-raising the cancellation.

Closes Kilo WARNINGs on PR #1962.

* fix(tests): mock incus container ops in TestBulkOperations

Mock stop_container, list_containers, start_container, and restart_container
in TestBulkOperations to prevent FileNotFoundError when incus binary is not
available (e.g. GitHub Actions CI). The kill-switch changes added incus
list/incus stop calls to the bulk stop flow, which exposed the missing mocks.

Fixes CI test failures on PR #1988.

* fix(agents): address Kilo+CodeRabbit findings on kill-switch shield PR

- Use asyncio.gather for concurrent force-kills in bulk_stop_agents
- Start _grace_kill before await for graceful stop (concurrent SIGKILL path)
- Remove Frozen/Freezing from run-state gate (only Running/Stopping trigger force)
- Fix inaccurate "120s _run cap" comment (no route timeout exists)
- Fix test assertion: "test-agent" → "taos-agent-test-agent" in force_calls
- Update test_bulk_stop to exercise Running container path instead of empty-list
- Add test_stop_agent_force_kills_running_container for single-agent path
- App.tsx: parse bulk stop response to detect individual agent failures

Targeted tests: 5/5 pass. TypeScript tsc --noEmit: clean.

* fix(agents): broaden run-state gate to all non-Stopped and add 30s grace-kill timeout

- Run-state gate uses c.status not in ('Stopped',) instead of exact ('Running', 'Stopping')
  to catch containers in transitional states (Freezing, Frozen, etc.) per Kilo findings
- Wrap asyncio.shield(task) in asyncio.wait_for(..., timeout=30) with TimeoutError
  handling to prevent hung incus calls from pinning workers
- Shield-cancel bug already fixed in prior commits (no task.cancel() remaining)

---------

Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
hognek added 7 commits July 20, 2026 02:39
… grace window

- Add Ctrl+Shift+K system shortcut in desktop SPA that triggers
  POST /api/agents/bulk/stop to immediately stop all running agents
- Enhance bulk_stop_agents and stop_agent backend routes with
  SIGTERM (incus stop) → 2s grace → SIGKILL (incus stop --force)
  to guarantee termination of runaway agent containers
- Desktop component AgentKillSwitch + TopBar integration already
  existed; this adds the keyboard shortcut and hardens the backend

Fixes jaylfc#132
Wrap the entire 2-second grace window + SIGKILL sequence in
asyncio.shield() for both stop_agent and bulk_stop_agents.
Previously only stop_container calls were shielded; the preceding
await asyncio.sleep(2) was not, so a request cancelled during the
grace window would skip SIGKILL entirely.
… comment, fix shield orphan

- Replace container_exists() with list_containers() status=='Running' check
  in both bulk_stop_agents and stop_agent force-kill paths.  container_exists()
  only resolves project membership — it returns True for Stopped containers,
  causing misleading force_killed:false reports after incus stop --force
  failures on already-dead containers.

- Add one-line comment noting that the sleep+force sequence inside
  asyncio.shield is bounded by the 120s _run cap.

- Fix asyncio.shield() orphan: create a Task, catch CancelledError, cancel
  the detached grace-kill task, and await its cleanup before re-raising.

- Add TestKillSwitchRunStateGate with 3 tests covering:
  * bulk_stop skips force-kill for Stopped containers
  * stop_agent skips force-kill for Stopped containers
  * bulk_stop still force-kills Running containers
… on incus-down, rename force_error->force_output

- desktop/App.tsx: check CSRF token before kill-switch POST, surface
  missing-token and server errors via notification store instead of
  silent catch{}; remove no-op ?. on withCsrf return.
- agents.py bulk_stop: when list_containers returns empty but agents
  are configured (incus unhealthy), attempt force-kill on all agents
  anyway instead of silently skipping SIGKILL.
- agents.py stop_agent: same incus-down fallback; rename force_error
  to force_output (it carries stdout even on success).
… on incus-down, rename force_error->force_output

- desktop/App.tsx: check CSRF token before kill-switch POST, surface
  missing-token and server errors via notification store instead of
  silent catch{}; use non-null assertion on withCsrf return (tsc).
- agents.py bulk_stop: when list_containers returns empty but agents
  are configured (incus unhealthy), attempt force-kill on all agents
  anyway instead of silently skipping SIGKILL.
- agents.py stop_agent: same incus-down fallback; rename force_error
  to force_output (it carries stdout even on success).
…l-switch CancelledError handlers

Removed explicit task.cancel() calls from the CancelledError exception
handlers in both bulk_stop_agents (line ~858) and stop_agent (line ~955).
Calling task.cancel() on a shielded task in its CancelledError handler
aborts the shielded operation before it can send SIGKILL — defeating the
'shielded from cancellation' guarantee.

Now the handlers simply await the shielded task (which continues running
because asyncio.shield protects it), allowing the 2-second grace period
and SIGKILL to complete before re-raising the cancellation.

Closes Kilo WARNINGs on PR jaylfc#1962.
@hognek
hognek force-pushed the feat/kilo-shield-fix branch from 05e86b8 to 8b897d5 Compare July 20, 2026 00:51
…c#1988)

- Start _grace_kill before graceful stop so grace window overlaps
  with container shutdown, not after (CodeRabbit)
- Use asyncio.gather for concurrent force-kills instead of serial
  loop in bulk_stop_agents (CodeRabbit)
- Remove Frozen/Freezing from run-state gate; only Running/Stopping
  trigger force-kill (Kilo SUGGESTION)
- Fix inaccurate '120s _run cap' comment — no route-level timeout
  exists, only asyncio.shield (Kilo SUGGESTION)
- Wrap list_containers in try/except to skip force-kill on transient
  incus errors, not on empty results (Kilo WARNING)
- Parse per-agent failures in desktop kill-switch response before
  reporting success (CodeRabbit)
- Fix test assertion: partial container name → full (CodeRabbit)
- Add test_stop_agent_force_kills_running_container (CodeRabbit)
- TypeScript: add Record<string,any> cast for filter callbacks
task = asyncio.create_task(_grace_kill())
grace_task = asyncio.create_task(_grace_kill())

# Phase 2: SIGTERM every agent container

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: grace_task is created before the graceful stop await; if _bulk_container_op raises, grace_task is never awaited and becomes an orphaned background task.

Here grace_task (the shielded force-kill) is scheduled first, then results = await _bulk_container_op(config, stop_container) runs. If that await raises (e.g. incus error), execution never reaches await asyncio.shield(grace_task), so the task is orphaned: it keeps running detached, later force-kills containers outside the request scope, and can surface "Task exception was never retrieved" / event-loop warnings. Also, because grace_task only sleeps 2s before force-killing while the graceful stop may still be in flight, the two incus stop / incus stop --force calls can race on the same container.

Consider awaiting/shielding grace_task before (or independent of) the graceful stop, and starting the graceful stop before scheduling the force-kill so the operations don't overlap.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/routes/agents.py Outdated
task = asyncio.create_task(_grace_kill())
grace_task = asyncio.create_task(_grace_kill())

stop_result = await stop_container(container_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Same orphaned-task risk as the bulk path: grace_task is created before stop_result = await stop_container(container_name), so if the graceful stop raises, grace_task is never awaited and runs detached.

If await stop_container(container_name) (the graceful SIGTERM) raises, this route returns the error but grace_task is left orphaned — it will still sleep 2s and then call stop_container(container_name, force=True) on the same container outside the request, and may emit "Task exception was never retrieved" warnings. It also means the force-kill can race with a still-in-flight graceful stop on the same container.

Await/shield grace_task regardless of whether the graceful stop succeeds, or start the graceful stop before scheduling the force-kill.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Wrap the graceful stop await in bulk_stop_agents and stop_agent in a
try/except that cancels the grace_task on failure. If _bulk_container_op
or stop_container raises, the grace_task was previously orphaned — it
would fire a SIGKILL after 2s detached from the handler lifecycle.

Addresses Kilo SUGGESTIONs from jaylfc#1988 review cycle.
@jaylfc
jaylfc merged commit c384ee3 into jaylfc:dev Jul 20, 2026
9 checks passed
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