The persistence seam is now create/open/stat/list returning per-session SessionHandles (read/append/flush/close); every log read and write flows through the owning handle. The seam package exports only the service and handle contracts, consumer-visible errors, and pure durable-data validation helpers; each backend owns its complete storage runtime, and the shared contract suites pin equivalent observable behavior. The backend routes published sessions' live events by id into the active write handle; agent-loop only acquires, seeds, and closes the handle. Resume appends interruptedTurnClosers through its write handle; session-query owns the revision-keyed cold cache. Legacy-only surfaces are removed in the same swap: locate/readRaw/supportsRawArtifacts, the legacy event-shape read migration, zstd torn-frame salvage, DSH_SESSION_JSONL, and hook transcript_path population; a torn final zstd frame is discarded whole; the session-list cold blank probe returns on stat metadata (eventCount derived from the last physical row, sizeBytes). The WebUI ZIP export serializes the logical log from a read handle, so both backends export identically. Refs #3245
3.8 KiB
Agent Note: Bound cold blank-session verification
Status: implemented
English | 中文
Problem
The Web session tree hides blank Sessions and reuses the selected blank entry as New Session. Attached Sessions can derive blankness from their in-memory event log, but session.list normally avoids loading every cold log. Treating every materialized cold Session as non-blank exposes empty Sessions left by older versions. Treating a projection-cache blank: true as current can instead hide a real conversation after the log advances and the fail-soft cache remains stale.
The same cold list used the JSONL artifact mtime for updatedAt. Opening a Session appends session/end-seed, so a pickup with no human prompt refreshed mtime and promoted that Session above recently used conversations.
Decision
dsh-api-session-controller registers sessionListMetadata, a projection containing blank and lastPromptAt. The attached summary folds the same functions directly over the live log. blank changes only from true to false on turn/start; lastPromptAt changes only on a user/message whose source kind is user.
A cold summary trusts cached blank: false, because a checkpoint prefix containing turn/start remains non-blank. Cached blank: true and a cache miss do not prove the current log is blank and are served blank: false, keeping the Session visible. The earlier physical-size probe — a locate() path plus a coldBlankProbeMaxBytes eligibility threshold gating an exact readFrom(id, 0) fold — is removed with the seam's path query (export and pre-release trims); persistence snapshot metadata (eventCount/sizeBytes on stat()/list()) is the reintroduction path for exact cold verification.
updatedAt is the later of createdAt and lastPromptAt. A cache miss or stale checkpoint orders the Session too old rather than promoting it from an unrelated file write.
Alternatives considered
Trust cached blank: true. Rejected because the projection cache deliberately permits a persisted log to advance beyond its checkpoint. A crash or fail-soft write failure after the first turn/start would hide a real conversation and could make the client reuse it as New Session.
Read every cold log. Rejected because list latency and I/O would scale with total stored conversation bytes; unverified cold entries degrade toward visibility instead.
Store blankness and recency in an authoritative persistence index. Deferred because the shipped JSONL provider has an immutable first line and would require a second durable artifact with ordered updates. An out-of-tree provider may use its own index only with defined update atomicity, versioning, and recovery. The broader exact-index design remains in the last-activity proposal.
Continue ordering JSONL by mtime. Rejected because mtime records every artifact write, including pickup boundaries, rather than the latest human prompt. Its error direction promotes untouched Sessions to the front.
Consequences
A stale cache cannot hide a stored turn/start, and a cold list performs no artifact I/O: cold rows are served from cached projections only. Blank cold Sessions without a cached non-blank projection remain visible, and missing or delayed recency cache entries fall back to createdAt. These are conservative degradations: the UI may show an extra empty row or order a Session too low, but it does not hide a conversation or promote one because it was merely opened.
The gateway-owned projection is an effect of the gateway fiber; unloading the gateway removes the key. Unit coverage pins stale-true rejection, monotonic false reuse, cache-miss visibility, human-prompt recency, and fiber disposal.