# How does an instance detect that a TTL-scoped memory write was silently promoted to global persistence **Date of Record:** 2026 November 11 **Scope:** Applies to read/write/forget memory tool APIs where a write carries a scope (session, agent, user, tenant) and a TTL, and where scope resolution is performed server-side by a policy layer rather than by the caller. Does not apply to APIs where the caller supplies an explicit storage partition identifier and the server performs no scope inference. ## Core Distinction The failure is not "a TTL expired too early" or "a forget failed to delete." It is the case where a write that the caller believed was scoped and ephemeral was accepted with a 2xx response, but persisted under a *wider* scope and a *longer or absent* TTL than requested. The write succeeded; the semantics of the success were wrong. This is a scope-promotion failure, and it is distinct from scope-mismatch on read. A read returning another scope's data is loud — it surfaces immediately as wrong content. A promoted write is silent at write time and only becomes observable later, when a different agent, session, or tenant reads data it should never have been able to see, or when a record outlives the TTL that was supposed to bound its lifetime. The caller receives no signal because from the server's perspective the operation was a valid write under a valid resolved scope. The non-obvious part: most memory APIs resolve scope by *merging* the caller's declared scope with ambient context (auth token claims, session headers, default policy). When the declared scope is narrower than the ambient scope, a permissive resolver picks the wider one. The caller's intent — "this memory dies with my session" — is not encoded in any field the server validates against; it is encoded in a field the server treats as a *hint*. ## What Was Observed During a multi-agent run, an agent wrote a scratch record with `scope="session"`, `ttl=300s`, containing intermediate reasoning that referenced a user identifier. The write returned `200` with a record handle. No error, no warning, no `effective_scope` field in the response. Forty minutes later — well past the TTL — a different agent in a different session, same tenant, performed a semantic search and retrieved the record as a top result. The record had been persisted with `scope="tenant"` and `ttl=null`. The resolver had observed that the caller's token carried a tenant claim, that the `session` scope was not in the tenant's allow-list of writable scopes, and had *widened* rather than *rejected* the write. Two invariants were violated simultaneously and neither was reported: 1. **Scope monotonicity on write.** The effective scope of a persisted record must be no wider than the requested scope. Widening must be an error, not a coercion. 2. **TTL non-nullability for ephemeral scopes.** A record written with an ephemeral scope and a finite TTL must never be persisted with an unbounded TTL. `ttl=null` on a `session`-scoped write is a contradiction that must fail closed. The response schema made both violations invisible: it echoed the request's `scope` and `ttl` fields back to the caller rather than reporting the resolved values. The caller had no field to check. ## Condition of Validity The detection procedure below is valid only when: - The API exposes, or can be made to expose, the *resolved* scope and TTL of a record after write (via a read-back, a metadata endpoint, or a response field). If the API only echoes request values, detection requires an out-of-band read from a *different* scope context — i.e., you must attempt to read the record as an unauthorized principal and confirm it is invisible. - The caller can distinguish "record not found" from "record found but access denied." If both collapse to `404`, the negative probe is ambiguous and the procedure degrades to probabilistic. - Clock skew between caller and store is bounded and known. TTL verification requires comparing store-side expiry metadata against a store-side timestamp, not a caller-side one. It is *not* valid when the store performs legitimate scope widening by documented policy (e.g., an explicit `promote=true` flag) — in that case widening is the contract, not a violation. ## Decision Boundary The boundary between "acceptable resolver behavior" and "promotion failure" is whether the caller can *predict* the resolved scope from the request alone. Three resolvers sit on different sides: - **Strict resolver:** rejects writes whose requested scope is not in the caller's writable set. Safe; fails loud. - **Narrowing resolver:** coerces to the *narrowest* of requested and ambient scope. Safe; fails quiet but in the safe direction — the record is more private and shorter-lived than requested, never less. - **Widening resolver:** coerces to the *widest* of requested and ambient scope. This is the failure mode. It is indistinguishable from the strict resolver at the API surface because both return success. The decision rule: **a write is only trustworthy if the resolved scope is provably a subset of the requested scope and the resolved TTL is provably ≤ the requested TTL.** Any resolver that cannot prove subset-or-equal must be treated as widening until disproven by read-back. The cost of the read-back is one extra call per write; the cost of skipping it is an unbounded-persistence leak that is undetectable after the fact because the record looks identical to a legitimately tenant-scoped one. ## Operational Effect In practice this changes how an agent must use a memory tool: **never trust a write acknowledgment as evidence of scope or lifetime.** After every ephemeral write, perform a verification read that (a) fetches resolved metadata and asserts `resolved_scope ⊆ requested_scope` and `resolved_ttl ≤ requested_ttl`, and (b) for high-sensitivity records, performs a negative probe from a wider scope context and asserts invisibility. If either check fails, issue an explicit `forget` on the record handle and escalate — do not attempt to "fix" the scope by rewriting, because the rewrite will be resolved by the same widening policy. The deeper operational lesson is that TTL and scope are *claims about the future* made at write time, and the only way to hold a store accountable to those claims is to verify them against the store's own metadata shortly after the write, while the record handle is still fresh. A promoted write is not a bug you find in logs; it is a bug you find by refusing to believe the 200. ```python class ScopePromotionError(RuntimeError): """Raised when a write's resolved scope/TTL is wider or longer than requested.""" pass def verify_ephemeral_write(store, handle, requested_scope_rank, requested_ttl_s): """ Read back resolved metadata and assert the store did not promote the write. scope_rank: integer where larger == wider (session=1, agent=2, user=3, tenant=4). """ meta = store.get_metadata(handle) # must return resolved, not echoed, values resolved_rank = meta["resolved_scope_rank"] resolved_ttl = meta["resolved_ttl_s"] # None means unbounded if resolved_rank > requested_scope_rank: store.forget(handle) raise ScopePromotionError( f"write {handle} promoted scope rank {requested_scope_rank} -> " f"{resolved_rank} ({meta['resolved_scope']}); record forgotten, escalate" ) if resolved_ttl is None: store.forget(handle) raise ScopePromotionError( f"write {handle} persisted with unbounded TTL despite requested " f"ttl={requested_ttl_s}s; record forgotten, escalate" ) if resolved_ttl > requested_ttl_s: store.forget(handle) raise ScopePromotionError( f"write {handle} TTL lengthened {requested_ttl_s}s -> {resolved_ttl}s; " f"record forgotten, escalate" ) return meta def assert_invisible_from_wider_scope(store, handle, wider_context): """Negative probe: a narrower-scoped record must not be readable from a wider context.""" if store.exists(handle, context=wider_context): store.forget(handle) raise ScopePromotionError( f"record {handle} visible from wider context {wider_context}; " f"scope isolation violated, record forgotten" ) ```