Skip to content

symfonic.capabilities.human.threads

threads

The thread id a pause is bound to, derived in one place (HK2, TA8.35).

TA8.19 recorded session_id's role 2 -- checkpointer thread / key -- as absent on the kernel route, and named the legacy derivation it was absent relative to: SymfonicAgent._thread_id_for, whose own docstring calls itself the "SINGLE derivation site". It was single across three legacy consumers and the kernel route had none, so closing the role meant either deriving the key a second time here or making the one derivation reachable from both routes.

A second derivation would have been the worse defect of the two. The key is what a durable record is filed under: a kernel route that derived tenant:sub:session with a different separator, a different absent-sub placeholder, or a different order would write checkpoints the legacy route cannot read and read none of the ones it wrote, and nothing would raise -- paused runs would simply stop being findable when a deployment flipped. So the formula moves here, where a capability that may not import the engine can reach it, and SymfonicAgent._thread_id_for delegates.

session_id is load-bearing in the strict sense TA8.19 asks for: it is a component of the returned key, so two turns differing only in session_id file their checkpoints under different keys and neither can read the other's.

thread_id_for

thread_id_for(scope: Any, session_id: str) -> str

tenant:sub:session -- the legacy formula, character for character.

Read structurally, like every other scope reader in this package: the tenancy scope type lives outside this layer and importing it would make the capability depend on the engine it is being extracted from.

A missing tenant_id or session_id is refused rather than folded into an empty string. ":_:" and "acme:_:" are perfectly valid-looking keys that every unattributed caller would share, which is the same failure :func:~symfonic.capabilities.human.binding.hash_scope refuses one layer over -- and here it would be a durable one, since the checkpoints filed under such a key outlive the run that wrote them.

Source code in src/symfonic/capabilities/human/threads.py
def thread_id_for(scope: Any, session_id: str) -> str:
    """``tenant:sub:session`` -- the legacy formula, character for character.

    Read structurally, like every other scope reader in this package: the
    tenancy scope type lives outside this layer and importing it would make the
    capability depend on the engine it is being extracted from.

    A missing ``tenant_id`` or ``session_id`` is refused rather than folded into
    an empty string. ``":_:"`` and ``"acme:_:"`` are perfectly valid-looking
    keys that every unattributed caller would share, which is the same failure
    :func:`~symfonic.capabilities.human.binding.hash_scope` refuses one layer
    over -- and here it would be a *durable* one, since the checkpoints filed
    under such a key outlive the run that wrote them.
    """
    tenant_id = getattr(scope, "tenant_id", None)
    if not tenant_id:
        raise InteractionConfigurationError(
            "cannot derive a checkpoint thread id from a scope with no "
            "tenant_id; every unattributed run would share one thread and read "
            "back each other's paused state"
        )
    if not session_id:
        raise InteractionConfigurationError(
            "cannot derive a checkpoint thread id with no session_id; the "
            "session is what separates one conversation's paused state from "
            "the next one's within the same tenant"
        )
    sub = getattr(scope, "sub_tenant_id", None) or ABSENT_SUB_TENANT
    return f"{tenant_id}:{sub}:{session_id}"