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:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-session-inherited-log-boundary.md
|
||||
2026-07-30-session-inherited-log-boundary.md: 3b9cb09d1d1a16d30fd212b439a97e3d9296bd6d
|
||||
2026-07-30-session-inherited-log-boundary.zh.md: de048bc994ca97f937f74783b9055fbeb171314a
|
||||
2026-07-30-session-inherited-log-boundary.md: b5ed9598078fae6fedd4c92613d57a7632eee323
|
||||
2026-07-30-session-inherited-log-boundary.zh.md: 83d3ce9ae87728228bcdfe73db1b2049bd55cc61
|
||||
|
||||
@@ -24,9 +24,11 @@ Two guards keep the marker from becoming noise. An empty seed writes nothing: a
|
||||
|
||||
## Persistence needs no changes
|
||||
|
||||
The marker is in `session.events` before the coordinator captures its creation seed, so it persists through the ordinary seed path — `onCreated`'s `createCore` + `appendCore`, or the ownerless-claim suffix write. No load-path write, no revision bump at load, no durable mark on a rejected `append`, and a read-only store still serves loads.
|
||||
The constructor append happens before `enter()`, so the session has no store attachment: the marker never publishes on `session/event`, exactly like the seed events below it. It is instead part of the log `initFor` captures as the creation seed, and persists through the ordinary seed path — `onCreated`'s `createCore` + `appendCore`, or the ownerless-claim suffix write. A consumer that watches the firehose therefore never sees the boundary and must read it from the log.
|
||||
|
||||
Being a live event, it reaches disk through the write-behind drain (`session/event` → `live.pending` → `scheduleDrain`) rather than a synchronous commit, so a crash can lose it. That costs nothing: `pending` drains in order, so a lost boundary means every live event above it is lost too, and the next pickup reads the same bytes the previous one did, appends its own boundary, and classifies the bracket identically. In-process consumers should prefer `firstLiveSeq`, which is exact before any write.
|
||||
Consequences for the seam: `load()` stays a pure read, with no revision bump, no `commitRepair` on a balanced log, and no durable mark left by a rejected `append`. **Attaching is not a pure read**, though — a pickup now writes where nothing was written before, so a read-only or full disk fails at `session/created` rather than at the first real turn. That is the one cost this placement adds, and it is narrower than the load-path version's (which failed the load itself).
|
||||
|
||||
A crash before the seed write reaches disk loses the boundary, and that costs nothing: the pending batch is written in order, so a lost boundary means every event above it is lost too. The next pickup reads the same bytes the previous one did, appends its own boundary, and classifies the bracket identically. In-process consumers should prefer `firstLiveSeq`, which is exact before any write.
|
||||
|
||||
## Scope of the guarantee
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ Status: implemented
|
||||
|
||||
## 持久化无需任何改动
|
||||
|
||||
协调器捕获创建种子时该标记已在 `session.events` 中,因此它通过普通的种子路径落盘——`onCreated` 的 `createCore` + `appendCore`,或无主认领的后缀写入。没有加载路径写入、加载时没有 revision 递增、被拒绝的 `append` 不留下持久标记,只读存储依然可以服务加载。
|
||||
构造函数中的 append 发生在 `enter()` 之前,因此会话尚无 store attachment:该标记不会在 `session/event` 上发布,与它下方的种子事件完全一样。它属于 `initFor` 捕获的那份创建种子,并通过普通的种子路径落盘——`onCreated` 的 `createCore` + `appendCore`,或无主认领的后缀写入。因此监听 firehose 的消费方永远看不到这条边界,必须从日志中读取它。
|
||||
|
||||
作为实时事件,它经由后写式 drain(`session/event` → `live.pending` → `scheduleDrain`)而不是同步提交到达磁盘,因此崩溃可能丢掉它。这没有代价:`pending` 按序 drain,所以丢掉一个边界意味着它上面的每个实时事件也一起丢掉,而下一次接手读到的字节与上一次相同,会追加自己的边界,并对括号作出完全相同的判定。进程内消费方应优先使用 `firstLiveSeq`,它在任何写入之前就是精确的。
|
||||
对 seam 的影响:`load()` 仍是纯读取,没有 revision 递增,对平衡日志不走 `commitRepair`,被拒绝的 `append` 也不留下持久标记。但**接手不是纯读取**——如今一次拾起会在此前完全无写入的路径上产生写入,因此只读存储或磁盘写满会在 `session/created` 处报错,而不是在第一个真实轮次处。这是本放置方式新增的唯一成本,并且比加载路径方案的成本更窄(后者会让加载本身失败)。
|
||||
|
||||
若崩溃发生在种子写入到达磁盘之前,边界会丢失,而这没有代价:待处理批次按序写入,所以丢掉一个边界意味着它上面的每个事件也一起丢掉。下一次接手读到的字节与上一次相同,会追加自己的边界,并对括号作出完全相同的判定。进程内消费方应优先使用 `firstLiveSeq`,它在任何写入之前就是精确的。
|
||||
|
||||
## 保证的适用范围
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md
|
||||
2026-07-29-durable-last-activity-index.md: 7d8a83e96d9339054b0d15b2331284c8aecfe65d
|
||||
2026-07-29-durable-last-activity-index.zh.md: b081aa91cfa51ef8bb87e93f4091431f2d6ddce1
|
||||
2026-07-29-durable-last-activity-index.md: 2569ce8dcdef4ce85713e4c3905c3a170be00a98
|
||||
2026-07-29-durable-last-activity-index.zh.md: 64078982bb09b0f94c41d5424d195d371c1a263f
|
||||
|
||||
@@ -12,7 +12,7 @@ mtime answers a different question: when the artifact was last written. Every du
|
||||
|
||||
The attached projection has a real fix — `lastActivityTime()` skips boundaries — but it needs the event log, and the cold path deliberately does not read one. Reading the log to compute `updatedAt` would defeat the header-only listing that keeps `list()` scaling with session count rather than log size.
|
||||
|
||||
The boundary change raised the frequency of this defect and documented it as a known limitation on `dsh-host-apiproxy`. It did not introduce the approximation, and removing the approximation is a durable-format decision, which is why it is scoped here rather than there.
|
||||
The [boundary change](../../implemented/architecture/2026-07-30-session-inherited-log-boundary.md) raised the frequency of this defect, because a pickup now writes where nothing was written before; `dsh-host-apiproxy`'s README records it under Known Limitations. It did not introduce the approximation, and removing the approximation is a durable-format decision, which is why it is scoped here rather than there.
|
||||
|
||||
## Proposal
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ mtime 回答的是另一个问题:这份产物上次是什么时候被写入
|
||||
|
||||
已附加会话的那个投影有真正的修复办法(`lastActivityTime()` 会跳过边界),但它需要事件日志,而冷路径有意不去读日志。为计算 `updatedAt` 而读取日志,会让只读 header 的列举失去意义,而正是它让 `list()` 的开销随会话数量而非日志体量增长。
|
||||
|
||||
边界那次变更提高了这个缺陷的出现频率,并把它作为一项已知限制记录在 `dsh-host-apiproxy` 上。它并没有引入这套近似做法,而移除这套近似是一项持久格式决策,因此它的范围划在本文,而不是那里。
|
||||
[边界那次变更](../../implemented/architecture/2026-07-30-session-inherited-log-boundary.md)提高了这个缺陷的出现频率,因为一次拾起如今会在此前完全无写入的路径上产生写入;`dsh-host-apiproxy` 的 README 已在 Known Limitations 中记录该项。它并没有引入这套近似做法,而移除这套近似是一项持久格式决策,因此它的范围划在本文,而不是那里。
|
||||
|
||||
## 提案
|
||||
|
||||
|
||||
Reference in New Issue
Block a user