Skip to content

fix: don't discard a finished task result when cancel races completion - #167

Merged
PleasePrompto merged 1 commit into
PleasePrompto:mainfrom
ryuhaneul:fix/task-cancel-observability
Jul 12, 2026
Merged

fix: don't discard a finished task result when cancel races completion#167
PleasePrompto merged 1 commit into
PleasePrompto:mainfrom
ryuhaneul:fix/task-cancel-observability

Conversation

@ryuhaneul

Copy link
Copy Markdown
Contributor

Problem

  1. Completion race loses finished results. TaskHub._run() obtains
    the CLI response, writes terminal status done, then awaits result
    delivery. A TaskHub.cancel() arriving in that window lands
    CancelledError at the delivery await: the handler overwrites the
    terminal status from done to cancelled and delivers only the
    TASKMEMORY excerpt — silently discarding the completed response the
    subprocess had already produced (and billed). From the user's side the
    task "finished into nothing".

  2. Cancellations leave no trace. Neither TaskHub.cancel(),
    cancel_all(), nor the internal API cancel endpoint logged anything,
    so after the fact there is no way to tell who cancelled a task or when.

Fix

  • Run the final delivery as an explicit task, keep a strong reference in
    a hub-level _pending_deliveries set (dropped via done-callback), and
    await it through asyncio.shield(). The CancelledError handler
    becomes a no-op once final delivery has started: the terminal done
    status stands and the finished result is delivered exactly once.
    Mid-run cancellation is unchanged (status cancelled, TASKMEMORY
    partial delivered). Shutdown drains any pending deliveries.
  • Log the cancel paths: accepted / non-running in cancel(), a count in
    cancel_all(), and the internal API cancel including the requesting
    agent.

Semantics note: the cancel API's success=true means the request was
accepted; when completion wins the race the task's final status remains
done and the result is delivered.

Tests

Six scenarios: late cancel via cancel() preserves the result (delivered
once, status done, no partial cancelled delivery); same via
cancel_all(); mid-run cancel still delivers the TASKMEMORY partial with
status cancelled; the four cancel log flows (caplog); shutdown drains a
blocked pending delivery; API cancel logs the requesting agent.

4 files changed, +253/−17. Rollback = revert (no config/schema impact).

Note for merging alongside #164: touches tasks/hub.py in hunks
adjacent to #164 (per-session reasoning effort). Independent
single-commit PRs off current main; either merge order yields at most
trivial context conflicts.

TaskHub.cancel() arriving between CLI completion and result delivery landed CancelledError at the delivery await: the handler then overwrote the task's terminal status from "done" to "cancelled" and delivered only the TASKMEMORY excerpt, silently discarding the completed response the subprocess had already produced (and billed).

Shield the final delivery and make the cancellation handler a no-op once delivery has started, so a late cancel preserves the finished result. Also log task cancellations (hub and internal API, including the requesting agent) — previously cancels left no trace in the log, making post-hoc diagnosis impossible.
@ryuhaneul
ryuhaneul force-pushed the fix/task-cancel-observability branch from 614a42e to 700780b Compare July 9, 2026 01:08
@ryuhaneul
ryuhaneul marked this pull request as ready for review July 9, 2026 02:22
@PleasePrompto
PleasePrompto merged commit ccb5b63 into PleasePrompto:main Jul 12, 2026
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