60db Logo
  • Engineering

Keep voice settings isolated across concurrent calls

Use owned voice snapshots and post-await revision checks to prevent configuration leaks across concurrent calls.

Editorial date
Reading time
26 min
Two call lanes keep their own tuning knobs and speech tiles behind a divider

TL;DR: Resolve and copy supported voice settings into an immutable session snapshot before dispatching asynchronous work. Bind every result to its session, generation, and configuration revision, then recheck those identities after waiting and immediately before local submission.


Two calls share one synthesis client. Call A selects one language, waits for capacity, and later speaks with the voice or rate selected by call B. Each request looked correct when logged at intake. The mutable configuration changed before the adapter consumed it.

The first useful repair is ownership. Keep reusable transport resources separate from per-call voice settings. Pass an owned request value to synthesis instead of letting a callback read whichever global configuration happens to be current.

A second check is necessary after the asynchronous operation. The settings can remain unchanged while the call closes or the response is superseded. An old result must not regain permission to play just because it was generated successfully.

The voice consistency corpus helps evaluate audible behavior. This article proves a different boundary: configuration and result isolation under deterministic interleavings. It provides no live SDK integration, acoustic result, or private implementation claim about 60db Agent.

Table of contents

  1. Define the configuration ownership boundary
  2. Resolve settings before asynchronous dispatch
  3. Run an isolated session reference
  4. Test adversarial interleavings
  5. Reuse resources without sharing call state
  6. Trace requested and observed settings
  7. Roll out the smallest complete repair
  8. Frequently asked questions
  9. Make the request own its voice

Define the configuration ownership boundary

A voice setting belongs to the request whose output it controls. A shared connection pool can be reusable while the voice, language, rate, format, and policy revision remain request-specific. Reuse does not require one mutable options object for all calls.

Start by listing the places that read or change settings. Intake, defaults, tenant policy, language detection, fallback selection, synthesis dispatch, callbacks, retries, cache lookup, and output processing can all participate. Fixing only the first assignment leaves later readers exposed.

Keep the accepted shape small and explicit. A voice identifier, locale, and rate multiplier are supported values in the reference below. Nested provider objects, credentials, arbitrary callbacks, and mutable extensions are rejected rather than copied incompletely.

Python's dataclasses documentation describes frozen dataclasses and their limits. Freezing a record does not recursively freeze a dictionary stored inside it. Use immutable supported field types or make an owned recursive representation with a declared value shape.

A read-only view over a caller's dictionary is also insufficient. MappingProxyType is a dynamic read-only view, so changes through another alias remain visible. Copy the approved values before exposing a read-only representation.

A shallow dictionary copy can still share nested values. If your supported configuration includes a list of pronunciation rules, freeze or copy that list and its supported members. If it does not support such a field, reject it instead of accepting an untested extension.

The cache-key contract adds another owner. Cached audio must include every material context required for safe reuse. An isolated request can still receive the wrong voice if its cache lookup ignores a relevant setting or scope.

Conceptual two call lanes validate settings and create owned snapshots before dispatch without cross-lane arrows

A session identity and a configuration revision answer different questions. The session identifies the conversation owner. The revision identifies which approved settings apply. A generation identifies which response remains active within that conversation.

Do not reuse those identities interchangeably. Two responses can share the same voice revision while only the newer response is allowed to play. A settings change can invalidate pending work even when the conversation identity remains the same.

The interruption-safe contract supplies the generation boundary. A configuration snapshot preserves what a request meant; it does not by itself decide whether that request remains relevant after the caller interrupts.

Terminal state is another independent fact. A closed session stays closed, even if an exact retry returns historical evidence or a provider later completes synthesis. Keep historical result retrieval separate from current permission to submit output.

For a bounded repair, write the ownership rule in plain language beside the adapter. "Every dispatched request receives an owned immutable voice value and a captured generation" is easier to verify than a broad promise that the whole client is thread-safe.

Resolve settings before asynchronous dispatch

Resolve precedence once at a documented boundary. Defaults, project policy, session preference, and an explicit per-response override can have a defined order. Preserve the resolved result rather than recomputing it from mutable inputs after an await.

Validation should use the provider-facing contract and the application's policy. A syntactically valid voice identifier can still be unsupported for the selected locale or deployment. A finite positive rate can still lie outside an evaluated product range.

The reference uses a deliberately local rate range and two synthetic voices. They are test-policy values, not a provider feature list or recommendation. A real adapter must substitute its supported public catalog and policy without claiming the example's values are universal.

Microsoft's synthesis guide illustrates public controls for voice selection and output format. It is a provider-specific interface. Use its current documented behavior when integrating that provider rather than assuming another client shares the same configuration lifecycle.

Keep requested, resolved, and observed settings distinct. Requested values describe caller intent. Resolved values describe the owned request sent under policy. Observed values describe returned metadata or measured output, where the interface provides that evidence.

A fallback must be explicit. If a requested voice is unavailable, either reject it under policy or select a documented substitute and record that choice. Silently retaining the previous pooled client's voice can make one call inherit another call's identity.

SSML 1.1 provides a public speech-markup vocabulary. When voice settings can appear both in a client configuration and markup, the adapter must follow the provider's documented precedence. Do not assume the global setting wins or that unsupported markup is harmless.

The confirmation prosody article explains why a material rate or pronunciation change can affect critical fields. Version the policy so a cached confirmation or pending synthesis result remains attached to the settings under which it was approved.

Validate a candidate configuration before replacing the current one. If validation fails, the session should retain its previous valid snapshot and revision. A partial update that changes voice but fails language validation creates a state that nobody approved.

A request snapshot should also be created before logging the resolved configuration. Logging the mutable intake dictionary proves only what it contained at that instant. The dispatch trace needs the exact owned value consumed by the provider adapter.

Avoid storing credentials inside the voice snapshot. Resource credentials and tenant permissions have their own access boundary. A diagnostic record can keep a non-sensitive resource class or authorized scope reference without publishing secrets or account identifiers.

The shared-phone identity guide handles a different session risk: deciding who the caller is. Voice isolation prevents configuration crossover, but it does not establish caller identity or authorize access to remembered facts.

Run an isolated session reference

This reference controls settings ownership and final local admission. It uses a synthetic asynchronous probe that returns a request record, so the harness can force interleavings without a real provider. It deliberately does not synthesize audio or pretend to be a production SDK.

The supported settings are flat immutable values. Two synthetic voice names, two synthetic locale choices, and a local tested rate interval define the fixture policy. The output contract is a request record accepted by a synchronous local submission callback.

Save the full fence as voice-isolation.py and run Python 3.11 or later. A real adapter must validate provider results, preserve its documented output format, and repeat admission checks after every additional asynchronous boundary.

import asyncio
import math
from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True)
class Voice:
    voice: str
    language: str
    rate: float

    def __post_init__(self):
        if type(self.voice) is not str or self.voice not in ("synthetic-A", "synthetic-B"):
            raise ValueError("unsupported fixture voice")
        if type(self.language) is not str or self.language not in ("en", "hi"):
            raise ValueError("unsupported fixture language")
        if type(self.rate) not in (int, float):
            raise ValueError("numeric fixture rate required")
        if not math.isfinite(self.rate) or not 0.75 <= self.rate <= 1.25:
            raise ValueError("outside local fixture rate policy")

def snapshot(raw):
    if type(raw) is not dict or set(raw) != {"voice", "language", "rate"}:
        raise ValueError("exact flat configuration required")
    return Voice(raw["voice"], raw["language"], raw["rate"])

@dataclass(frozen=True)
class Request:
    session: str
    generation: int
    revision: int
    settings: Voice
    text: str

class Session:
    def __init__(self, identity, raw):
        if type(identity) is not str or not identity:
            raise ValueError("session identity required")
        self._identity = identity
        self._settings = snapshot(raw)
        self._revision = 1
        self._generation = 0
        self._closed = False

    @property
    def settings(self):
        return self._settings

    def configure(self, raw):
        if self._closed:
            raise RuntimeError("closed")
        candidate = snapshot(raw)
        if candidate != self._settings:
            self._settings = candidate
            self._revision += 1

    def close(self):
        self._closed = True
        self._generation += 1

    async def render(self, text, synthesize, submit):
        if self._closed:
            return "closed"
        if type(text) is not str or not text:
            raise ValueError("nonempty text required")
        self._generation += 1
        request = Request(self._identity, self._generation,
                          self._revision, self._settings, text)
        result = await synthesize(request)
        # Final check and synchronous local submit share one event-loop turn.
        if self._closed:
            return "closed"
        if request.generation != self._generation:
            return "stale-generation"
        if request.revision != self._revision:
            return "stale-settings"
        if result != request:
            raise ValueError("synthetic result identity mismatch")
        submit(request)
        return "submitted-locally"

def rejects(error, fn):
    try:
        fn()
    except error:
        return
    raise AssertionError("expected " + error.__name__)

async def main():
    raw = {"voice": "synthetic-A", "language": "en", "rate": 1.0}
    a = Session("synthetic-call-A", raw)
    raw["voice"] = "synthetic-B"
    assert a.settings.voice == "synthetic-A"
    rejects(FrozenInstanceError, lambda: setattr(a.settings, "voice", "synthetic-B"))
    rejects(AttributeError, lambda: setattr(a, "settings", snapshot(raw)))
    old = a.settings
    rejects(ValueError, lambda: a.configure({**raw, "rate": float("nan")}))
    assert a.settings == old
    rejects(ValueError, lambda: snapshot({**raw, "extra": []}))
    rejects(ValueError, lambda: snapshot({**raw, "rate": True}))
    for rate in (0.75, 1.25):
        assert snapshot({**raw, "rate": rate}).rate == rate
    for rate in (0.749, 1.251):
        rejects(ValueError, lambda rate=rate: snapshot({**raw, "rate": rate}))

    entered, release = asyncio.Event(), asyncio.Event()
    async def held(request):
        entered.set()
        await release.wait()
        return request
    async def immediate(request):
        return request
    accepted = []
    task = asyncio.create_task(a.render("A", held, accepted.append))
    await entered.wait()
    b = Session("synthetic-call-B", raw)
    assert await b.render("B", immediate, accepted.append) == "submitted-locally"
    release.set()
    assert await task == "submitted-locally"
    assert [(x.session, x.settings.voice) for x in accepted] == [
        ("synthetic-call-B", "synthetic-B"),
        ("synthetic-call-A", "synthetic-A")]

    # A repeated identical configuration does not invalidate work.
    entered.clear(); release.clear()
    task = asyncio.create_task(a.render("same settings", held, accepted.append))
    await entered.wait()
    a.configure({"voice": "synthetic-A", "language": "en", "rate": 1.0})
    release.set()
    assert await task == "submitted-locally"

    # Wrong result identity fails without submitting locally.
    async def wrong(request):
        return None
    count = len(accepted)
    try:
        await b.render("wrong result", wrong, accepted.append)
    except ValueError:
        pass
    else:
        raise AssertionError("result mismatch accepted")
    assert len(accepted) == count

    # A revision during await rejects the old result.
    entered.clear(); release.clear()
    task = asyncio.create_task(a.render("old settings", held, accepted.append))
    await entered.wait()
    a.configure(raw)
    release.set()
    assert await task == "stale-settings"

    # A newer response supersedes an older response in the same session.
    entered.clear(); release.clear()
    task = asyncio.create_task(a.render("old response", held, accepted.append))
    await entered.wait()
    assert await a.render("new response", immediate, accepted.append) == "submitted-locally"
    release.set()
    assert await task == "stale-generation"

    # Closing is terminal even if synthesis completes later.
    entered.clear(); release.clear()
    task = asyncio.create_task(a.render("closing", held, accepted.append))
    await entered.wait()
    count = len(accepted)
    a.close(); release.set()
    assert await task == "closed" and len(accepted) == count
    assert await a.render("retry", immediate, accepted.append) == "closed"
    rejects(RuntimeError, lambda: a.configure(raw))
    print("Synthetic isolation fixtures passed; no audio or provider tested")

asyncio.run(main())

The settings property has no setter, and the frozen value contains supported scalar types. The harness tests both item mutation and property rebinding. That is stronger than returning a mutable dictionary and asking callers to leave it alone.

A failed configuration update leaves the old snapshot intact. The revision advances only for a material change. An exact repeated valid configuration therefore does not invalidate pending work under this reference policy.

The render method captures the session, generation, revision, settings, and text before awaiting synthesis. After the await, it checks current state before calling a synchronous local submit callback. There is no asynchronous gap between that check and this example's admission boundary.

The single-event-loop assumption matters. This reference is not thread-safe across arbitrary threads or processes. If the real submit function awaits capacity, performs asynchronous conversion, or queues through another owner, that boundary needs its own current-state check and ordered ownership contract.

The provider probe returns the request itself so the harness can test routing identity. A production adapter must validate real result metadata and media format. The printed success explicitly disclaims live provider and audio validation.

No expiry or queue is modeled here. A production queue must distinguish expired, empty, closed, stale, and overloaded states when recovery differs. The backpressure contract supplies those additional boundaries rather than hiding them in this small example.

Test adversarial interleavings

A concurrency test should force the ordering that would expose a leak. The harness starts call A, holds it at an explicit event, runs call B with another voice, then releases A. A sleep-based test can pass merely because the race did not happen during that run.

Python's asyncio task documentation describes scheduling and cancellation behavior. The important application property is still explicit: a suspended request must retain its own settings and must be revalidated when it resumes.

Test both directions. A starts first and completes last; B starts first and completes last. Include repeated requests in one session and simultaneous requests in different sessions. The expected result should name which generation is allowed, not merely count successful callbacks.

Conceptual interleaving cards cover a waiting call, revised settings, closed calls, and late callbacks

Mutate the original intake dictionary after constructing the session. Then try to mutate the exposed settings value and rebind the property. Each test covers a different alias path, so one passing copy test does not substitute for all of them.

For nested settings in a real adapter, mutate a nested list or dictionary through every retained caller alias. Verify that a dispatch uses the original approved value or rejects the unsupported shape. A read-only outer object can still contain mutable inner state.

Change settings while synthesis waits. The old result should receive the documented stale-settings disposition. Then start a new render under the new revision and verify it uses the new snapshot. Rejecting old output is only half the behavior; current work must still proceed.

Start a second response in the same session while the first waits. Under the reference's latest-generation policy, the old response becomes stale even if both use identical settings. This distinguishes response cancellation from configuration ownership.

Close the session while synthesis waits. The late callback must not submit anything, and a later configure or retry must not reopen the session. The shutdown semantics guide explains why terminal state needs an explicit rule.

Conceptual final check after synthesis accepts only a still-current request and sends stale work to rejection

Test exceptions and task cancellation at the integrated adapter. A failed synthesis should not return a pooled object with another call's callbacks still attached. A canceled task should release only the resources it owns and preserve any required cleanup under the runtime's cancellation rules.

The shared-batch cancellation article covers a related shared-resource boundary. One call's cancellation should not cancel unrelated requests merely because they share a connection or execution resource.

Include unsupported configuration values, unknown keys, nonfinite rates, booleans where numbers are expected, and exact policy boundaries. Validation should fail before dispatch, so invalid input cannot partially mutate a live session or contaminate a pooled resource.

The deterministic harness should run alongside an actual provider contract test where authorized. The provider test can verify public parameter mapping, returned format, cancellation behavior, and callback routing. Keep those observations separate from the local reference proof.

Include the output callback's ownership in the test. An isolated synthesis request can still route its result into another call's writer if a shared callback field was rebound. Capture the intended local consumer with the request, or pass the request through an owner that validates its session identity before accepting it.

The callback should report its real boundary. If it places bytes into an asynchronous queue, the render method's synchronous return proves enqueue acceptance only. The consumer still needs a final generation, revision, age, and clear-barrier check immediately before ordered submission to its own output interface.

A held chunk can become obsolete while the queue appears empty. Empty storage does not mean the consumer has no in-flight value. Keep held state in the lifecycle model, and reject an obsolete generation before applying expiry side effects that could clear or cancel the newer response.

When a queue drains, preserve generation-wide ordering history. Removing the last queued item should not erase the last accepted creation position. Otherwise an older same-generation arrival can be accepted after the drain and spoken out of order despite every individual chunk carrying a valid voice snapshot.

An equal-time arrival needs an explicit tie rule. Use the source's declared sequence identity or reject ambiguous ordering rather than letting incidental callback order decide. Invalidation can reset that history for the next generation, provided old callbacks remain unable to submit under the new one.

Those queue rules are outside the published reference's scope, but they are concrete integration obligations. Keep them in the adapter's test protocol when the real submission boundary includes buffering. A small correct snapshot example should not be stretched into a claim about a queue it does not implement.

Reuse resources without sharing call state

A shared resource is safe only under its documented concurrency contract. Some clients accept independent request parameters. Others expose mutable instance settings and require serialized use or separate instances. Read current official documentation before deciding what to pool.

Microsoft's server synthesis sample demonstrates a provider-specific pooling approach. It is evidence that lifecycle and resource reuse need deliberate handling, not a universal instruction to share one mutable synthesizer among all calls.

A historical iOS synthesis concurrency question asks about shared system behavior. It supplies a real developer concern, while the answer's platform details require current verification before use. Do not infer one platform's concurrency model from another's object names.

Prefer the smallest supported ownership model. An immutable request passed to a documented concurrent client is simple. If the client requires exclusive instance use, lease it, apply the full resolved configuration, detach callbacks, and return it only after the documented terminal boundary.

A lock around settings mutation alone can be insufficient. If the SDK reads those settings later, releasing the lock before the read allows the next call to overwrite them. The protected lifetime must match the documented consumption boundary, or the adapter should avoid shared mutable settings entirely.

A pool key must include material resource constraints. Different endpoints, permissions, output formats, or voice-specific capabilities can require separate resources. Keep tenant and authorization scope explicit rather than allowing a convenient pool key to merge incompatible callers.

The cache-key guide provides similar collision reasoning. Reuse is safe only when the key captures the required compatibility contract; a fast cache hit or pool checkout does not prove isolation.

Conceptual comparison distinguishes shared mutable state, a dynamic read-only view, and an owned stable snapshot

Here ✓ means the approach supplies the stated property under its scope. ✗ means it does not. The table compares ownership strategies and makes no throughput claim.

Property Shared mutable dictionary Read-only view of caller data Owned immutable snapshot
Caller cannot mutate through exposed view ✗ Same object ✓ View blocks writes ✓ Supported values frozen
Another alias cannot change approved values ✗ Shared ownership ✗ Dynamic view ✓ Copied supported shape
Pending result is rejected after revision ✗ Needs lifecycle gate ✗ Needs lifecycle gate ✗ Still needs lifecycle gate
Terminal session cannot restart ✗ Separate state required ✗ Separate state required ✗ Separate state required
Exact request meaning can be reproduced ✗ Can change after logging ✗ Can change through alias ✓ With retained snapshot and revision

Context-local state can help tracing, but it does not make a mutable object immutable. Python contextvars provides context-local bindings for asynchronous code. If several contexts reference the same mutable dictionary, application ownership still matters.

Use context variables for appropriate correlation data under the framework's documented propagation rules. Pass approved voice values explicitly at the adapter boundary so a provider request can be reviewed without reconstructing an implicit execution context.

Avoid adding a generic configuration framework to solve one leak. A small validated immutable value and a clear lifecycle check often cover the complete requirement. Expand only when the existing supported value shape or resource contract demands more.

A provider wrapper should have one explicit mapping from the application's immutable voice value to the provider request. Review that mapping for omitted fields, defaults, and precedence. A correct snapshot is ineffective if the wrapper reads a global voice name while ignoring the snapshot argument.

If the SDK exposes mutable instance state, determine when it consumes that state. The boundary can be construction, request invocation, stream start, or later internal work under the documented interface. Where documentation is unclear, use an authorized contract test and preserve the uncertainty instead of assuming the earliest convenient boundary.

A pool lease should own its callback registrations. Detach or reset them according to the SDK contract before returning the instance. A stale callback can route a later request to a previous session even when the voice settings themselves were fully reset.

Do not return a failed instance to the pool merely because the application caught an exception. The resource may require disposal or another documented recovery. Keep the failure disposition and resource lifecycle separate from the caller's conversational fallback.

When an instance is reused, apply the complete resolved setting set rather than only changed fields unless the provider explicitly supports a safe delta contract. A field omitted by the next caller can otherwise retain the previous caller's language, style, or output format.

Prefer request-local parameters where the public interface supplies them. They make the mapping easier to inspect and reduce the lifetime of mutable client configuration. This is a boundary choice, not a universal performance claim about client pooling.

Trace requested and observed settings

A useful trace records the non-sensitive session correlation, response generation, configuration revision, resolved voice values, selected adapter, and result disposition. Keep customer identifiers and credentials out of the public example and apply the application's retention policy to real traces.

Requested and observed values should not overwrite one another. If the provider returns an actual voice or model identity, preserve it beside the request. If the interface supplies no such metadata, mark the field unavailable rather than claiming the requested value was observed.

Conceptual requested, resolved, and observed configuration remain linked by the same request identity

A configuration digest can help correlate repeated settings, but retain the supported raw values where policy permits reproduction. A digest alone cannot tell an engineer which locale or rate was used. Do not log secret-bearing provider objects to make the record more complete.

The observability guide helps attach those values to the right span and media event. A single request-success event cannot reveal whether a late result was rejected, whether a fallback changed the voice, or whether local output accepted the result.

Name the disposition precisely. Submitted locally means the synchronous callback accepted the current request under this reference contract. It does not mean endpoint playback, acoustic audibility, intelligibility, or successful task completion.

The playback evidence article defines the stronger delivery boundaries. Keep those observations attached to the same request and generation so a late acknowledgment cannot be mistaken for the newer response.

When a mismatch appears, inspect the earliest divergence. Intake can be correct while resolution is wrong; resolution can be correct while SDK parameter mapping is wrong; dispatch can be correct while callback routing is wrong. Capture enough boundary evidence to choose the smallest repair.

Do not infer a voice leak only from an unusual sound. Rate changes, clipping, output gain, codec handling, and language-specific prosody can alter perception. The voice consistency method provides a controlled listening path after deterministic routing checks pass.

Keep failure counters by disposition. Stale-settings rejection can be expected after a deliberate change. A sudden increase in unexpected fallback or result-identity mismatch needs investigation. Combining those classes into a generic error count obscures the behavior the gate protects.

A production incident packet should preserve the adapter version, settings policy, deterministic interleaving reproduction, and restricted trace boundaries. The public regression can use synthetic call identities and values while still reproducing the ownership failure.

Roll out the smallest complete repair

A complete repair reaches every reader of the settings and every consumer of late results. Update the shared adapter, its request contract, cache or pool compatibility where affected, and the tests that exercise the actual boundary. Do not stop after copying the intake dictionary if the SDK later reads another global object.

The regression testing guide helps retain the failing-before interleaving. A deterministic event-controlled reproduction should fail the original implementation and pass the repaired one without weakening its expected configuration or terminal outcome.

Run the narrow ownership harness first. Then test the real adapter's documented parameter mapping and callback lifecycle. Broaden to concurrent workloads when shared resource behavior changed, using authorized synthetic traffic and the bursty load method.

A local snapshot can increase per-request allocation slightly. Measure operating cost if it matters, but do not trade away ownership based on an unmeasured concern. Keep the supported shape small and reuse immutable values where the application already has that pattern.

For live rollout, use the existing release mechanism. Monitor configuration mismatches, stale-result dispositions, unexpected fallbacks, and per-call completion. A rollback should preserve validation boundaries that prevent malformed or cross-scope settings from reaching the provider.

Recheck capacity separately from correctness. An isolated client instance per call may be correct but too expensive under the target workload. A shared client may be efficient but unsupported for concurrent mutation. The cold-versus-warm capacity guide helps measure the resource decision without weakening the isolation invariant.

For 60db Agent, verify public interface behavior with two controlled concurrent calls and distinct supported settings. Capture which values were requested and which evidence is available afterward. No private implementation disclosure is needed to demonstrate the isolation boundary.

The release note should say what was proved: owned supported settings, no cross-call mutation under forced interleavings, stale revision rejection, and terminal session behavior. List live provider, device, or acoustic checks separately when they were not performed.

A practical provider contract test can use two distinct supported voices and clearly synthetic text labels, then capture request metadata and the routed output separately. The primary assertion is ownership: every callback and output artifact belongs to the call that created it. Listening can add evidence about audible selection, while returned metadata and deterministic routing remain separate observations.

Repeat the test with one call canceled and one allowed to complete. Then reverse their completion order. A shared connection should not make the canceled call's cleanup detach the active call's callbacks or clear its media. Preserve the exact completion schedule so a future SDK update can replay the same boundary.

Use the admission-control policy to distinguish waiting for a resource from acquiring permission to produce stale output. A capacity lease can be valid while the conversation generation has changed. Acquiring the resource therefore requires a current-state check before dispatch and another after any later wait.

The waiting-speech guide addresses a related conversational concern. A progress utterance must use its own current configuration and truthful state. It should not inherit a pending request's voice object or imply that synthesis completed while the provider is still waiting.

For migration, inspect all construction sites and exports of the affected settings object. A new immutable type helps only when old callers stop sharing the mutable path. Keep a short caller inventory and remove obsolete mutation helpers once the accepted callers use the owned contract.

A regression report should retain the original failing interleaving and the repaired result with the same expected output identities. Do not replace the hard concurrency case with a serial smoke test because it is easier to run. The event-controlled boundary is the reason the fix exists.

Finally, keep the acoustic claim modest. Passing the local harness means the supported configuration and current-state admission rules behaved as asserted. It does not establish pronunciation, voice quality, live-provider fairness, or device playback. Those are separate tests with their own evidence and appropriate scope.

During review, trace one request through intake, resolution, dispatch, completion, and local admission. At each boundary, compare its session, generation, revision, and supported voice values. The exercise should reveal where ownership changes and where the adapter must copy or revalidate, without requiring a new framework or private runtime disclosure.

Keep the caller inventory with the repair report rather than embedding task labels in production code. The code should state the invariant through its types and checks. The report can explain which historical path violated it, which test forced the failure, and which interfaces still need live-provider verification.

After deployment, an observed mismatch should create a reproduction task using the same synthetic interleaving. Preserve the real trace under its access policy, then publish only the generic failure shape. That keeps the regression useful without exposing customer settings or treating an incident artifact as public documentation.


Frequently asked questions

Why can voice settings leak between calls?

A shared mutable object can be changed after one call logs its settings but before the adapter consumes them. Another call then controls the eventual request. Trace every settings reader and create an owned validated snapshot before asynchronous dispatch.

Is a shallow dictionary copy enough?

Only when every supported value is immutable and the shape is flat. Nested lists, dictionaries, or provider objects can remain shared through aliases. Copy and freeze the supported shape explicitly, or reject unsupported nested fields rather than implying they are safely isolated.

Does a read-only mapping prevent alias mutation?

A dynamic read-only view prevents writes through that view, while another owner can still mutate the backing mapping. Own the approved values first. The reference uses immutable scalar fields and tests both mutation attempts and property rebinding.

Why recheck state after synthesis completes?

The call can close, settings can change, or a newer response can supersede the old one while synthesis waits. A successful provider result describes completed work, not current permission to play it. Check session, generation, and revision before local submission.

Can an immutable snapshot prevent canceled playback from restarting?

It preserves the request's meaning, but terminal lifecycle state still needs a gate. The reference checks closed state after awaiting and refuses later renders or configuration changes. Late historical results must not reopen a canceled or closed session.

What does submitted locally mean?

It means the synchronous callback accepted the still-current request at the reference boundary. It does not establish endpoint playback, audibility, comprehension, or business success. Preserve stronger delivery observations separately and attach them to the same request identity.

Is the synthetic probe a real speech SDK?

No. It returns a request record so the harness can force and inspect interleavings deterministically. A real adapter must map public provider parameters, validate results and media formats, and test the documented resource and cancellation lifecycle.

Why use events instead of sleeps in concurrency tests?

Events force the exact order that exposes the race. Sleeps depend on scheduling luck and machine speed. Hold one request, run the conflicting operation, then release it and assert the precise configuration or stale disposition expected by the contract.

Should every call have a separate client instance?

Follow the client's documented concurrency and configuration model. Independent request parameters can support safe sharing. Mutable instance settings can require exclusive leases or separate instances. Measure resource cost after preserving the required ownership boundary.

Does a lock around configuration changes solve the problem?

Only if its protected lifetime covers the actual settings consumption boundary. If the SDK reads settings after the lock is released, another call can overwrite them first. Prefer owned request values when supported, or lease the mutable instance for its documented lifecycle.

Can context variables replace explicit settings arguments?

They can carry context-local correlation, but they do not recursively freeze mutable values or prove provider consumption. Pass approved voice settings explicitly at the adapter boundary. Use context propagation according to the framework's current documented behavior.

What should an invalid configuration update do?

Reject it before replacing the current snapshot or advancing the revision. The reference validates a candidate first, so a failure leaves the previous approved state intact. Avoid partial updates that combine a new voice with an old language or unsupported rate.

Why distinguish generation from settings revision?

A new response can supersede an old response without changing voice settings. A settings update can invalidate pending work inside the same call. Separate identities let the adapter reject the correct kind of stale result and report why it was rejected.

Should an exact repeated configuration invalidate work?

Define the policy explicitly. The reference advances the revision only for a material settings change, so an exact valid repeat preserves pending work. Another application can choose otherwise, but its tests, dispositions, and documentation should agree.

What if the provider silently chooses a fallback voice?

Record returned metadata when available and keep it separate from requested and resolved settings. If the provider supplies no evidence, mark the actual choice unavailable. Test supported fallback behavior instead of assuming the requested voice was observed.

How should cached audio handle voice settings?

Its compatibility key must include the material voice and context contract, including relevant scope and policy. An isolated request can still receive wrong audio from an incomplete cache key. Test collisions and cross-call reuse alongside the adapter ownership repair.

Does this reference handle queue expiry?

No queue or expiry policy is modeled. A production output queue needs explicit empty, expired, stale, closed, and overload outcomes where recovery differs. Recheck age and clear barriers after waiting, and do not let an old generation alter newer state.

Can a perceived voice change prove a configuration leak?

It is a symptom that needs evidence. Rate, gain, clipping, codec behavior, and prosody can alter perception too. First compare requested, resolved, and observed settings and callback identity, then use a controlled listening corpus for audible dimensions.

What should be kept out of configuration traces?

Credentials, secret-bearing SDK objects, real account identifiers, and unnecessary personal data. Retain non-sensitive supported values and correlation sufficient for authorized reproduction. Public examples can use synthetic sessions and settings without exposing customer or provider secrets.

What is the smallest complete isolation fix?

An owned validated request value, correct result routing, post-await current-state checks, and deterministic negative interleaving tests at the shared adapter. Include affected cache or pool compatibility. Expand only when the actual supported resource contract needs more machinery.

Make the request own its voice

Resolve supported settings once, copy them into an immutable value, and dispatch that exact snapshot. Keep session identity, response generation, configuration revision, and terminal state separate because they invalidate work for different reasons.

Run the event-controlled harness before relying on ordinary concurrent traffic. Force mutation, revision, supersession, and close during a wait. Then verify the real adapter's public parameter and lifecycle contract under authorized synthetic calls.

Take the shared settings object nearest your synthesis boundary and trace every reader. Replace the proven alias path with the smallest owned value your application supports, retain the failing interleaving, and report exactly which submission boundary the test establishes.