60db Logo
  • Engineering

Cancel model work without breaking other requests in a batch

Cancel batched model work per request. Test ownership, terminal state, late output, and backend cleanup without stopping sibling calls.

Editorial date
Reading time
25 min
One request rail stops inside a shared execution frame while independent sibling rails continue to their outputs

TL;DR: Cancel the conversation-owned request, invalidate its output immediately, and ask the backend to release only the resources that request owns. Shared execution can continue for sibling requests, while terminal state and a final post-wait admission check prevent late results from restoring canceled speech.


One caller interrupts. The application cancels a shared model task, and unrelated callers lose their responses because several requests happened to be executed together.

Another application avoids that outage by leaving the batch running, but forwards the canceled caller's late tokens into speech. Both failures come from giving a scheduling group the authority that belongs to an individual request.

Start by separating request ownership from shared execution. The interruption safety guide defines which response may speak; this guide adds the shared-runtime boundary and an executable local ownership model.

The examples use synthetic identities and public interfaces. They can support evaluation of an application using 60db Agent, but they do not describe a private batching algorithm, serving topology, or measured product cancellation time.

Public runtimes also make the boundary visible. The Triton cancellation guide distinguishes cancellation detection in the server from backend handling after execution has started, so a canceled client future cannot by itself prove that compute stopped.

Table of contents

  1. Give each request its own owner
  2. Separate cancellation evidence levels
  3. Preserve terminal state and revisions
  4. Run a local ownership reference
  5. Check admission after every wait
  6. Cancel through the public runtime
  7. Prove sibling isolation
  8. Diagnose retained work and late output
  9. Compare cancellation policies
  10. Frequently asked questions
  11. Make cancellation a tested contract

Give each request its own owner

A batch is a scheduling arrangement. It can combine work to improve resource use, but it does not make every request share the same conversation lifecycle, authorization, or reason to continue.

Assign a request identity when the application admits the work. Associate that identity with the owning conversation generation and retain the mapping to the public runtime handle that can cancel the request.

A generation distinguishes a current response from work superseded by an interruption or repair. A request identity distinguishes one backend operation from another within that generation, and the two identifiers should not be casually substituted for each other.

The spoken self-correction guide explains why a caller can change a value while the system is working. That change can invalidate one request without ending the entire call or every operation associated with it.

Treat the batch handle as runtime-owned unless the public API says otherwise. An application should not close a shared client, stop a shared scheduler, or terminate a worker process to cancel one conversation's output.

Keep connection ownership explicit. A connection dedicated to one request may be safely closed under its adapter contract; a shared streaming connection can carry several requests, so closing it may cancel siblings or destroy routing state.

The Triton client documentation distinguishes per-request cancellation from stopping a stream with pending requests. That public distinction is enough to justify an adapter test for scope without assuming anything about the runtime's internal scheduler.

Do not reuse request identities while delayed callbacks remain possible. Reassigning an old identity to a new operation makes a late cancellation or completion event ambiguous even if the current generation counter is correct.

A small owner record should contain the application request identity, generation, current disposition, runtime handle reference, and safe correlation evidence. It should not expose raw prompts or customer task identifiers in normal metrics.

The observability guide provides the related tracing discipline. A request correlation record is useful when it connects named boundaries; a large payload dump is not a substitute for ownership.

Ownership also determines cleanup. The request can release its local buffers and invalidate its queued speech while a shared batch continues using memory and execution resources that belong to other requests.

Avoid reference-counting abstractions unless the public adapter requires them. A direct mapping from one application request to one supported runtime cancellation handle is easier to inspect than a generic group controller that guesses which resources are shared.

One conversation invalidates its own output and sends a per-request cancellation while shared execution continues for siblings

The conceptual figure separates immediate local invalidation from backend cleanup. Neither arrow gives the canceled request authority over another request's lifecycle.

Separate cancellation evidence levels

A cancellation intent means the controller decided that a result is no longer useful. A local terminal transition means the application will no longer admit that request's output, even if a backend callback arrives later.

A cancel call accepted by the client library is another boundary. It establishes that the application invoked the supported cancellation mechanism, not that a remote worker observed it or stopped executing immediately.

A backend acknowledgment provides stronger evidence about the remote request disposition. Resource release and disappearance from active accounting can be later events, especially when execution is cooperative or distributed across services.

The gRPC cancellation guide explains that application code needs to respond to cancellation while work is executing. This supports a conservative rule: invalidate local output immediately and measure remote cleanup separately.

Do not wait for remote cleanup before suppressing stale speech. The caller's interruption changes who owns the output now, while the runtime may need time to reach a safe stopping point.

Do not call suppressed output "compute canceled" either. A system can correctly discard late tokens while continuing to spend resources on work that no longer has a consumer.

Use distinct timestamps for intent, local invalidation, cancel submission, cancel observation when available, final callback, and resource release evidence. Missing remote visibility remains unknown rather than a zero-duration cancellation.

Keep the clock domain of each timestamp. A local intent-to-submit duration can use one monotonic clock, while a remote cleanup observation needs an explicit tracing or synchronization model before subtraction.

The latency measurement guide applies here too. A cancellation latency claim needs endpoints, a workload, and outcome counts before it can support a comparison.

Deadlines add another reason to stop. The gRPC deadline guide describes deadline propagation, but an application still needs to stop its own useless work and preserve the expired disposition when a late result arrives.

Distinguish expiry from user cancellation in the ledger. They can share output suppression behavior while producing different recovery actions, operational alerts, and explanations to the caller.

The waiting speech guide helps preserve that distinction in conversation. A timeout can justify a truthful fallback; it cannot justify speaking an obsolete result as if the request were still active.

Preserve terminal state and revisions

A terminal request stays terminal. Completed, canceled, and expired are outcomes of that request identity, and delayed evidence must not transition any of them back to running.

A new attempt needs a new identity or an explicitly modeled attempt number. Reopening the same record erases the meaning of late callbacks and makes exact retry behavior difficult to explain.

A revision identifies the current payload within an active request. Strictly older deliveries should be classified as stale before testing whether the record has finalized, because an old harmless duplicate is different from a changed payload trying to revise final evidence.

Exact duplicates can return a historical acknowledgment while preserving the current terminal disposition. That distinction prevents a client retry from restoring output ownership merely because the same payload was accepted earlier.

The partial transcript revision guide uses the same ordering principle. Identity, revision ordering, and finalization each answer a separate question and need a deliberate comparison order.

An equal revision with different bytes is a conflict. A greater revision after finalization is also a conflict under the reference contract, while a strictly lower revision is stale even if the current record is final.

Own payload snapshots when retaining evidence. If a caller can mutate a dictionary after approval, the request can appear to have the same identity and revision while its meaning changes underneath the controller.

For the narrow reference below, payloads are immutable bytes. A real JSON proposal needs a copied and frozen supported value shape, or an immutable canonical serialization whose schema is validated before capture.

The cache-key guide provides that snapshot pattern. A frozen outer container is insufficient if it still points to a mutable caller-owned child.

Keep terminal evidence separate from business success. A model request can complete while a tool fails, or a response can finish locally while the caller never receives its audio.

The outcome metrics guide explains those denominators. Cancellation tests should report request dispositions without quietly treating a completed model stream as a successful conversation.

Run a local ownership reference

This reference models local ownership and admission for separate requests. It deliberately does not emulate a real model server or claim that a Python task cancellation stops a GPU operation.

The asynchronous wait represents an external clear barrier or writer wait. After it returns, the function rechecks the owning request, generation, terminal state, barrier epoch, and age before its synchronous local append.

Run the complete fence with Python 3.11 or later as python batch_cancel_reference.py. All identities, timestamps, and payloads in the fixture are synthetic.

import asyncio
from dataclasses import dataclass, replace, FrozenInstanceError
import math


@dataclass(frozen=True, slots=True)
class Request:
    generation: int
    state: str = "running"
    revision: int = -1
    payload: bytes = b""
    final: bool = False


class Owners:
    def __init__(self):
        self.rows = {}
        self.barrier_epoch = 0
        self.clear = True
        self.sent = []

    def start(self, identity, generation):
        if (type(identity) is not str or not identity or identity in self.rows or
                type(generation) is not int or generation < 0):
            raise ValueError("invalid or reused request identity")
        self.rows[identity] = Request(generation)

    def finish(self, identity, disposition):
        if disposition not in ("completed", "cancelled", "expired"):
            raise ValueError("invalid terminal disposition")
        row = self.rows[identity]
        if row.state == "running":
            self.rows[identity] = replace(row, state=disposition)
        return self.rows[identity].state

    def update(self, identity, revision, payload, final=False):
        if (type(revision) is not int or revision < 0 or
                type(payload) is not bytes or type(final) is not bool):
            raise ValueError("invalid output evidence")
        row = self.rows[identity]
        if revision < row.revision:
            return "stale"
        if revision == row.revision:
            return "duplicate" if (payload, final) == (row.payload, row.final) else "conflict"
        if row.state != "running" or row.final:
            return "conflict"
        self.rows[identity] = replace(row, revision=revision,
                                     payload=payload, final=final)
        return "accepted"

    async def admit(self, identity, generation, born, now, max_age, wait):
        if (type(born) not in (int, float) or not math.isfinite(born) or
                type(max_age) not in (int, float) or
                not math.isfinite(max_age) or max_age < 0):
            raise ValueError("invalid age inputs")
        epoch = self.barrier_epoch
        await wait()
        row = self.rows[identity]
        if row.generation != generation:
            return "stale"
        if row.state != "running":
            return row.state
        current = now()
        if type(current) not in (int, float) or not math.isfinite(current) or current < born:
            raise ValueError("invalid local clock")
        if not self.clear or epoch != self.barrier_epoch:
            return "blocked"
        if current - born > max_age:
            self.finish(identity, "expired")
            return "expired"
        if row.revision < 0:
            return "empty"
        self.sent.append((identity, generation, row.payload))
        return "submitted"


async def noop():
    pass


def rejected(action):
    try:
        action()
    except (ValueError, FrozenInstanceError):
        return
    raise AssertionError("invalid operation accepted")


async def tests():
    owners = Owners()
    owners.start("a", 1)
    owners.start("b", 1)
    assert owners.update("a", 2, b"old answer", True) == "accepted"
    assert owners.update("a", 1, b"older") == "stale"
    assert owners.update("a", 2, b"changed", True) == "conflict"
    assert owners.update("a", 3, b"later") == "conflict"
    assert owners.finish("a", "cancelled") == "cancelled"
    assert owners.finish("a", "completed") == "cancelled"
    assert owners.update("a", 2, b"old answer", True) == "duplicate"
    assert owners.rows["a"].state == "cancelled"
    assert owners.update("a", 1, b"older") == "stale"
    rejected(lambda: setattr(owners.rows["a"], "state", "running"))
    assert await owners.admit("a", 1, 0, lambda: 0, 1, noop) == "cancelled"
    assert owners.update("b", 0, b"sibling") == "accepted"
    assert await owners.admit("b", 1, 0, lambda: 1, 1, noop) == "submitted"
    assert owners.sent == [("b", 1, b"sibling")]
    assert owners.finish("b", "completed") == "completed"
    assert owners.finish("b", "cancelled") == "completed"
    rejected(lambda: owners.start("b", 2))
    rejected(lambda: owners.start("invalid", True))
    rejected(lambda: owners.update("b", True, b"x"))
    rejected(lambda: owners.update("b", 1, bytearray(b"x")))
    owners.start("c", 2)
    owners.update("c", 0, b"current")
    assert await owners.admit("c", 1, 0, lambda: 99, 1, noop) == "stale"
    assert owners.rows["c"].state == "running"
    async def cancel_during_wait():
        owners.finish("c", "cancelled")
    assert await owners.admit("c", 2, 0, lambda: 0, 1, cancel_during_wait) == "cancelled"
    owners.start("d", 1)
    owners.update("d", 0, b"held")
    clock = [0]
    async def age_during_wait():
        clock[0] = 2
    assert await owners.admit("d", 1, 0, lambda: clock[0], 1, age_during_wait) == "expired"
    assert owners.finish("d", "completed") == "expired"
    owners.start("e", 1)
    owners.update("e", 0, b"barrier")
    async def changed_barrier():
        owners.barrier_epoch += 1
    assert await owners.admit("e", 1, 0, lambda: 0, 1, changed_barrier) == "blocked"
    owners.clear = False
    assert await owners.admit("e", 1, 0, lambda: 0, 1, noop) == "blocked"
    owners.clear = True
    owners.start("empty", 1)
    assert await owners.admit("empty", 1, 0, lambda: 0, 1, noop) == "empty"
    assert len(owners.sent) == 1
    print("per-request ownership and post-wait fixtures passed")


asyncio.run(tests())

The reference deliberately retains the latest evidence after cancellation. That makes an exact retry answerable without granting it new authority, while immutable byte payloads keep the historical comparison stable even if the caller still holds its original input buffer elsewhere.

The function validates evidence before comparing revisions. Invalid types are rejected rather than becoming stale by accident, so a boolean revision cannot pass as an integer and an unsupported mutable byte buffer cannot enter the owner record.

A finalized output and a terminal request are separate state dimensions. Final output can still be awaiting local admission when cancellation arrives, and completed state should be recorded only at the application's chosen completion boundary rather than inferred from the model's final token flag.

The registry is owned by one event-loop controller. Calls that mutate it run synchronously without intervening waits, and adapters must route external events into that owner rather than writing records from arbitrary threads.

The local append is the submission boundary in this reference. A real asynchronous writer introduces another wait, so it needs an adapter-level admission mechanism or a repeated ownership check immediately before ordered submission.

The public rows dictionary exists to keep the example inspectable, not as an invitation for application callers to mutate it. Production code should keep the owner store private and expose snapshots and operations through the surrounding project's existing pattern.

The terminal and revision tests cover different guarantees. Exact historical duplicates do not change cancellation, stale revisions are classified before final conflicts, and one request's cancellation leaves the sibling's output eligible.

Python's asyncio task documentation describes task cancellation and structured concurrency behavior. Use that documentation for coroutine lifecycle, while keeping remote inference cancellation an explicit adapter operation with its own evidence.

Check admission after every wait

A request can become obsolete while waiting for a cache read, a network writer, a clear acknowledgment, or a capacity slot. A pre-wait ownership check says nothing about changes that occur during that wait.

Perform the final generation and terminal checks after the wait. Check obsolete generation before applying expiry side effects so an old continuation cannot expire the newer work that now owns the conversation.

The reference checks the barrier epoch too. If the output-clear boundary changed during the wait, the held candidate is blocked even when the barrier currently appears clear, because the earlier wait did not establish the new barrier's condition.

An adapter can respond by waiting for the current barrier and retrying admission under a bounded policy. It must not silently treat a stale barrier token as proof that playback is cleared.

The audio backpressure guide explains another held-item case. A chunk can expire after it leaves the queue, so checking only queued items leaves the consumer holding stale speech that bypasses the queue's age limit.

Age uses a local clock and a declared creation boundary. The reference allows an age equal to the limit and expires only when age is greater, which makes the equality fixture part of the public contract.

A different equality rule is possible, but code, diagrams, and tests must agree. Avoid vague prose such as "roughly too old" when the implementation makes a deterministic admission decision.

Keep empty distinct from expired. Empty means there is no candidate payload under this local state; expired means the request crossed its age budget and moved to a terminal disposition.

Keep blocked distinct from canceled. A barrier mismatch can be retried after the correct barrier completes, while canceled output has permanently lost admission for that request identity.

The chunk sizing guide adds the transport boundary. Smaller chunks can make cancellation opportunities more frequent, but chunk size does not replace ownership validation or prove immediate acoustic silence.

Check the actual submission interface. If a method returns after buffering internally, its acceptance is still earlier than endpoint playback, and the adapter's clear semantics determine how much queued material can remain downstream.

The playback evidence guide explains how to report that uncertainty. Local suppression is a useful guarantee even when the system lacks a direct observation of sound at the caller's device.

Cancel through the public runtime

Inventory the supported cancellation unit before writing an adapter. It may be a request handle, stream, sequence, connection, or entire engine, and only some units are safe for a single conversation's cancellation.

Use the narrowest documented unit that matches ownership. If the API exposes only a shared stream stop, isolate streams by ownership or choose another supported route instead of pretending the broad operation is per-request.

Triton's Python backend documentation describes per-request cancellation checks and backend responsibilities. This is useful public evidence for a test protocol, while the exact runtime version and chosen backend still determine what your deployment supports.

Record that version in the adapter report. API shape, cancellation detection points, and backend support can change, so a passing fixture against one version is not a standing guarantee for every future release.

Do not reach into private scheduler state to remove a batch member. Besides creating a fragile dependency, a local removal can leave accounting, memory ownership, and final callbacks inconsistent with the runtime's supported lifecycle.

Use public metrics and request outcomes to observe cleanup. The Triton metrics documentation distinguishes requests, inferences, executions, and failure reasons; those units should remain separate in a cancellation report.

A request count is not a batch execution count. If one canceled request was part of an execution that still served siblings, the shared execution can remain valid while the request's output is suppressed.

Distributed serving can add another cleanup owner. A public vLLM issue about cancellation and retained prefill state reports a specific failure mode involving a disconnected decode request and retained resources elsewhere.

Treat that issue as a community-reported scenario to test, not proof that every current release has the same defect. The generic lesson is to retain the public request mapping needed to observe cleanup across each documented boundary.

If the runtime cannot promptly stop compute, keep local cancellation correct and report the resource limitation. A truthful adapter can say "output suppressed, cleanup pending" without inventing an acknowledgment or stopping unrelated requests.

Prove sibling isolation

Construct a small cohort with one target request and several siblings under the same public client and runtime configuration. Use harmless synthetic inputs and an environment where you are authorized to create the workload.

First run the cohort without cancellation. Record each request's output routing, disposition, timing boundary, and safe identity so that the cancellation run has a reference for unrelated request behavior.

Then cancel only the target at several stages: before admission, while queued, during execution, after final output evidence, and after local completion. Each stage tests a different race and should have its own expected disposition.

A post-completion cancel should preserve completed state under the declared contract. A cancel during execution should suppress the target's later output while leaving siblings routed to their own consumers.

Synthetic tests compare target cancellation with sibling completion, late-output suppression, terminal retries, and barrier changes

Check routing byte-for-byte for the synthetic payloads. A sibling that completes with another request's output is not isolated merely because the connection stayed open and the runtime reported success.

Exercise the shared connection's behavior deliberately. If closing the target's stream can close siblings, the test should reveal it before the adapter uses that action as a general cancellation path.

The transport comparison guide is useful when the same application carries model and media streams over different interfaces. Cancellation authority should follow the operation owner across those interfaces rather than following whichever socket is easiest to close.

Use a fixture matrix rather than one "cancel works" test. The matrix should name the target stage, public operation invoked, expected target disposition, expected sibling disposition, expected local output, and the cleanup evidence that the runtime exposes.

For the queued case, arrange enough harmless work to keep the target waiting, then cancel only its handle. Verify that a later free slot serves another eligible request and that the canceled target does not reappear merely because the queue was drained.

For the executing case, choose a bounded synthetic input that keeps the operation active long enough to issue the cancel. Record the request handle before cancellation, and avoid creating timing assumptions based only on an arbitrary sleep that may miss the intended stage on a faster or slower machine.

Use an observed stage event where the public API provides one. Where it does not, report the cancellation window as approximate and repeat the experiment, while keeping the strong claim limited to the dispositions and outputs that the harness observed.

For the final-output case, retain the final payload and issue an exact duplicate after cancellation. The duplicate classification should remain historical evidence, while a new greater revision must fail under the finalized-record rule and a lower revision must remain stale.

Test output routing with distinct synthetic payload markers that contain no customer data. The markers let the harness prove which consumer received each payload without relying on model wording or collecting a realistic private conversation.

Repeat with a slow sibling and a fast target. This catches group cleanup that assumes the target is the last active consumer, while a reversed case catches delayed target callbacks arriving after sibling completion.

Keep admission limits constant across runs. Changing concurrency at the same time as cancellation can alter batching and obscure whether a sibling failure came from cancellation scope or overload.

The admission control guide provides the companion overload contract. A sibling rejected before admission should be reported as rejected, not counted as a cancellation-induced runtime failure.

Diagnose retained work and late output

Start with the local disposition. If a canceled request still reaches the speech writer, inspect generation routing and the final post-wait check before investigating remote compute cleanup.

If local output is suppressed but resources remain active, inspect whether the supported cancel operation was submitted and whether the runtime exposes an observation or release event. Those are different failures with different owners.

If siblings fail at the same instant, inspect the scope of the control action. Shared stream closure, client shutdown, process termination, and engine-wide abort can explain a group failure without any defect in per-request output filtering.

A diagnostic tree separates local stale-output bugs, missing cancel submission, pending backend cleanup, and broad shared control actions

Retain the request mapping until cleanup can be reconciled. Deleting it immediately after local invalidation can make a late acknowledgment impossible to join, and retrying a broad cancel without that mapping can affect the wrong work.

Bound retention using the runtime's documented lifecycle and the operational policy. Keep tombstones or safe terminal summaries long enough to reject late evidence, while avoiding indefinite retention of payloads that no longer serve a diagnostic purpose.

The shutdown guide explains why terminal records matter during draining. Closing admission should not erase the evidence needed to finish or cancel active requests safely.

Retained work needs a unit. A request handle count, pending output bytes, backend active-request count, and shared execution count describe different resources, so give each time series its original unit and identity scope before comparing them.

Preserve original cumulative counters when deriving deltas. A decrease can indicate a reset or a changed receiver, and subtracting across that boundary can create a negative cleanup rate or an apparently impossible improvement; retain the receiver identity and raw counter values so the report can detect that case.

Zero remains different from unavailable. A runtime that reports no active target requests supplies different evidence from an adapter that cannot read active-request accounting, and the report should not fill the latter cell with zero to make a cleanup chart look complete.

A shared execution can remain active because a sibling still needs it. That is expected under per-request cancellation, so the cleanup criterion should identify target-owned resources rather than requiring every shared resource to disappear immediately after the target stops.

Measure cleanup under cancellation storms. A single canceled request may release promptly while a burst of canceled work exposes delayed cleanup, queue contention, or an unbounded local tombstone store.

The bursty load guide supplies the workload discipline. Report scheduled, started, canceled, completed, and cleanup-pending counts rather than presenting only the requests that reached a successful output endpoint.

Compare cancellation policies

Immediate local invalidation is required when a request loses output ownership. Remote stopping policy can differ according to the public runtime's support and the cost of allowing work to finish.

A finish-and-discard policy can preserve sibling execution when per-request stopping is unavailable. It consumes resources after cancellation, so bound the resulting workload and report that limitation rather than calling it full cancellation.

A cooperative per-request cancel can release work at supported checkpoints. Its cleanup delay is workload-dependent, and a long running backend operation can still need time before it observes the request.

A group abort is appropriate only when the whole group shares the same owner or a deliberate wider recovery operation requires it. The convenience of one group handle is insufficient reason to give a single caller that authority.

Checks in this table mean that the policy directly preserves the named behavior under its stated scope. Crosses mean it does not, and remote release remains dependent on the runtime's documented support.

Criterion Finish and discard Per-request cancel Shared group abort
Suppresses target output with local gate ✓ ✓ ✓
Preserves independent sibling execution ✓ ✓ when supported ✗
Requests target compute release ✗ ✓ ✓ with broader scope
Proves immediate physical silence ✗ ✗ ✗
Needs terminal local ownership ✓ ✓ ✓
Safe default for one caller's interruption Bounded fallback Preferred supported route ✗

Three cancellation policies separate local suppression, per-request cleanup, and a broader group abort

Choose using the resource and correctness contract. A model computation that can safely finish may be handled differently from a business write whose outcome must be reconciled even after the conversation ends.

The speculative tool calling guide explains that proposal generation and irreversible action need different gates. Canceling a model request does not roll back a tool action already accepted by an external system.

Synthetic race scenarios preserve canceled target state, permit sibling output, reject obsolete generations, and block changed clear barriers

A cancellation experiment should also establish what happens when the cancel request itself fails. Local output remains invalidated, while the adapter records an unknown or pending remote outcome and uses a bounded retry or reconciliation policy that addresses the same request identity.

A transport timeout during cancellation is not proof that the runtime ignored the request. The runtime may have accepted it before the response was lost, so a retry must follow the public API's idempotency behavior rather than creating a new operation or widening the scope to a shared group.

Make the retry budget explicit in the test report. Record attempts against the same target, whether a final remote disposition was observed, and which local resources remained retained while the outcome was unknown; do not turn repeated cancel calls into an unbounded background loop.

Test a callback delivered after that budget ends. The local terminal record must still reject output, and the callback should either complete cleanup reconciliation or be recorded as late evidence under the retention policy rather than reopening the request.

Cancellation can race with successful completion. Define the local linearization point as the owner's first terminal transition, then test both event orders: completion before cancellation preserves completed state, while cancellation before completion preserves canceled state and discards later output.

That local rule does not resolve an external business write. A tool can commit before the application sees the completion event, so a canceled conversation can coexist with a successful action that still needs a truthful reconciliation response through an approved channel.

Keep the action ledger separate from the inference ledger. An unknown external action outcome requires reconciliation under its idempotency contract; replaying canceled model output is not a safe way to discover what happened.


Frequently asked questions

What does per-request cancellation mean?

It targets one admitted operation and its owned output without granting authority over independent requests that share execution resources. The public runtime's supported cancellation unit determines which adapter operation can implement that scope.

Is canceling a Python task enough?

It stops or signals local coroutine work under Python's task semantics. Remote inference needs its own supported cancellation route, and the adapter should retain separate evidence for local invalidation, cancel submission, backend observation, and cleanup.

Should an interruption abort the whole batch?

Only when the whole batch belongs to that same owner and the public interface defines that behavior safely. Independent callers sharing a scheduling batch should retain their requests when one caller interrupts.

Can canceled compute continue running?

Yes, when stopping is cooperative, delayed, unsupported, or not yet observed by the backend. Local output suppression can still be correct, but the report must keep resource cleanup pending instead of claiming the computation stopped.

Why keep generation and request identities?

Generation identifies which conversational response owns output, while request identity identifies one backend operation. Several operations can belong to one generation, and a new generation can supersede them without reusing their identities.

Can a canceled request become completed later?

The reference preserves the first terminal disposition. A later completion callback can be recorded as historical evidence for diagnosis, but it does not change canceled state or restore permission to submit output.

How do exact retries behave after cancellation?

An exact evidence duplicate returns the duplicate classification while the current request remains canceled. Historical acceptance and present output authority are separate facts, so a duplicate must not reactivate the request.

Why classify older revisions before conflicts?

A strictly older delivery is stale even when the current record is final. Testing finalization first would mislabel delayed old evidence as a new conflict and make revision handling inconsistent across active and terminal records.

What is an equal-revision conflict?

It is changed payload or finalization evidence that reuses the current revision identity. The reference rejects it because the same identity cannot safely describe two different values under an immutable evidence contract.

Why use immutable bytes in the example?

They keep retained payload ownership narrow and inspectable. A real structured proposal needs schema validation and a copied immutable snapshot so caller alias mutation cannot change an already accepted revision.

Why recheck ownership after waiting?

An interruption, expiry, or clear-barrier change can occur during the wait. A pre-wait check cannot authorize output submitted later, so the final gate must inspect current generation and disposition at the actual local admission boundary.

What does a clear-barrier epoch protect?

It distinguishes the barrier observed before a wait from a newer clear operation created during that wait. A currently clear flag alone cannot prove that the held candidate waited for the right barrier.

Is empty the same as expired?

No. Empty means no candidate output exists in the current local record, while expired means its age budget was exceeded and the request became terminal. The controller needs different recovery actions for those outcomes.

Can smaller audio chunks solve cancellation?

They can change buffering and the frequency of submission opportunities. They do not establish request ownership, stop remote compute, or prove acoustic silence, so retain the generation and final admission checks regardless of chunk size.

How do we test sibling isolation?

Use a baseline cohort, cancel one target at several lifecycle stages, and verify every sibling's routing and disposition. Keep workload and admission limits constant, and inspect shared connection closure as a separate control-scope test.

Does a backend acknowledgment prove resource release?

It depends on the documented acknowledgment boundary. Keep cancel acceptance, backend observation, final request disposition, and resource release evidence separate unless the public contract explicitly joins them.

What if the runtime only exposes group cancellation?

Use an ownership-compatible grouping or a bounded finish-and-discard fallback while preserving local suppression. Do not expose an engine-wide abort as if it were a harmless per-caller operation.

Does cancellation undo a completed tool action?

No. An external action needs its own idempotency and reconciliation contract, even if the model request that proposed it is canceled. Keep inference completion separate from business-action outcome.

What should cancellation metrics count?

Count intents, local terminal transitions, submitted cancels, observed acknowledgments when available, late outputs discarded, and cleanup-pending requests. Preserve the denominator and distinguish requests from shared executions so the report remains recomputable.

What does the reference verify?

It verifies local terminal monotonicity, revision ordering, immutable byte evidence, sibling independence, and post-wait admission under synthetic fixtures. It does not run a live model server, measure device audio, or prove any private implementation detail of 60db Agent.

Make cancellation a tested contract

Define the owner and the public cancellation unit before connecting an interruption to a runtime control action. Invalidate output locally, retain terminal evidence, and observe backend cleanup without broadening the action to unrelated requests.

Run the complete reference fence and then adapt its cases to your public runtime. Include a target canceled during a wait, a sibling that completes, an exact retry after terminal state, an obsolete generation, and a changed clear barrier.

Keep one failing-before fixture when repairing an existing adapter. If the old code closes a shared stream, demonstrate that the same target cancellation interrupts a sibling in the isolated test, then show that the corrected per-request route preserves that sibling under the same workload and public runtime version.

Do not weaken the baseline to make the fix pass. Keep the same synthetic routing markers, admission limits, completion boundary, and cancellation stage evidence, because changing those conditions can hide the very scope error the regression is meant to preserve.

A release-ready report should explain both correctness and cost. State which output was suppressed, which siblings continued, what cleanup was observed, and what remains unknown, then use those boundaries when evaluating an application with 60db Agent or another public voice interface.