60db Logo
  • Engineering

Resume after a network change without replaying old speech

Fence stale media across reconnects with transport epochs, response generations, final admission checks, and explicit playback limits.

Editorial date
Reading time
26 min
An obsolete speech card stops at a gate while a fresh connection arc proceeds

TL;DR: Recovering connectivity does not make old speech current. Keep transport epochs, response generations and business-operation state separate, then recheck identity, age and clear-barrier state after waiting and immediately before local media submission.


A caller changes networks halfway through a reply. The connection recovers, and the agent flushes the speech it accumulated while the caller was offline, including an answer to a question the caller has already corrected.

The reconnect succeeded at its transport boundary. The conversational recovery failed because queued bytes were treated as permanently valid work rather than media owned by a current response.

Begin with a stale-media fence. A new connection can accept only media whose transport epoch, response generation, age and required barrier state still match the current controller.

The transport comparison explains why browser, WebSocket and telephony paths have different controls. This guide adds a small single-owner controller and event-driven fault fixtures for the validity boundary shared by those adapters.

All media and events are synthetic. The program does not establish real ICE connectivity, purge a physical receiver, authenticate a caller, or describe private 60db Agent implementation details.

Table of contents

  1. Separate connectivity from continuity
  2. Give each lifetime its own identity
  3. Define a final admission contract
  4. Run a reconnect fault harness
  5. Interpret acknowledgments narrowly
  6. Preserve ordering across drains
  7. Verify media after route changes
  8. Reconcile business actions separately
  9. Capture a useful recovery trace
  10. Qualify the recovery policy
  11. Turn every wait into a validity test
  12. Build an adapter contract around local submission
  13. Reconcile the task before choosing the next sentence
  14. Record what a passing recovery test establishes
  15. Review the boundary with a reproducible event order
  16. Frequently asked questions
  17. Begin with a held old chunk

Separate connectivity from continuity

Connectivity answers whether a transport path can exchange the relevant data. Conversation continuity answers which current question, evidence and response remain valid when that path becomes available again.

Those facts can change independently. A short outage can leave the task unchanged, while a caller correction can make a response obsolete even when the connection never failed.

The WebRTC browser specification defines restartIce as requesting an ICE restart through subsequent negotiation. Invoking it is not proof that negotiation completed, media flows in both directions, or a previous response should resume.

RFC 8445 supplies the ICE context. Keep signaling, selected route and actual media observations separate when recording recovery.

A historical developer question asks why mobile WebRTC does not resume after an internet dropout. It is a useful reproduction prompt, while the actual browser, route and adapter evidence decide the cause in your system.

A new connection should not inherit an unexamined queue of old work. Reconcile the current question, preserve terminal states and decide whether the next useful response should continue, summarize or start fresh.

The post-interruption guide provides that wording decision. A reconnect changes delivery opportunity; it does not itself answer which content the caller needs now.

Transport epoch, response generation and business operation occupy independent lifecycle lanes

Give each lifetime its own identity

A transport epoch identifies one accepted connection lifetime in the local controller. Increment it when the adapter replaces the path under the contract, and keep late callbacks attached to their issued epoch.

A response generation identifies one authorized output plan. A corrected question can advance the generation without reconnecting, while a transport repair can advance the epoch without changing the current question.

A business operation has its own durable identity. It can commit while the call is disconnected, so neither transport replacement nor speech cancellation should silently create another operation.

The workflow enforcement guide explains that business boundary. Keep the operation's unknown outcome visible until the owning service supplies reconciliation evidence.

A clear barrier is another current-state observation. It records the adapter's declared readiness condition before new output proceeds, and it must be scoped to the epoch and generation that requested it.

An old clear acknowledgment cannot satisfy a new barrier. If the controller changed while the request was waiting, record the historical acknowledgment without applying it to current output.

The interruption-safe output guide supplies the generation discipline. Its purpose is to prevent stale work from being relabeled as current simply because an asynchronous operation completed successfully.

A receiver measurement epoch also matters. Restarted media can reuse an SSRC, so counter baselines need an identity that distinguishes the old receiver lifetime from the new one.

Define a final admission contract

Final admission checks the item immediately before the protected local submission. Checks performed when the item was created or dequeued cannot establish that it remains valid after another await.

Compare stale identity before applying age-related side effects. An old chunk can be both obsolete and expired, but its expiry must not cancel a replacement response under a different identity.

Keep outcomes distinct. Closed means the task is terminal, stale means the item belongs to an obsolete identity, expired means the current item exceeded policy, and barrier_pending means the adapter has not supplied the required current readiness evidence.

An empty queue is another outcome. It says no local queued item is available; it cannot conceal a held chunk that still requires expiration handling or cancellation.

The backpressure guide develops that held-item boundary. Queue length is insufficient evidence for the complete output path.

The table compares checks, not vendors. A check means the policy explicitly supplies that property; a cross means another boundary is required.

Criterion Reconnect callback only Queue flush only Epoch and generation admission
Rejects old transport callbacks ✗ No identity check ✗ Queue is not callback state ✓ Exact epoch
Rejects corrected-response media ✗ Connectivity is unrelated ✗ Held work can survive ✓ Exact generation
Checks age after waiting ✗ No ✗ Depends on sender ✓ Final current check
Establishes endpoint silence ✗ No ✗ No ✗ Adapter evidence required
Prevents duplicate business action ✗ No ✗ No ✗ Durable operation contract

After waiting, explicit comparisons verify epoch, generation, age and current clear barrier before local submission

Run a reconnect fault harness

Save the whole fence as reconnect_fence.py and run Python 3.11 or later. It uses one event-loop owner and one consumer; no real transport, codec, endpoint or background service is started.

The controller owns the queue, terminal state, generation-wide ordering history and current barrier. Adapters supply trusted monotonic time, verified identity transitions, clear acknowledgments and a synchronous local writer.

The writer's acceptance is deliberately narrow. A successful local write does not establish receiver purge, acoustic silence or what the caller heard.

import asyncio
from dataclasses import dataclass


@dataclass(frozen=True)
class State:
    epoch: int
    generation: int
    barrier: int
    closed: bool


@dataclass(frozen=True)
class Chunk:
    epoch: int
    generation: int
    created: int
    serial: int
    data: bytes


class Controller:
    def __init__(self):
        self._state = State(0, 0, 0, False)
        self._ready = False
        self._queue = []
        self._last = None

    @property
    def state(self):
        return self._state

    def reconnect(self):
        s = self._state
        if s.closed:
            return "closed"
        self._state = State(s.epoch + 1, s.generation, s.barrier + 1, False)
        self._ready = False
        self._queue.clear()
        return "reconnecting"

    def new_response(self):
        s = self._state
        if s.closed:
            return "closed"
        self._state = State(s.epoch, s.generation + 1, s.barrier + 1, False)
        self._ready = False
        self._queue.clear()
        self._last = None
        return "new_response"

    def acknowledge(self, issued):
        if issued != self._state or issued.closed:
            return "stale_ack"
        self._ready = True
        return "local_barrier_accepted"

    def close(self):
        s = self._state
        self._state = State(s.epoch, s.generation, s.barrier + 1, True)
        self._ready = False
        self._queue.clear()

    def enqueue(self, chunk):
        if (type(chunk) is not Chunk or type(chunk.created) is not int
                or chunk.created < 0 or type(chunk.serial) is not int
                or chunk.serial < 0 or type(chunk.data) is not bytes):
            raise ValueError("owned immutable chunk required")
        s = self._state
        if s.closed:
            return "closed"
        if (chunk.epoch, chunk.generation) != (s.epoch, s.generation):
            return "stale"
        key = (chunk.created, chunk.serial)
        if self._last is not None and key <= self._last:
            return "out_of_order"
        self._last = key
        self._queue.append(chunk)
        return "queued"

    def take(self):
        if self._state.closed:
            return "closed", None
        return ("item", self._queue.pop(0)) if self._queue else ("empty", None)

    def admit(self, chunk, now, max_age):
        if any(type(x) is not int or x < 0 for x in (now, max_age)):
            raise ValueError("nonnegative local clock ticks required")
        s = self._state
        if s.closed:
            return "closed"
        if (chunk.epoch, chunk.generation) != (s.epoch, s.generation):
            return "stale"
        if now < chunk.created:
            return "invalid_clock"
        if now - chunk.created >= max_age:
            return "expired"
        return "ready" if self._ready else "barrier_pending"


async def send_held(owner, chunk, clock, wait, writer):
    await wait()
    result = owner.admit(chunk, clock(), 10)
    if result != "ready":
        return result
    writer(chunk.data)  # Synchronous local acceptance only; no further await.
    return "local_write_accepted"


def endpoint_claim(clear_accepted, old_playout_pending):
    if old_playout_pending:
        return "old_playout_pending"
    return "no_endpoint_evidence" if clear_accepted else "no_clear_evidence"


async def tests():
    owner = Controller()
    s = owner.state
    assert owner.acknowledge(s) == "local_barrier_accepted"
    first = Chunk(0, 0, 1, 0, b"synthetic")
    assert owner.enqueue(first) == "queued"
    assert owner.take() == ("item", first)
    assert owner.take() == ("empty", None)
    assert owner.enqueue(Chunk(0, 0, 0, 0, b"old")) == "out_of_order"
    assert owner.enqueue(Chunk(0, 0, 1, 1, b"equal-time")) == "queued"
    assert owner.admit(first, 11, 10) == "expired"
    assert owner.admit(first, 0, 10) == "invalid_clock"
    started, release = asyncio.Event(), asyncio.Event()
    writes = []

    async def wait():
        started.set()
        await release.wait()

    pending = asyncio.create_task(send_held(owner, first, lambda: 20,
                                            wait, writes.append))
    await started.wait()
    assert owner.reconnect() == "reconnecting"
    assert owner.acknowledge(s) == "stale_ack"
    assert owner.acknowledge(owner.state) == "local_barrier_accepted"
    release.set()
    assert await pending == "stale"
    assert writes == []
    current = Chunk(1, 0, 2, 0, b"current")
    assert owner.admit(current, 3, 10) == "ready"
    owner.new_response()
    fresh = Chunk(1, 1, 0, 0, b"new")
    assert owner.enqueue(fresh) == "queued"
    assert owner.admit(fresh, 0, 10) == "barrier_pending"
    owner.acknowledge(owner.state)
    assert owner.admit(fresh, 0, 10) == "ready"
    assert endpoint_claim(True, True) == "old_playout_pending"
    assert endpoint_claim(True, False) == "no_endpoint_evidence"
    # Each caller owns the complete ledger, including interrupted schedules.
    async def scheduled(change, expected, expected_writes):
        subject = Controller()
        subject.acknowledge(subject.state)
        held = Chunk(0, 0, 1, 0, b"held")
        entered, proceed = asyncio.Event(), asyncio.Event()
        ticks = [2]
        ledger = []

        async def gate():
            entered.set()
            await proceed.wait()

        task = asyncio.create_task(send_held(subject, held, lambda: ticks[0],
                                             gate, ledger.append))
        await entered.wait()
        change(subject, ticks)
        proceed.set()
        assert await task == expected
        assert ledger == expected_writes

    await scheduled(lambda c, t: c.new_response(), "stale", [])
    await scheduled(lambda c, t: t.__setitem__(0, 11), "expired", [])
    await scheduled(lambda c, t: None, "local_write_accepted", [b"held"])
    unready = Controller()
    assert unready.admit(Chunk(0, 0, 0, 0, b"x"), 1, 10) == "barrier_pending"
    assert unready.admit(Chunk(0, 0, 0, 0, b"x"), 1, 0) == "expired"
    try:
        owner.enqueue(Chunk(1, 1, 1, 1, bytearray(b"mutable")))
    except ValueError:
        pass
    else:
        raise AssertionError("mutable payload must be rejected")
    owner.close()
    assert owner.enqueue(fresh) == "closed"
    assert owner.take() == ("closed", None)
    assert owner.admit(fresh, 0, 10) == "closed"
    assert owner.reconnect() == owner.new_response() == "closed"
    assert owner.acknowledge(owner.state) == "stale_ack"
    print("synthetic reconnect fencing fixtures passed")


if __name__ == "__main__":
    asyncio.run(tests())

The numeric age bound is a synthetic fixture setting in local ticks. It is not a recommended timeout or a measurement of any provider's clear behavior.

If a real writer awaits, the adapter needs another current-state check at its actual protected submission boundary. The synchronous writer here makes that assumption visible instead of hiding an asynchronous race inside a helper.

Interpret acknowledgments narrowly

A clear request can be accepted locally while old media remains buffered downstream. The fixture preserves that case as old_playout_pending rather than claiming that the caller heard nothing.

Twilio's public Media Streams message contract documents its own mark and clear semantics. A different receiver can expose different controls, so the adapter must state what each acknowledgment establishes.

A writer acknowledgment can also arrive after the task closes. Keep it as historical evidence of the old attempt, while terminal closure continues to block new output from that identity.

The shutdown semantics guide develops terminal ownership. A cached successful send must not reopen a closed conversation or replay a canceled response.

Do not interpret missing endpoint evidence as confirmed silence. Unknown means the observation is unavailable, and a safe recovery plan should describe the supported current state without inventing what the caller heard.

A repeated clear acknowledgment should not allocate another response generation. The controller can record it idempotently under its issued identity, while a real adapter decides whether additional receiver work is necessary.

Preserve ordering across drains

Creation-order history belongs to the response generation, not to the queue's present length. Removing the last item must not allow an older same-generation arrival to be treated as a fresh first item.

The harness retains its last accepted creation key after take drains the queue. It permits equal creation ticks only when a strictly increasing serial provides the declared tie-breaker.

A transport reconnect clears queued local items but preserves that generation's creation history. A new response generation deliberately resets the history because it establishes a new output ownership scope.

The chunk-size guide explains the media-unit boundary. Do not use packet arrival order as an implicit substitute for source creation order after recovery.

One consumer is an explicit assumption here. Multiple consumers would require another ordering or atomic submission contract, because two independently held items can pass checks and still be submitted in the wrong order.

The WebSocket stall guide provides a related ordering diagnosis. Ordered complete-message delivery can preserve byte order while application callbacks still become obsolete under a newer conversation state.

Verify media after route changes

Observe inbound and outbound media separately after connectivity recovers. A signaling success or selected candidate pair does not prove the microphone and speaker paths are both working.

RFC 8656 describes TURN relay behavior. Record the actual selected route category for the trial rather than assuming a repaired path has the same latency and loss properties as the previous one.

The mobile browser guide identifies device and lifecycle conditions that can coincide with network changes. A background transition or audio route switch can leave the call connected while media behavior changes.

Keep receiver identities and epochs in the measurement bundle. The WebRTC statistics specification defines the counters' boundaries, while the adapter must avoid calculating deltas across incompatible receiver lifetimes.

Use a harmless synthetic phrase for adapter testing. Retain evidence of what was sent and what reached the declared endpoint without exposing a real caller's conversation.

Signaling, selected route, inbound media, outbound media and playback evidence are separate recovery checks

Reconcile business actions separately

A booking or update can commit while the connection fails. Replaying the spoken explanation is not the same as repeating the business operation, and a reconnect callback must not do the latter by accident.

Keep a stable operation identity outside the response generation. If its outcome is unknown, query or reconcile through the owning service's supported contract before reporting success or issuing another effect.

The workflow enforcement guide explains why confirmation and action authority have their own current-state checks. Fresh media transport does not renew stale permission or an old proposal revision.

The truthful waiting speech guide helps describe pending reconciliation. Tell the caller what is known without promising that the action failed merely because its response was lost.

A caller can correct the task during an outage. Preserve that accepted correction and invalidate dependent preparation instead of replaying the pre-outage payload when the route returns.

The spoken correction guide provides the value-replacement model. Transport retry and business-state revision should never be hidden inside the same generic retry flag.

Transport repair, response restart and action reconciliation have separate responsibilities

Capture a useful recovery trace

A trace should identify the old and new transport epochs, response generation, barrier identity, held media identity, creation time, age decision and local submission outcome. Keep endpoint observations separate from those local events.

The observability guide gives the boundary-oriented pattern. Retain original counters and timestamps with clock domains so a later report can recompute the decision instead of trusting a derived recovered label.

Record which callback supplied each identity. A late event from the old epoch should remain attributable to the old path even when its handler runs after a new connection is active.

Avoid logging credentials, raw addresses or customer audio to make the trace more detailed. Synthetic task and media identities can test these races without disclosing a real route or person.

The latency benchmark guide helps measure the actual recovery interval. Time to a reconnect callback, time to first local write and time to a useful observed response are different boundaries.

A synthetic old request completes after a new epoch is active and is rejected instead of restoring playback

Qualify the recovery policy

Use event-driven fixtures to force inconvenient ordering rather than hoping a timed sleep produces the race. Hold old work, change current identity, then release the completion and assert both the typed outcome and absence of a local write.

The regression guide explains how to retain that schedule. A test that checks only the final response text can miss an earlier stale chunk that already escaped.

Include the empty-queue held-item case and a successful-clear acknowledgment with old endpoint output still pending. Those tests distinguish controller correctness from unsupported delivery claims.

Then run a controlled real-adapter trial on the supported routes. The warm handoff guide provides another continuity boundary where ownership and current task state must survive transport changes.

This article reports no production reconnect success rate or physical silence result. Its full reference fence verifies a local single-owner contract; actual signaling, receiver controls, audio devices and business reconciliation need their own evidence.


Turn every wait into a validity test

The most revealing reconnect fixture begins before the reconnect. Let a consumer remove an old chunk from the visible queue, then pause that consumer at an explicit wait point. At that moment the queue can be empty even though old work still exists. A test that only clears the queue will miss the consumer-owned chunk entirely.

While the consumer waits, change the controller's transport epoch. Release the wait and inspect the writer's caller-owned ledger. The required outcome is both a stale classification and no local submission of the old chunk. Checking only the return value is weaker because an incorrectly implemented function could return stale after already calling the writer.

The published harness uses an event to make that schedule deterministic. It does not depend on a sleep being long enough for a race to occur. The caller owns the submitted-byte ledger, so interruption or a failed assertion cannot make the test's evidence disappear with a short-lived writer object.

Repeat that pattern for response generation. Hold a chunk from the old answer, activate a new answer, and release the old consumer. The transport can remain unchanged throughout the test. A generation fence should still reject the obsolete answer because conversational validity does not require a network failure to change.

Age deserves a separate fixture. Admit a current chunk to an intermediate stage while it is still fresh, then advance the injected clock before final admission. The final check should expire it even though its epoch and generation remain current. Checking age only at enqueue would allow a long local wait to turn an accepted chunk into stale speech.

Barrier state is another independent condition. A chunk can have the current epoch and generation while a required local clear transition has not yet been accepted. Preserve that waiting outcome instead of calling it expired or empty. Those labels identify different next steps: time can make age worse, while the matching barrier acknowledgment can make local submission eligible.

Also test the successful path after the negative fixtures. Release a current, fresh chunk under the matching accepted barrier and confirm exactly one writer call. Negative-only tests can accidentally validate an implementation that rejects every chunk. The contract needs both a reason to reject obsolete work and a bounded way for current work to progress.

The stale-audio prevention guide explains the broader ownership issue. The same final-admission rule applies when a tool completion, a resumed transport or a background producer wakes after a caller correction. Every wait can separate earlier approval from current validity.

Keep the final mutation boundary visible in review. In this small program, final admission and the synchronous writer invocation have no intervening await on the same owner. A real adapter that awaits inside its submission function must define whether that await precedes irrevocable acceptance and where a new validity check can still prevent sending. The Python example does not guarantee behavior inside an opaque asynchronous transport library.

Do not try to repair that gap with a comment saying that the writer is quick. Identify the actual acceptance boundary, cancelability and ownership rules. If the API cannot conditionally submit against current state, narrow the guarantee to what the adapter can enforce and measure any later cancellation behavior separately.

Build an adapter contract around local submission

The reference controller deliberately ends at local submission. That makes it reusable as a reasoning tool, but it leaves real adapter work that should be specified rather than implied. An adapter contract names the transition from controller-owned media to transport-owned media and the evidence available after that transition.

Start with the writer's input. Is the submitted value copied, retained by reference, or consumed immediately? The artifact uses immutable bytes to prevent a caller from rewriting an approved chunk after admission. If a real API accepts a mutable buffer, establish whether the API copies it before returning or whether the caller must retain exclusive ownership until a later completion.

Next define acceptance and failure. A synchronous return may mean local queue acceptance. An asynchronous completion may mean a different boundary. A failure after partial acceptance can leave uncertainty about which media became transport-owned. Record that uncertainty explicitly; blindly retrying the same bytes can replay audio even when the controller's earlier decision was correct.

Give local submissions stable identities. A response generation identifies the current answer, while a chunk identity identifies a particular piece of that answer. Those identifiers let a trace distinguish a legitimate fresh chunk from a retry of a possibly accepted one. They also support a clear policy for duplicate completion callbacks without treating every repeated notification as a new network transition.

Define the adapter's clear operation independently. Which queue does it clear? Does it affect media held by another application consumer? Does it request provider-side clearing? What acknowledgment, if any, is available, and what does the provider document that acknowledgment to mean? A generic success Boolean cannot answer all of these questions.

The playback acknowledgment guide is useful when choosing that observation boundary. A local clear acknowledgment does not prove that previously accepted media vanished from a distant device. It also cannot retract sound already emitted into the room.

Keep an explicit uncertain-playout state when the remote boundary is unavailable. That state can guide a conversational repair, such as asking the caller to disregard an earlier partial statement or confirming the current question. It should not be hidden behind an assertion that the old answer was unheard.

The artifact's pending-old-playout fixture is designed to preserve that limit. It supplies a successful local acknowledgment together with evidence that old playout remains pending. The resulting claim stays narrow. The correct controller outcome and a still-pending remote outcome can coexist; one does not invalidate the other.

Document what reconnect means for adapter-owned work. Some APIs expose a new connection object, while others preserve an object across a changed lower-layer route. Use an explicit lifecycle event to advance the transport epoch rather than deriving identity only from an object address, a reused stream label or an unchanged counter value.

The receiver buffer discussion describes another source of waiting beyond the local queue. A controller can prevent new stale submissions while earlier accepted media remains buffered elsewhere. Qualifying that boundary makes the guarantee useful and testable without claiming control over components the adapter cannot observe.

Reconcile the task before choosing the next sentence

Once transport progress returns, the application still needs to decide what to say. Treat that as a task-state decision using the current caller request and verified business evidence. Replaying the previous response simply because its bytes are available turns an implementation convenience into a conversational policy.

Begin by checking whether the caller's latest question is still the one that produced the held response. A correction can replace an amount, change a date or withdraw a request. If the response generation changed during the outage, old media should remain obsolete even when its wording happens to sound generally relevant.

Next determine which business operations reached a durable outcome. A network interruption during a write can leave the local client uncertain while the owning service completed the action. Query the operation's status or reconcile through the service's supported idempotency contract. A new media epoch does not authorize a new business action.

The tool-write safety guide addresses that separation. A media retry and a business retry have different identities, effects and evidence. Keeping them distinct prevents a restored voice path from accidentally duplicating an appointment, charge or other external change.

Then choose a useful current response. If the task is unchanged and the unheard portion is known at a supported boundary, a continuation may be appropriate. If the heard portion is uncertain, a brief recap or confirmation can avoid assuming the caller received a missing condition. If the caller changed the request, start from the new intent.

Do not infer the heard portion by subtracting local queue length from generated duration. Media can be held by consumers, transports, providers and devices. Without a documented join to playout evidence, the calculation identifies local movement rather than acoustic delivery.

A repair sentence should preserve the same uncertainty as the trace. If the system knows only that connectivity returned, it can acknowledge the interruption and restate the current question. If it knows a business action completed, it can report that verified result. It should not invent an explanation for the outage or claim that an earlier sentence was never heard.

Keep silence and duplication visible in the review procedure. A technically safe policy that rejects everything can leave the caller waiting indefinitely. A policy that resumes aggressively can repeat obsolete or misleading speech. Evaluate whether current work progresses, whether the caller can interrupt it, and whether the repair wording makes the active task clear.

Use synthetic task state in development fixtures. Represent the prior question, the corrected question and the verified operation outcome without importing customer identifiers or real call content. The point is to test the validity transition, not to reproduce private business data.

The recovery decision should produce a new generation when it creates a new answer. That makes the chosen wording subject to the same admission rules as any other response. Reconnection should not create a privileged path that bypasses age checks or allows earlier generations back into the writer.

Record what a passing recovery test establishes

A passing bounded test establishes a property of the supplied controller, schedule and evidence. It can prove that a held old chunk did not reach the test writer after an epoch change. It cannot establish that every physical endpoint purges old audio under every network condition.

Write the claim with those nouns intact. Name the input identity, the lifecycle transition, the local acceptance boundary and the observed outcome. Include the writer ledger and final controller state. A future maintainer can then rerun the same schedule after changing queue ownership or adapter code.

Preserve the negative outcomes as first-class results. Stale, expired, waiting, closed and empty should remain distinguishable in logs and tests. Empty means no queued item was available at that observation. It does not certify that no consumer owns media. Waiting means a current item cannot yet cross a required barrier; it does not mean that item is old.

Retain ordering history when draining the queue. Otherwise an old producer can submit a previously rejected position after the visible backlog disappears. The program keeps its last accepted ordering key outside the queue so a drain changes occupancy without erasing the response's admission history.

Keep the terminal test at the end of the lifecycle and exercise attempted resurrection. Closing the controller must reject enqueue, reconnect and final admission through their documented outcomes. A cached ready flag or an earlier accepted barrier cannot restore a closed session. If a new conversation is legitimate, create a new explicitly owned controller rather than reviving the old one through an unrelated callback.

Store the injected clock values with the event order. A test report that says a chunk expired is incomplete without its creation time, final admission time, permitted age and clock domain. The same discipline applies to identity: retain raw epoch, generation and barrier values rather than only a friendly current-or-stale label.

Finally include what remains unmeasured. Real route restoration, provider clearing, remote playout and human comprehension need their own observations. The bounded harness supplies a reliable foundation for those trials because it prevents local stale-work bugs from being mistaken for unavoidable network behavior.

Review the boundary with a reproducible event order

A recovery review is easier when every fixture can be read as a short event sequence. Name the initially current state, the chunk owned by the consumer, the event that changes validity, the release of the wait and the final local writer observation. That sequence is the actual test; elapsed wall time is incidental.

Keep the initial state in the report even when its numeric values seem obvious. An old generation and an old transport epoch are different causes. A stale acknowledgment should retain the state against which it was issued so a reader can see why it no longer matches the controller.

Use the complete state when accepting a barrier acknowledgment. Matching only the connection identity can accidentally accept an acknowledgment issued before a new response or a later clear transition. The reference compares the immutable state value, including the barrier revision and terminal flag, rather than relying on a cached ready Boolean alone.

Review equal creation times explicitly. Two chunks can be produced within one clock tick. The serial component makes their ordering deterministic in this synthetic contract. Reusing the same ordering key is rejected, while the next serial at the same creation time can progress. An adapter must supply that key from its documented producer order rather than inventing a later timestamp to bypass rejection.

Review an empty drain alongside retained history. The absence of queued entries changes neither the most recent accepted ordering key nor the identity of a consumer-owned item. Keeping those facts outside queue occupancy prevents an old producer from regaining admission simply because another worker consumed the visible backlog.

Finally inspect the caller-owned writer ledger after every interrupted fixture. A return label alone cannot establish absence of a side effect. The ledger proves which synthetic bytes crossed the local writer boundary, including the empty result required for stale or expired work. That is the evidence the harness can establish before a real endpoint trial begins.

Keep a positive control in that review. Current, fresh media under the matching accepted local barrier should reach the synchronous test writer exactly once. A safety check that blocks every generation can satisfy a negative fixture while breaking conversation progress. The harness's positive writer ledger prevents that accidental interpretation of success.

Keep a zero-age boundary as well. Under the synthetic rule, age equal to the allowed bound is expired, including a bound of zero. That choice is explicit policy for the reference function, not a browser timeout recommendation. An adapter can choose another policy only by documenting and testing the new equality rule.

When the schedule changes, rerun the exact published fence rather than an earlier extracted copy. The manuscript is the artifact readers receive. Recording its code hash and execution result ties the evidence to that copy and prevents a passing local helper from masking a later documentation error.

Frequently asked questions

What does ICE restart request?

It requests an ICE restart through subsequent negotiation under the browser API contract. Calling the method does not prove the path recovered or that current media can flow in both directions.

Does connected prove two-way audio?

Connected transport state is weaker than verified inbound and outbound media. Test microphone, receiver and playback observations separately on the actual route.

Should transport epoch equal response generation?

They represent different lifetimes. A route can change while the question stays valid, and the question can change while the transport remains connected.

Can old callbacks arrive after reconnect?

Old asynchronous work can complete after replacement state is active. Keep its issued epoch and reject it at the current-state boundary instead of relabeling it as new work.

What should cached success return after closure?

A historical success can remain diagnostic evidence, but it cannot reopen output authority. Retries must preserve the task's current closed or canceled disposition.

Why recheck after every wait?

Identity, age and barrier state can change while work is suspended. Check again at the protected local operation, and repeat the check if another asynchronous step is introduced.

Can a held chunk expire in an empty queue?

A consumer can hold an item after dequeue while waiting for readiness. Queue emptiness says nothing about that held item's current age or authority.

What does a clear barrier protect?

It protects the adapter's declared readiness boundary for a particular epoch and generation. It does not establish universal endpoint purge or acoustic silence unless separate evidence supports those claims.

Should old expiry cancel new output?

An obsolete item should be rejected by identity before its expiry causes side effects. Any cancellation must target the exact affected generation rather than whichever response happens to be current.

How do I preserve ordering after drain?

Retain generation-wide creation-order history when the queue becomes empty. Reset it only under the declared new-generation rule, with an explicit tie-breaker for equal timestamps.

Does a network change require a new task?

A network change can leave the business task unchanged. Reconcile current intent and operation state rather than automatically creating another task or replaying the old one.

Can a business operation be retried on reconnect?

Retry only under the owning service's idempotency and reconciliation contract. A missing response does not establish failure, and transport recovery does not authorize another effect.

How should duplicate recovery signals behave?

Duplicate signals should preserve the accepted identity and disposition without reopening terminal state or allocating unnecessary work. Scope acknowledgments to the current issued barrier.

What if the caller corrects the task offline?

Apply the accepted correction and invalidate dependent preparation. The next eligible response should use current facts rather than flushing pre-outage speech.

How should receiver identity changes affect stats?

A new receiver epoch starts another baseline even when an SSRC is reused. Do not calculate deltas across incompatible cumulative counters.

Can TURN restore every failed path?

TURN supplies a relay mechanism under its protocol contract. Availability, policy and route conditions still determine whether a particular trial recovers useful media.

How should microphone changes be separated?

Track device and capture state independently from transport state. A connected route with a failed or changed input device needs a media-path diagnosis rather than another generic reconnect attempt.

What does the synthetic harness prove?

It verifies local epoch, generation, age, barrier, terminal and ordering decisions under controlled fixtures. It does not prove real connectivity, receiver purge, acoustic output or business-action correctness.

Which failures belong in the recovery denominator?

Keep disconnected, unavailable, timed-out and incomplete trials in workload accounting. If timing uses completed runs only, show that conditional population alongside the other outcomes.

Where should a reconnect test start?

Hold one old chunk, replace the transport epoch, then release the old completion. Assert its stale disposition and that the local writer received nothing before extending the trial to real adapters.

Begin with a held old chunk

Run the reference harness and preserve its old-completion-after-reconnect schedule. Attach the same identities and final admission checks to one actual adapter without broadening the claimed observation boundary.

The next useful proof is a trace that separates restored connectivity, current response authority and supported playback evidence. Keep business reconciliation independent so a successful reconnect never becomes an accidental duplicate action.