Skip to content

runtime/session: suspend actors after abandoned delivery - #3635

Open
shashankram wants to merge 1 commit into
kagent-dev:mainfrom
shashankram:suspend-turnless
Open

shashankram wants to merge 1 commit into
kagent-dev:mainfrom
shashankram:suspend-turnless

Conversation

@shashankram

Copy link
Copy Markdown
Contributor

Description

Canceling a message while its actor finishes restoring can leave the actor RUNNING without a turn, holding worker capacity indefinitely. The existing Session lifecycle worker now suspends that actor and keeps the Session READY for the next message.

  • Retain expired dispatch IDs so abandoned delivery remains discoverable without a task event.
  • Claim suspension only for a RUNNING actor with no active turn, newer delivery, or competing lifecycle work. New delivery can supersede unclaimed cleanup; once claimed, execution waits until the snapshot is recorded.
  • Reuse the existing worker and dispatch fields, adding one database field for suspension ownership. Preserve history and snapshots so subsequent messages resume normally.

Uncertain suspension outcomes retain the claim, consistent with existing automatic task suspension.

Cancellation cleanup flow
sequenceDiagram
    participant G as Gateway
    participant D as Session store
    participant W as Lifecycle worker
    participant S as Substrate
    G->>D: Expire delivery and retain dispatch ID
    G->>W: Wake existing worker
    W->>D: Find expired dispatch
    W->>S: Read actor identity and state
    alt Actor is RUNNING
        W->>D: Claim if dispatch matches and no active turn
        alt Claim succeeds
            W->>S: Suspend actor and save snapshot
            S-->>W: Confirm snapshot and suspension
            W->>D: Record snapshot and clear dispatch and claim
        end
    end
Loading

Testing

  • Added persistence and service coverage for canceled and expired delivery, newer-message races, checkpoint and lifecycle coordination, and retry behavior.
  • Added an E2E regression that cancels after RUNNING but before task acceptance, checks suspension and released worker capacity with empty task history, then verifies a subsequent message completes.

Change Type

/kind fix

Changelog

Actors left running after canceled message delivery now suspend and release worker capacity automatically.

\# Description

Canceled delivery can leave a restored actor running without a turn,
holding worker capacity indefinitely.

- Retain expired dispatch attempts for the existing lifecycle worker to process.
- Claim suspension only when no active turn or newer delivery owns the Session.
- Preserve snapshots and logical readiness so subsequent messages resume normally.
- Cover cancellation, competing lifecycle work, retries, and subsequent delivery.

\# Change Type

```
/kind fix
```

\# Changelog

```release-note
Actors left running after canceled message delivery now suspend and release worker capacity automatically.
```

\# Additional Notes

Signed-off-by: Shashank Ram <shashank.ram@solo.io>

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

🤖 AI-generated review, guided and reviewed by me before posting.

I focused on the migration and on rollbacks within 1.0. I found one rollback issue and have one restart question. Both also apply to #3649. The store and migration tests pass locally at d30cb0b.

-- +goose Up
ALTER TABLE session
ADD COLUMN dispatch_cleanup_executor_id UUID,
ADD CONSTRAINT session_dispatch_cleanup_claim CHECK (

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.

This CHECK makes a rollback within 1.0 unsafe. A rollback keeps this schema, because the runner accepts a newer database. The older code clears dispatch_id on the first task save, on revoke, on deletion and on crash recovery. If the row has a claim, each of these writes fails the CHECK. Then the session cannot start a turn, and each delete request fails.

A controller that stops during cleanup leaves a claim. So the rollback itself can cause this problem. The Down migration also refuses to run while these rows exist. I reproduced the problem at d30cb0b (SQLSTATE 23514).

Two options:

  1. Drop the CHECK. Also make each query that checks for a claim require dispatch_id IS NOT NULL. I prefer this option.
  2. Keep the CHECK. Document that a rollback needs zero claimed rows, and document how to clear them.

}
if err != nil {
logging.FromContext(ctx).ErrorContext(ctx, "abandoned dispatch suspension outcome unknown", "session_id", work.Session.Id, "dispatch_id", work.DispatchID, "error", err)
return nil // A possibly issued call cannot safely release admission.

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.

What happens to this claim after a restart? The claim stays after a shutdown or after any error other than Aborted. GetPendingSessionDispatchCleanup skips claimed rows, so no worker gets the claim again. Each new message then gets ErrDispatchBusy until crash recovery or force deletion.

Task quiescence has the same limit today. But this path runs on every canceled delivery. So each rollout can strand a claim.

Could a new owner adopt the claim and only send the suspend again? If not, please document how to clear a stuck session.

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