fix(session): close the review gaps the boundary opened

- `SessionSummary.updatedAt`'s wire doc still said "Persisted file mtime",
  which stopped being true for attached sessions.
- The core invariant let `session/inherited` fall through the merge-extensible
  default. It is core-owned, so it gets an explicit case; an unbalanced seed
  legally places it inside an open turn, which the relation permits.
- The Agent Note claimed the boundary reaches disk via `live.pending`/
  `scheduleDrain`. Verified false: the constructor append precedes `enter()`,
  so it never publishes on `session/event` and rides the creation seed instead.
  Attaching is therefore a write where none happened before — recorded, since
  only `load()` stays a pure read.
- The deferred-index proposal asserted this change documented the cold-mtime
  skew on `dsh-host-apiproxy`. It did not; the README entry now exists.
- `firstLiveSeq`'s firehose gap runs through its own seq, not below it.
- The boundary is not always at `firstLiveSeq` (the idempotence guard), so
  consumers scan for the last one.
- `lastActivityTime` excludes by type, so a pickup time still leaks onto a
  synthetic closer when a boundary ends an open turn. Documented.
- Pin the fork claim end-to-end: a child inherits a still-running parent's
  open bracket below its own boundary, while the parent has none. Fails if the
  write moves back to the load path.
- Fix the telemetry title that contradicted its own assertions.

The `/status` call site cannot be pinned the way the other two are: the
command appends its own `command/run` before rendering, so the boundary is
never the log tail there. Its fixture now at least renders over a
boundary-bearing log.
This commit is contained in:
Hypatia May
2026-07-30 13:59:08 +08:00
parent a00786362f
commit 39ebd8f745
26 changed files with 115 additions and 23 deletions

View File

@@ -97,6 +97,10 @@ interface SessionEventMap {
* and no writer in this lifecycle produced it. Payload is empty — position
* and `time` carry the meaning.
*
* Locate the LAST one rather than reading `firstLiveSeq`: a seed already
* ending in a boundary is not re-marked, so reopening an untouched session
* does not grow its log per pickup.
*
* An owner of a standalone open/close bracket (`compact/start` …
* `compact/end`) reads it because inherited history and live work are
* otherwise byte-identical: an unmatched opening marker below the boundary
@@ -346,6 +350,10 @@ declare class Session {
* `session/inherited` event at this seq, which is what a consumer reading
* STORED history reads. Prefer this field in-process — it is exact before
* the marker's write reaches storage.
*
* The marker is appended before the store attaches, so when one exists the
* event AT this seq did not publish either: the firehose gap runs through
* `firstLiveSeq`, not just below it.
*/
readonly firstLiveSeq: number;
constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);