What resource-leak invariants must survive session termination?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session allocates external resources (file handles, database connections, network sockets, GPU memory, API quotas) and terminates, and the session must ensure all resources are explicitly released, not just abandoned. Does not apply to sessions running in fully containerized environments where the runtime guarantees resource cleanup on termination, or purely computational sessions with no external resources.

Core Distinction

The separation between **resource release** (the session's process exits) and **resource cleanup verification** (the external system confirms the resources are actually released). Relying on process exit creates the **Zombie Resource Leak**: the session terminates, the process is gone, but external resources remain allocated (database connections open, file locks held, API quotas consumed) until an external watchdog cleans them up, causing resource exhaustion for successor sessions.

What Was Observed

In resource-allocating agent architectures, failing to verify cleanup produces three primary failure modes:

  1. **Connection Pool Exhaustion:** The session opens 100 database connections and terminates without closing them. The database's connection pool is exhausted, and the successor cannot connect.
  2. **File Lock Orphans:** The session holds file locks in a shared filesystem and terminates without releasing them. Successor sessions block indefinitely on the locked files.
  3. **API Quota Leakage:** The session acquires API quota leases and terminates. The successor cannot acquire the same quota because the leases have not expired, and the session's termination did not trigger lease release.

Recorded Elements

  1. **`resource_allocation_manifest`:** The complete list of external resources allocated by the session (connection IDs, file handles, quota lease IDs), updated in real-time as resources are acquired.
  2. **`release_receipt_per_resource`:** For each allocated resource, a cryptographic proof of its release (e.g., signed connection close receipt, file lock release notification, quota lease revocation).
  3. **`resource_release_completion_receipt`:** The cryptographic proof that all resources in the manifest have been released, issued by a trusted cleanup verifier (external watchdog, resource manager, or database).
  4. **`leak_quarantine_receipt`:** For resources that could not be released (e.g., crashed external system), the exact record allowing a successor to force-release them.

Condition of Validity

* **Applies when:** The session allocates external resources, the resources can outlive the session if not explicitly released, and successor sessions could be blocked by leaks. * **Does not apply when:** The runtime environment guarantees resource cleanup on termination (e.g., container kill terminates all connections), or the session allocates only resources that are inherently released on process exit (local memory).

Decision Boundary

  1. **Does the session allocate external resources?** If no, standard termination applies. If yes, proceed.
  2. **Is the `resource_allocation_manifest` maintained in real-time?** If no, the session cannot know what to release. The session must maintain the manifest or fail-closed.
  3. **Are all resources in the manifest covered by `release_receipt_per_resource`?** If no, the session must explicitly release them before termination.
  4. **Conversely, when can a session skip resource release verification?** Only when the runtime environment guarantees cleanup (container termination, kernel-enforced process teardown), or when the resources are strictly ephemeral with kernel-enforced timeouts shorter than the successor's expected start delay.

Operational Effect

  1. **Zombie Prevention:** Verifies resource release before termination, preventing successor blocking.
  2. **Leak Attribution:** Provides a complete resource manifest, allowing identification of which session leaked which resource.
  3. **Deterministic Cleanup:** Forces explicit release verification, enabling successors to trust resource availability.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class ResourceVerdict(Enum):
    NO_EXTERNAL_RESOURCES = auto()
    CLEANUP_VERIFIED = auto()
    LEAK_DETECTED = auto()
    QUARANTINE_REQUIRED = auto()

@dataclass(frozen=True)
class AllocatedResource:
    resource_id: str
    resource_type: str
    release_receipt: str | None
    is_force_releasable: bool

@dataclass(frozen=True)
class SessionTermination:
    session_id: str
    allocated_resources: tuple[AllocatedResource, ...]
    runtime_guarantees_cleanup: bool

@dataclass(frozen=True)
class ResourcePolicy:
    require_cleanup_verification: bool

@dataclass(frozen=True)
class ResourceManifest:
    verdict: ResourceVerdict
    unreleased_count: int
    directive: str
    manifest_digest: str

class ResourceLeakGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_termination(
        cls, state: SessionTermination, policy: ResourcePolicy
    ) -> ResourceManifest:
        
        # Gate 1: No resources
        if not state.allocated_resources:
            digest = cls._hash(json.dumps({
                "session": state.session_id,
                "resources": "NONE"
            }, sort_keys=True))
            return ResourceManifest(
                verdict=ResourceVerdict.NO_EXTERNAL_RESOURCES,
                unreleased_count=0,
                directive=f"PROCEED: Session {state.session_id} allocated no external resources.",
                manifest_digest=digest
            )

        # Gate 2: Runtime cleanup guarantee
        if state.runtime_guarantees_cleanup:
            digest = cls._hash(json.dumps({
                "session": state.session_id,
                "resources": str(len(state.allocated_resources)),
                "cleanup": "RUNTIME_GUARANTEED"
            }, sort_keys=True))
            return ResourceManifest(
                verdict=ResourceVerdict.CLEANUP_VERIFIED,
                unreleased_count=0,
                directive="PROCEED: Runtime guarantees cleanup.",
                manifest_digest=digest
            )

        # Gate 3: Explicit release verification
        unreleased = [r for r in state.allocated_resources if not r.release_receipt]

        if not unreleased:
            digest = cls._hash(json.dumps({
                "session": state.session_id,
                "released": str(len(state.allocated_resources))
            }, sort_keys=True))
            return ResourceManifest(
                verdict=ResourceVerdict.CLEANUP_VERIFIED,
                unreleased_count=0,
                directive=f"PROCEED: All {len(state.allocated_resources)} resources explicitly released.",
                manifest_digest=digest
            )

        # Gate 4: Leak handling
        non_forceable = [r for r in unreleased if not r.is_force_releasable]
        if non_forceable:
            return ResourceManifest(
                verdict=ResourceVerdict.LEAK_DETECTED,
                unreleased_count=len(unreleased),
                directive=f"HALT: {len(non_forceable)} unreleased non-forceable resources. Successors will block.",
                manifest_digest=cls._hash(f"LEAK:{len(non_forceable)}")
            )

        digest = cls._hash(json.dumps({
            "session": state.session_id,
            "unreleased": tuple(sorted(r.resource_id for r in unreleased))
        }, sort_keys=True))
        
        return ResourceManifest(
            verdict=ResourceVerdict.QUARANTINE_REQUIRED,
            unreleased_count=len(unreleased),
            directive=f"ACTION: {len(unreleased)} resources left unreleased. Recorded for successor force-release.",
            manifest_digest=digest
        )