mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
refactor(session-projection): review fixes for the summary key set
- Export `ProjectionKeySelection` (the read faces' key selection) and reuse it from the projection cache instead of restating the union twice. The Cordis catalog can now resolve the type its generated signatures reference. - Make `wire.summary` a plain boolean: omitted means `true`, and a key whose map entry carries the summary mark declares `false` so the runtime key set and the client view type agree. The type no longer forbids a declaration. - Describe the mark accurately: it has no runtime representation; `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the registration erase only widens the state type. - Say plainly in the package README, the subsystem page, the client field comments, and the Agent Note that a list row carries the summary projection and never the full projection: a connected Session's store may hold more values, but those are outside the list contract. - Document the summary vocabulary on the subsystem page with `ts type-equiv` blocks for the mark, its stripping, the excluded-key test, the summary value type, and the key selection; split the prose paragraph. - Revert the behavior-neutral cache-double refactors in three specs, and record in the Agent Note why no upgrade guide accompanies the wire change and how a consumer migrates.
This commit is contained in:
1 parent
64a1d1e95f
commit
f230fc2fc2
21 files changed
+312
-106
No files matched your search
+4
-4
@@ -9,14 +9,14 @@
|
||||
en: f5ca718ca5e618c6
|
||||
zh: cbea7f94a2a293b0
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/decision:
|
||||
en: 031d752b8069ef0c
|
||||
zh: 96cc33d309837dec
|
||||
en: 6e8f43d7a53c35fd
|
||||
zh: a6e2bccc20474515
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/alternatives-considered:
|
||||
en: ce8320f1032018ed
|
||||
zh: 4c5fad5e605bf524
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/consequences:
|
||||
en: 556ee55b2404b631
|
||||
zh: 6c429bd311ce1297
|
||||
en: d0d9bc21e9bb2480
|
||||
zh: 3bccbf0b6c4a416c
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/testing:
|
||||
en: 48ee647077dcecde
|
||||
zh: b079a5920c33504f
|
||||
+5
-1
@@ -14,7 +14,7 @@ The Session summary is every registered client view except the keys whose `Sessi
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) declares the type-only mark `SessionProjectionSummaryExcluded<T>` over a unique symbol that is never exported and never present at runtime. `SessionProjectionViewValue<T>` strips it distributively, so a nullable entry keeps its `null`; `SessionProjectionSummaryExcludedKey` tests the non-nullish part; `SessionProjectionSummaryValues` is every view value minus those keys.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), and `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Registration is an interlock: `ProjectionDefinition.wire` requires `summary: false` exactly when the key's map entry carries the mark, and accepts only `summary?: never` otherwise. The single runtime erasure of the mark is the wire erasure in `SessionProjectionRegistry.register`, which stores whether the unit belongs to summaries.
|
||||
- Registration carries the same decision as a plain boolean: `wire.summary` says whether a Session summary includes the unit, and omitting it means `true`. A key whose map entry carries the mark declares `false` so the runtime key set and the client view type agree. The mark itself has no runtime representation — `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the erase in `register` only widens the state type.
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) exposes `summaryKeys(): ReadonlySet<key>`, derived from the live registrations and rebuilt only when a new key registers or a key's last registrant disposes. `cachedSnapshot`, `viewCheckpoint`, and `snapshot` accept `ProjectionKeySelection` (`readonly Key[] | ReadonlySet<Key>`) and use a passed set as-is.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live and cold cached faces. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- Client types only: `SessionSummary.projectionValues` narrows to `Partial<SessionProjectionSummaryValues>`, while `SessionProjectionSnapshot.values` keeps every view value. No wire field, function, or UI name changes, and an opened Session still receives every key through `session/follow`, the control baseline, and `session.projections`.
|
||||
@@ -37,6 +37,10 @@ Estimated effect: with logs whose outlines dominate the projection payload, the
|
||||
|
||||
Bought: a list response no longer scales with outline length or log count, the excluded keys cannot be requested by name, and reading an excluded key from a list row is a type error. The registration interlock makes a marked key without `summary: false` (and the reverse) a compile error rather than a review finding.
|
||||
|
||||
A list row carries the summary projection, never the full projection: it promises the summary key set and nothing more. A connected Session's client store may hold more values from its control/follow baseline, but those are outside the list contract and must not be read from a row; the opened Session's own baselines keep every value.
|
||||
|
||||
No upgrade guide records this change. The externally perceptible part is the wire: `session/list` rows no longer carry the four keys, and a client type that reads `projectionValues.turnOutline` no longer compiles. A consumer migrating reads the opened Session instead — its `session/follow` opening snapshot, the `session/control` baseline, or a `session.projections` read — which keep every projected value; the package README and the subsystem page state the rule.
|
||||
|
||||
Paid: `ProjectionKeySelection` is a new public signature type and `ReadonlySet` joins the catalog's foundation types; `summaryKeys()` has two invalidation points (a new key, the last disposer) that a future registration path must not miss; the summary key set, like the unit table, is process-wide, so a key excluded by one preset is excluded for every Session; and a nullable value cannot be marked directly — its `null` must stay outside the wrapper.
|
||||
|
||||
## Testing
|
||||
|
||||
+5
-1
@@ -14,7 +14,7 @@ Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMa
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) 声明类型层标记 `SessionProjectionSummaryExcluded<T>`,它基于一个从不导出、运行时从不存在的 unique symbol。`SessionProjectionViewValue<T>` 以分发方式去掉标记,可空条目因此保留 `null`;`SessionProjectionSummaryExcludedKey` 取非空部分判断;`SessionProjectionSummaryValues` 是全部视图值减去这些键。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo),以及 `imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 注册是一道互锁:`ProjectionDefinition.wire` 恰好在该键的 map 条目带标记时要求 `summary: false`,其余情况只接受 `summary?: never`。标记唯一的运行时擦除发生在 `SessionProjectionRegistry.register` 的 wire 擦除处,由它记录该单元是否属于摘要。
|
||||
- 注册用普通布尔值承载同一决定:`wire.summary` 表示 Session 摘要是否包含该单元,省略即 `true`。map 条目带标记的 key 声明 `false`,让运行时键集合与客户端视图类型一致。标记本身没有运行时表示 —— `wire.view` 与 `viewSchema` 的签名已用 `SessionProjectionViewValue` 去标记,因此 `register` 里的擦除只是放宽状态类型。
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) 提供 `summaryKeys(): ReadonlySet<key>`,从实时注册派生,只有新键注册或某键最后一个注册者注销时才重建。`cachedSnapshot`、`viewCheckpoint`、`snapshot` 接受 `ProjectionKeySelection`(`readonly Key[] | ReadonlySet<Key>`),传入 Set 时直接使用。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给 live 与 cold 两个缓存读取面。`cachedPredecessorTitle` 保持只读 title。
|
||||
- 客户端只有类型变化:`SessionSummary.projectionValues` 收窄为 `Partial<SessionProjectionSummaryValues>`,而 `SessionProjectionSnapshot.values` 保留全部视图值。wire 字段、函数名与 UI 名称均不变;打开的 Session 仍通过 `session/follow`、控制基线与 `session.projections` 收到每个键。
|
||||
@@ -37,6 +37,10 @@ Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMa
|
||||
|
||||
换来的:列表响应不再随 outline 长度或日志数量增长;被排除的键无法按名请求;从列表行读取被排除键是类型错误。注册互锁让「带标记却未声明 `summary: false`」(以及反向)成为编译错误,而不是评审发现。
|
||||
|
||||
列表行携带的是摘要投影,绝不是全量投影:它只承诺摘要键集合,不承诺更多。已连接 Session 的客户端 store 可能因 control/follow 基线持有更多值,但那不属于列表契约,不得从行上读取;打开的 Session 自身的基线保留全部取值。
|
||||
|
||||
本次没有升级指南记录该变更。外部可感知的部分在 wire:`session/list` 行不再携带四个键,读取 `projectionValues.turnOutline` 的客户端类型不再编译。迁移方式是改读打开的 Session —— 其 `session/follow` 初始快照、`session/control` 基线或 `session.projections` 读取 —— 它们保留全部投影值;包 README 与子系统页写明了该规则。
|
||||
|
||||
付出的:`ProjectionKeySelection` 是新的公开签名类型,`ReadonlySet` 也需加入 catalog 的 foundation 类型;`summaryKeys()` 有两个失效点(新键、最后一个 disposer),未来的注册路径不得漏掉;摘要键集合与单元表一样是进程级的,某个 preset 排除的键对所有 Session 都排除;可空值不能直接标记 —— 它的 `null` 必须留在包装之外。
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
en: a49a57c600c3cbd0
|
||||
zh: c92f00b8a9d71bad
|
||||
/session-projections/summary-reads:
|
||||
en: ec31355c373c5dc6
|
||||
zh: 274cb8944339333b
|
||||
en: d2bc9bcc775acfb7
|
||||
zh: b1d2ba74496305b8
|
||||
/session-projections/summary-reads/summary-vocabulary:
|
||||
en: 5920d94f98fb323a
|
||||
zh: a0e24d6ad2a6b739
|
||||
/session-projections/the-registry-ctx-sessionprojections:
|
||||
en: 50e79c1bc16abb63
|
||||
zh: 200ecbf0e4ab19d2
|
||||
@@ -56,15 +56,15 @@ interface ProjectionDefinition<
|
||||
* @returns the whole current value for this unit's key.
|
||||
*/
|
||||
view(state: NoInfer<S>): SessionProjectionViewValue<SessionProjectionMap[K]>
|
||||
} & (K extends SessionProjectionSummaryExcludedKey
|
||||
? {
|
||||
/** Required by this key's summary-exclusion mark: a Session summary omits it. */
|
||||
summary: false
|
||||
}
|
||||
: {
|
||||
/** Set only by a key whose `SessionProjectionMap` entry carries the summary-exclusion mark. */
|
||||
summary?: never
|
||||
}) : never
|
||||
/**
|
||||
* Whether a Session summary carries this unit; omitted means `true`.
|
||||
* Declare `false` on a key whose map entry carries the
|
||||
* `SessionProjectionSummaryExcluded` mark: the runtime key set follows this
|
||||
* field, while the client view type follows the mark.
|
||||
* @default true
|
||||
*/
|
||||
summary?: boolean
|
||||
} : never
|
||||
/**
|
||||
* Persisted-cache invalidation version: bump whenever the serialized state fields or the
|
||||
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
|
||||
@@ -111,7 +111,79 @@ type ProjectionChangeListener = (
|
||||
|
||||
## Summary reads
|
||||
|
||||
A Session summary carries every registered client view except the keys whose `SessionProjectionMap` entry carries the `SessionProjectionSummaryExcluded<T>` mark. `summaryKeys()` derives that set from the live registrations and keeps returning the same instance until a registration or its disposal changes the key table; a summary consumer passes it to the existing key-taking faces — `cachedSnapshot(session, keys)` for cached cells and `viewCheckpoint(rows, keys)` for checkpoint rows, both keeping their watermark rules and both using a set without copying — so the served keys cannot drift from the marks the owning units declare and no summary-specific read face exists. `snapshot`, `cachedSnapshot`, and `viewCheckpoint` remain complete when called without a key subset and still serve every wire key, including those a summary omits; an opened Session therefore receives every projected value. Summary reads serve the Session list rows; the `@` reference lists Sessions the same way but reads only its label keys through the same key-taking faces. The types require `wire.summary: false` exactly on a key whose map entry carries the mark, and forbid it on every other key.
|
||||
A Session summary carries every registered client view except the keys whose `SessionProjectionMap` entry carries the `SessionProjectionSummaryExcluded<T>` mark. The mark has no runtime representation: it decides the client view type, while `wire.summary` decides the runtime key set, with omission meaning `true`. A key whose entry carries the mark declares `wire.summary: false` so the two agree; the field is a plain boolean, so the type does not enforce it.
|
||||
|
||||
`summaryKeys()` derives the key set from the live registrations and keeps returning the same instance until a registration or its disposal changes the key table. A summary consumer passes it to the existing key-taking faces — `cachedSnapshot(session, keys)` for cached cells and `viewCheckpoint(rows, keys)` for checkpoint rows, both keeping their watermark rules and both using a set without copying — so there is no summary-specific read face and no second definition of the set.
|
||||
|
||||
A list row carries that summary projection, never the full projection: it promises the summary key set and nothing more. `snapshot`, `cachedSnapshot`, and `viewCheckpoint` remain complete when called without a key subset, which is how an opened Session, its follow baseline, and the control baseline keep every projected value; the `@` reference reads only its label keys through the same key-taking faces.
|
||||
|
||||
### Summary vocabulary
|
||||
|
||||
Each block below is the declaration the summary rule is stated in. The mark, its stripping, the excluded-key test, and the summary value type live in the pure-type outlet; the key selection both read faces accept lives in the service module.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One `SessionProjectionMap` entry excluded from Session summary payloads
|
||||
* (the Session list row and any other summary read). The mark is type-only;
|
||||
* `T` keeps its exact runtime value and wire representation. Wrap only the
|
||||
* excluded contributor's own map entry, and place a nullable entry's `null`
|
||||
* outside the wrapper (`SessionProjectionSummaryExcluded<Item[]> | null`) so
|
||||
* the required symbol cannot absorb that member.
|
||||
*/
|
||||
type SessionProjectionSummaryExcluded<T> = T & {
|
||||
readonly [sessionProjectionSummaryExcluded]: true
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One map entry's client view value: the summary mark stripped. The
|
||||
* conditional distributes over a nullable entry's union, so a `| null` member
|
||||
* written outside the wrapper survives unchanged.
|
||||
*/
|
||||
type SessionProjectionViewValue<T> =
|
||||
T extends SessionProjectionSummaryExcluded<infer Value> ? Value : T
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Every client view value, keyed by projection key, with no summary mark. */
|
||||
type SessionProjectionViewValues = {
|
||||
[K in keyof SessionProjectionMap]: SessionProjectionViewValue<SessionProjectionMap[K]>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Keys a Session summary omits: map entries whose non-nullish value carries
|
||||
* the summary mark. Nullish members are ignored, so a nullable entry is still
|
||||
* recognized when its `null` lives outside the wrapper.
|
||||
*/
|
||||
type SessionProjectionSummaryExcludedKey = {
|
||||
[K in keyof SessionProjectionMap]:
|
||||
NonNullable<SessionProjectionMap[K]> extends SessionProjectionSummaryExcluded<unknown> ? K : never
|
||||
}[keyof SessionProjectionMap]
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The client values a Session summary carries: every client view value except
|
||||
* {@link SessionProjectionSummaryExcludedKey}. A summary read returns these
|
||||
* keys only; an opened Session's full snapshot keeps every view value.
|
||||
*/
|
||||
type SessionProjectionSummaryValues =
|
||||
Omit<SessionProjectionViewValues, SessionProjectionSummaryExcludedKey>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One read's client-visible key selection: an explicit key list, or a derived
|
||||
* set such as {@link SessionProjectionRegistry.summaryKeys}. A set is used as
|
||||
* given; only a list is copied into one.
|
||||
*/
|
||||
type ProjectionKeySelection =
|
||||
| readonly Extract<keyof SessionProjectionMap, string>[]
|
||||
| ReadonlySet<Extract<keyof SessionProjectionMap, string>>
|
||||
```
|
||||
|
||||
## The registry: `ctx.sessionProjections`
|
||||
|
||||
@@ -151,7 +223,7 @@ The persisted projection cache service. Opens the `session_projcache` domain at
|
||||
* @returns the viewed block, or `undefined` when no usable row exists for
|
||||
* this lifecycle at the current Session format.
|
||||
*/
|
||||
cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[] | ReadonlySet<Extract<keyof SessionProjectionMap, string>>, ): ProjectionSnapshot | undefined
|
||||
cachedSnapshot( meta: SessionHeader, keys?: ProjectionKeySelection, ): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Read only a predecessor checkpoint's title as a zero-I/O listing hint.
|
||||
|
||||
@@ -56,15 +56,15 @@ interface ProjectionDefinition<
|
||||
* @returns the whole current value for this unit's key.
|
||||
*/
|
||||
view(state: NoInfer<S>): SessionProjectionViewValue<SessionProjectionMap[K]>
|
||||
} & (K extends SessionProjectionSummaryExcludedKey
|
||||
? {
|
||||
/** Required by this key's summary-exclusion mark: a Session summary omits it. */
|
||||
summary: false
|
||||
}
|
||||
: {
|
||||
/** Set only by a key whose `SessionProjectionMap` entry carries the summary-exclusion mark. */
|
||||
summary?: never
|
||||
}) : never
|
||||
/**
|
||||
* Whether a Session summary carries this unit; omitted means `true`.
|
||||
* Declare `false` on a key whose map entry carries the
|
||||
* `SessionProjectionSummaryExcluded` mark: the runtime key set follows this
|
||||
* field, while the client view type follows the mark.
|
||||
* @default true
|
||||
*/
|
||||
summary?: boolean
|
||||
} : never
|
||||
/**
|
||||
* Persisted-cache invalidation version: bump whenever the serialized state fields or the
|
||||
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
|
||||
@@ -111,7 +111,79 @@ type ProjectionChangeListener = (
|
||||
|
||||
## 摘要读取
|
||||
|
||||
Session 摘要包含每个已注册客户端视图,但排除 `SessionProjectionMap` 条目带 `SessionProjectionSummaryExcluded<T>` 标记的 key。`summaryKeys()` 从实时注册派生该集合,并在注册或注销改变 key 表之前持续返回同一实例;摘要消费方把它传给既有的带 key 读取面 —— 缓存 cell 用 `cachedSnapshot(session, keys)`,checkpoint 行用 `viewCheckpoint(rows, keys)`,两者保持各自的水位规则且直接使用 Set、不做复制 —— 因此被服务的 key 不会与各 owning unit 声明的标记漂移,也不存在摘要专用读取面。`snapshot`、`cachedSnapshot` 与 `viewCheckpoint` 在不传 key 子集时仍是完整的,仍返回每个 wire key(包括摘要省略的那些);打开的 Session 因此会收到全部投影值。摘要读取服务于 Session 列表行;`@` 引用同样以列表方式枚举 Session,但只通过同一组带 key 的读取面读取其标签 key。类型要求恰好是:map 条目带标记的 key 声明 `wire.summary: false`,其他 key 一律禁止声明。
|
||||
Session 摘要包含每个已注册客户端视图,但排除 `SessionProjectionMap` 条目带 `SessionProjectionSummaryExcluded<T>` 标记的 key。标记没有运行时表示:它决定客户端视图类型,`wire.summary` 决定运行时键集合,省略即 `true`。map 条目带标记的 key 应声明 `wire.summary: false` 以保持一致;该字段是普通布尔值,类型层不做强制。
|
||||
|
||||
`summaryKeys()` 从实时注册派生该集合,并在注册或注销改变 key 表之前持续返回同一实例。摘要消费方把它传给既有的带 key 读取面 —— 缓存 cell 用 `cachedSnapshot(session, keys)`,checkpoint 行用 `viewCheckpoint(rows, keys)`,两者保持各自的水位规则且直接使用 Set、不做复制 —— 因此不存在摘要专用读取面,集合也只有一处定义。
|
||||
|
||||
列表行携带的是这个摘要投影,绝不是全量投影:它只承诺摘要键集合,不承诺更多。`snapshot`、`cachedSnapshot` 与 `viewCheckpoint` 在不传 key 子集时仍是完整的,打开的 Session、其 follow 基线与控制基线因此保留每个投影值;`@` 引用只通过同一组带 key 的读取面读取其标签 key。
|
||||
### 摘要词汇
|
||||
|
||||
以下每块都是摘要规则所依据的声明。标记、其剥离、被排除键的判定与摘要值类型位于纯类型出口;两个读取面共同接受的 key 选择位于服务模块。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One `SessionProjectionMap` entry excluded from Session summary payloads
|
||||
* (the Session list row and any other summary read). The mark is type-only;
|
||||
* `T` keeps its exact runtime value and wire representation. Wrap only the
|
||||
* excluded contributor's own map entry, and place a nullable entry's `null`
|
||||
* outside the wrapper (`SessionProjectionSummaryExcluded<Item[]> | null`) so
|
||||
* the required symbol cannot absorb that member.
|
||||
*/
|
||||
type SessionProjectionSummaryExcluded<T> = T & {
|
||||
readonly [sessionProjectionSummaryExcluded]: true
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One map entry's client view value: the summary mark stripped. The
|
||||
* conditional distributes over a nullable entry's union, so a `| null` member
|
||||
* written outside the wrapper survives unchanged.
|
||||
*/
|
||||
type SessionProjectionViewValue<T> =
|
||||
T extends SessionProjectionSummaryExcluded<infer Value> ? Value : T
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Every client view value, keyed by projection key, with no summary mark. */
|
||||
type SessionProjectionViewValues = {
|
||||
[K in keyof SessionProjectionMap]: SessionProjectionViewValue<SessionProjectionMap[K]>
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Keys a Session summary omits: map entries whose non-nullish value carries
|
||||
* the summary mark. Nullish members are ignored, so a nullable entry is still
|
||||
* recognized when its `null` lives outside the wrapper.
|
||||
*/
|
||||
type SessionProjectionSummaryExcludedKey = {
|
||||
[K in keyof SessionProjectionMap]:
|
||||
NonNullable<SessionProjectionMap[K]> extends SessionProjectionSummaryExcluded<unknown> ? K : never
|
||||
}[keyof SessionProjectionMap]
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The client values a Session summary carries: every client view value except
|
||||
* {@link SessionProjectionSummaryExcludedKey}. A summary read returns these
|
||||
* keys only; an opened Session's full snapshot keeps every view value.
|
||||
*/
|
||||
type SessionProjectionSummaryValues =
|
||||
Omit<SessionProjectionViewValues, SessionProjectionSummaryExcludedKey>
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One read's client-visible key selection: an explicit key list, or a derived
|
||||
* set such as {@link SessionProjectionRegistry.summaryKeys}. A set is used as
|
||||
* given; only a list is copied into one.
|
||||
*/
|
||||
type ProjectionKeySelection =
|
||||
| readonly Extract<keyof SessionProjectionMap, string>[]
|
||||
| ReadonlySet<Extract<keyof SessionProjectionMap, string>>
|
||||
```
|
||||
|
||||
## 注册表:`ctx.sessionProjections`
|
||||
|
||||
`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动权:一份 `session/event` 订阅、对每个已注册单元即时调用 `apply`,以及每会话每单元的水位线(watermark)cell。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect,其 disposer 随调用方 fiber 走:领域插件卸载后,其 key(连同缓存的 cell)从后续驱动与快照中消失,客户端将其读作能力缺失;key 以不同 `stateVersion` 重复时直接 throw,同版本注册方则共享一个单元并被计数。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。
|
||||
@@ -150,7 +222,7 @@ The persisted projection cache service. Opens the `session_projcache` domain at
|
||||
* @returns the viewed block, or `undefined` when no usable row exists for
|
||||
* this lifecycle at the current Session format.
|
||||
*/
|
||||
cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[] | ReadonlySet<Extract<keyof SessionProjectionMap, string>>, ): ProjectionSnapshot | undefined
|
||||
cachedSnapshot( meta: SessionHeader, keys?: ProjectionKeySelection, ): ProjectionSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Read only a predecessor checkpoint's title as a zero-I/O listing hint.
|
||||
|
||||
@@ -9,7 +9,12 @@ import type { SessionSummary } from '../../types.ts'
|
||||
/** Host list summary enriched with the latest Session Controller title projection. */
|
||||
export interface TitledSessionSummary extends SessionSummary {
|
||||
title?: string
|
||||
/** Current host-computed summary projection values for list consumers. */
|
||||
/**
|
||||
* The row's summary projection values, never the full projection set. A
|
||||
* connected Session's client store may hold more values from its
|
||||
* control/follow baseline; those are outside the list contract and must not
|
||||
* be read here.
|
||||
*/
|
||||
projectionValues?: Readonly<Partial<SessionProjectionSummaryValues>>
|
||||
}
|
||||
|
||||
@@ -30,7 +35,10 @@ export interface SessionListEntry {
|
||||
/** Format classification supplied by the Host list. */
|
||||
formatStatus?: SessionSummary['formatStatus']
|
||||
cwd?: string
|
||||
/** Current host-computed summary projection values for list consumers. */
|
||||
/**
|
||||
* The row's summary projection values, never the full projection set; see
|
||||
* `TitledSessionSummary.projectionValues`.
|
||||
*/
|
||||
projectionValues?: Readonly<Partial<SessionProjectionSummaryValues>>
|
||||
/** Lineage indent depth: root = 0; the UI just multiplies by the indent width. */
|
||||
depth: number
|
||||
|
||||
@@ -58,7 +58,12 @@ export interface SessionListSnapshot {
|
||||
|
||||
}
|
||||
|
||||
/** Shared projection values and the lifecycle of their explicit baseline read. */
|
||||
/**
|
||||
* Every projection value the store currently holds, with the summary mark
|
||||
* stripped, and the lifecycle of its explicit baseline read. This is the full
|
||||
* attached-Session set, not a list row's summary contract (see
|
||||
* `SessionListEntry.projectionValues`).
|
||||
*/
|
||||
export interface SessionProjectionSnapshot {
|
||||
readonly values: Readonly<Partial<SessionProjectionViewValues>>
|
||||
readonly state: 'idle' | 'loading' | 'ready' | 'migration-required' | 'error'
|
||||
|
||||
@@ -49,7 +49,11 @@ export interface SessionSummary {
|
||||
*/
|
||||
blank: boolean
|
||||
updatedAt: number
|
||||
/** Current host-computed summary projection values retained by the object layer. */
|
||||
/**
|
||||
* The row's summary projection values retained by the object layer, never the
|
||||
* full projection set. A connected Session's store may hold more values from
|
||||
* its control/follow baseline; those are outside the list contract.
|
||||
*/
|
||||
projectionValues?: Readonly<Partial<SessionProjectionSummaryValues>>
|
||||
}
|
||||
|
||||
|
||||
@@ -319,7 +319,8 @@ export class ApiSessionList {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap one projection block as Session-list hints of the named sequence space.
|
||||
* Wrap one summary projection block as Session-list hints of the named sequence space.
|
||||
* The block is the summary key set's values, not every value the source holds.
|
||||
* @param kind - which sequence space the block's watermark belongs to.
|
||||
* @param block - the block, or `undefined` when no source served one.
|
||||
* @returns the hints, or `undefined` when the block is absent or carries no value.
|
||||
@@ -329,8 +330,8 @@ function hintsOf(
|
||||
block: ProjectionSnapshot | undefined,
|
||||
): SessionProjectionHints | undefined {
|
||||
if (block === undefined || Object.keys(block.values).length === 0) return undefined
|
||||
// Listing hints contain every wire value the source currently holds but
|
||||
// remain partial: missing cells and cache rows are never materialized here.
|
||||
// Listing hints contain every summary wire value the source currently holds
|
||||
// but remain partial: missing cells and cache rows are never materialized here.
|
||||
return { kind, asOfSeq: block.asOfSeq, values: block.values as SessionProjectionValues }
|
||||
}
|
||||
|
||||
|
||||
@@ -83,12 +83,11 @@ async function harness(rows: readonly Row[], summaryMs = 8, workSliceMs = 8) {
|
||||
})
|
||||
const listSessions = vi.fn(async (_signal?: AbortSignal) => records)
|
||||
ctx.provide('sessionQuery', { listSessions } as never)
|
||||
const cachedRow = (header: SessionRecord['header']) => {
|
||||
recordSummary('cold', header.id)
|
||||
return { asOfSeq: -1, values: { sessionListMetadata: { blank: false, lastPromptAt: null } } }
|
||||
}
|
||||
ctx.provide('sessionProjectionCache', {
|
||||
cachedSnapshot: cachedRow,
|
||||
cachedSnapshot: (header: SessionRecord['header']) => {
|
||||
recordSummary('cold', header.id)
|
||||
return { asOfSeq: -1, values: { sessionListMetadata: { blank: false, lastPromptAt: null } } }
|
||||
},
|
||||
cachedPredecessorTitle: () => undefined,
|
||||
} as never)
|
||||
const summaryFor = list.summaryFor.bind(list)
|
||||
|
||||
@@ -141,9 +141,8 @@ describe('sessions.list cold merge', () => {
|
||||
stat,
|
||||
inspect,
|
||||
})
|
||||
const noCachedRow = () => undefined
|
||||
ctx.provide('sessionProjectionCache', {
|
||||
cachedSnapshot: noCachedRow,
|
||||
cachedSnapshot: () => undefined,
|
||||
cachedPredecessorTitle: (meta: SessionHeader) => meta.id === sid('legacy-title')
|
||||
? { asOfSeq: 2, values: { title: 'Cached predecessor title' } }
|
||||
: undefined,
|
||||
@@ -191,24 +190,23 @@ describe('sessions.list cold merge', () => {
|
||||
inspect,
|
||||
})
|
||||
const cacheCalls: string[] = []
|
||||
const cachedRow = (meta: SessionHeader) => {
|
||||
cacheCalls.push(String(meta.id))
|
||||
if (meta.id === sid('cached-blank')) {
|
||||
return { asOfSeq: 0, values: { sessionListMetadata: { blank: true, lastPromptAt: null } } }
|
||||
}
|
||||
if (meta.id === sid('cached-conversation')) {
|
||||
return { asOfSeq: 1, values: { sessionListMetadata: { blank: false, lastPromptAt: 1000 } } }
|
||||
}
|
||||
if (meta.id === sid('seeded-cold')) {
|
||||
return {
|
||||
asOfSeq: 5,
|
||||
values: { title: 'Forked title', sessionListMetadata: { blank: false, lastPromptAt: 1200 } },
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
ctx.provide('sessionProjectionCache', {
|
||||
cachedSnapshot: cachedRow,
|
||||
cachedSnapshot: (meta: SessionHeader) => {
|
||||
cacheCalls.push(String(meta.id))
|
||||
if (meta.id === sid('cached-blank')) {
|
||||
return { asOfSeq: 0, values: { sessionListMetadata: { blank: true, lastPromptAt: null } } }
|
||||
}
|
||||
if (meta.id === sid('cached-conversation')) {
|
||||
return { asOfSeq: 1, values: { sessionListMetadata: { blank: false, lastPromptAt: 1000 } } }
|
||||
}
|
||||
if (meta.id === sid('seeded-cold')) {
|
||||
return {
|
||||
asOfSeq: 5,
|
||||
values: { title: 'Forked title', sessionListMetadata: { blank: false, lastPromptAt: 1200 } },
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
cachedPredecessorTitle: () => undefined,
|
||||
} as never)
|
||||
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
|
||||
|
||||
@@ -541,20 +541,19 @@ describe('session.list projections column', () => {
|
||||
inspect: load,
|
||||
open: load,
|
||||
}) as never)
|
||||
// The carrier hands the listed header through as the identity witness.
|
||||
const cachedRow = (meta: { id: unknown; createdAt: number }) =>
|
||||
(meta.id === coldId && meta.createdAt === 5
|
||||
? {
|
||||
asOfSeq: SessionSeq(7),
|
||||
values: {
|
||||
'test/last-user': { text: 'cached' },
|
||||
sessionListMetadata: { blank: false, lastPromptAt: 6 },
|
||||
title: 'Cached title',
|
||||
},
|
||||
}
|
||||
: undefined)
|
||||
ctx.provide('sessionProjectionCache', {
|
||||
cachedSnapshot: cachedRow,
|
||||
// The carrier hands the listed header through as the identity witness.
|
||||
cachedSnapshot: (meta: { id: unknown; createdAt: number }) =>
|
||||
(meta.id === coldId && meta.createdAt === 5
|
||||
? {
|
||||
asOfSeq: SessionSeq(7),
|
||||
values: {
|
||||
'test/last-user': { text: 'cached' },
|
||||
sessionListMetadata: { blank: false, lastPromptAt: 6 },
|
||||
title: 'Cached title',
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
} as never)
|
||||
const response = await remote(ctx).list(request({}))
|
||||
if (!response.ok) throw new Error('unreachable')
|
||||
|
||||
@@ -2313,7 +2313,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
description: 'The persisted projection cache service. Opens the `session_projcache` domain at init, checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus three mandatory points — session creation, `turn/end`, and session disposal (the live-to-cold moment) — and serves the cached rows for a session header. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'cachedSnapshot( meta: SessionHeader, keys?: readonly Extract<keyof SessionProjectionMap, string>[] | ReadonlySet<Extract<keyof SessionProjectionMap, string>>, ): ProjectionSnapshot | undefined',
|
||||
signature: 'cachedSnapshot( meta: SessionHeader, keys?: ProjectionKeySelection, ): ProjectionSnapshot | undefined',
|
||||
description: 'The zero-I/O listing read: whole values viewed straight from the stored rows (version-matching keys only) of the record bound to the caller\'s lifecycle. The header is the only identity witness a listing holds, so this face matches the lifecycle identity (`formatVersion`, `createdAt`, `cwd`, `isSeeded`) and not the inherited cut: within one format generation the cut is fixed at fork time, so it distinguishes no lifecycle the other fields do not, and a viewed value never seeds a fold. The view is as stale as the last durable checkpoint but never wrong and never from an unrelated log. Its `asOfSeq` is the lowest watermark among the served rows: the stored record\'s own position, which the header cannot relate to the log the caller later opens. The Session list therefore labels the block as cached, and the client lets every value the connected Session produces supersede it whatever this number says.',
|
||||
parameters: [{ name: 'meta', description: 'the listed session\'s header (identity witness; no log read).' }, { name: 'keys', description: 'optional projection keys required by the caller\'s audience.' }],
|
||||
returns: 'the viewed block, or `undefined` when no usable row exists for this lifecycle at the current Session format.',
|
||||
@@ -6576,7 +6576,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ProjectionDefinition',
|
||||
declaration: 'export interface ProjectionDefinition<K extends keyof SessionProjectionStateMap, S extends SessionProjectionStateMap[K] = SessionProjectionStateMap[K]> {\n key: K;\n stateSchema: ZodType<S>;\n init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer<S>;\n apply(state: NoInfer<S>, event: SessionEvent): NoInfer<S>;\n wire?: K extends keyof SessionProjectionMap ? {\n viewSchema: ZodType<SessionProjectionViewValue<SessionProjectionMap[K]>>;\n view(state: NoInfer<S>): SessionProjectionViewValue<SessionProjectionMap[K]>;\n } & (K extends SessionProjectionSummaryExcludedKey ? {\n summary: false;\n } : {\n summary?: never;\n }) : never;\n stateVersion: number;\n}',
|
||||
declaration: 'export interface ProjectionDefinition<K extends keyof SessionProjectionStateMap, S extends SessionProjectionStateMap[K] = SessionProjectionStateMap[K]> {\n key: K;\n stateSchema: ZodType<S>;\n init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer<S>;\n apply(state: NoInfer<S>, event: SessionEvent): NoInfer<S>;\n wire?: K extends keyof SessionProjectionMap ? {\n viewSchema: ZodType<SessionProjectionViewValue<SessionProjectionMap[K]>>;\n view(state: NoInfer<S>): SessionProjectionViewValue<SessionProjectionMap[K]>;\n summary?: boolean;\n } : never;\n stateVersion: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ProjectionKeySelection',
|
||||
declaration: 'export type ProjectionKeySelection = readonly Extract<keyof SessionProjectionMap, string>[] | ReadonlySet<Extract<keyof SessionProjectionMap, string>>;',
|
||||
},
|
||||
{
|
||||
name: 'ProjectionSnapshot',
|
||||
@@ -7386,10 +7390,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionProjectionSummaryExcluded',
|
||||
declaration: 'export type SessionProjectionSummaryExcluded<T> = T & {\n readonly [sessionProjectionSummaryExcluded]: true;\n};',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionSummaryExcludedKey',
|
||||
declaration: 'export type SessionProjectionSummaryExcludedKey = {\n [K in keyof SessionProjectionMap]: NonNullable<SessionProjectionMap[K]> extends SessionProjectionSummaryExcluded<unknown> ? K : never;\n}[keyof SessionProjectionMap];',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionsValue',
|
||||
declaration: 'export type SessionProjectionsValue = (SessionProjectionBaseline & {\n readonly kind: \'sequenced\';\n}) | {\n readonly kind: \'migration-required\';\n readonly values: SessionProjectionValues;\n} | null;',
|
||||
|
||||
@@ -28,6 +28,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
ProjectionCheckpoint,
|
||||
ProjectionKeySelection,
|
||||
ProjectionSnapshot,
|
||||
SessionProjectionMap,
|
||||
} from '@deepseek-ai/dsh-session-projection'
|
||||
@@ -160,7 +161,7 @@ export class SessionProjectionCache extends Service {
|
||||
*/
|
||||
cachedSnapshot(
|
||||
meta: SessionHeader,
|
||||
keys?: readonly Extract<keyof SessionProjectionMap, string>[] | ReadonlySet<Extract<keyof SessionProjectionMap, string>>,
|
||||
keys?: ProjectionKeySelection,
|
||||
): ProjectionSnapshot | undefined {
|
||||
const expected = lifecycleIdentityOf(meta)
|
||||
const record = this.requireTable().get(meta.id)
|
||||
@@ -199,7 +200,7 @@ export class SessionProjectionCache extends Service {
|
||||
*/
|
||||
private viewRecord(
|
||||
record: CheckpointRecord,
|
||||
keys?: readonly Extract<keyof SessionProjectionMap, string>[] | ReadonlySet<Extract<keyof SessionProjectionMap, string>>,
|
||||
keys?: ProjectionKeySelection,
|
||||
): ProjectionSnapshot | undefined {
|
||||
const values = this.ctx.sessionProjections.viewCheckpoint(record.rows, keys)
|
||||
let asOfSeq: ProjectionSnapshot['asOfSeq'] | undefined
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
en: 3bbc61a438aeabd0
|
||||
zh: bd1824f4da278764
|
||||
/deepseek-ai-dsh-session-projection/use-this-package/session-summaries:
|
||||
en: 4a9e068f1d672b61
|
||||
zh: 373ace99986e7627
|
||||
en: bbdefde61fb3b02f
|
||||
zh: c6871e05e63ff4ed
|
||||
/deepseek-ai-dsh-session-projection/understand-the-implementation:
|
||||
en: 3397f58d6f49bfda
|
||||
zh: efb85d382e25574d
|
||||
|
||||
@@ -70,9 +70,9 @@ Every unit's state is checkpointed — client-visible and host-only alike — th
|
||||
|
||||
### Session summaries
|
||||
|
||||
A Session summary is every registered client view except the keys whose non-nullish `SessionProjectionMap` entry carries the `SessionProjectionSummaryExcluded<T>` mark. `summaryKeys()` returns that set, derived from the live registrations and stable — the same instance — until a registration or its disposal changes the key table. A summary consumer passes it to the existing key-taking reads — `cachedSnapshot(session, keys)` for live cells, `viewCheckpoint(rows, keys)` for checkpoint rows, and `cachedSnapshot(header, keys)` through the persisted cache — which treat a set as already selected, so there is no summary-specific read face, no copy per read, and no second definition of the set. Because the registry derives it, a consumer cannot request an excluded key and cannot drift from the marks the owning units declare.
|
||||
A Session summary is every registered client view except the keys whose non-nullish `SessionProjectionMap` entry carries the `SessionProjectionSummaryExcluded<T>` mark. `summaryKeys()` returns that set, derived from the live registrations and stable — the same instance — until a registration or its disposal changes the key table. A summary consumer passes it to the existing key-taking reads — `cachedSnapshot(session, keys)` for live cells, `viewCheckpoint(rows, keys)` for checkpoint rows, and `cachedSnapshot(header, keys)` through the persisted cache — which treat a set as already selected, so there is no summary-specific read face, no copy per read, and no second definition of the set. Because the registry derives it, a summary consumer cannot request an excluded key through the derived set; a complete face still serves an explicitly named key, which is how an opened Session keeps every value. A list row carries the summary projection and never the full projection.
|
||||
|
||||
The mark and the registration are one interlock: a key whose map entry carries the mark must declare `wire.summary: false`, and a key without the mark cannot declare it. A nullable entry keeps `null` outside the wrapper (`SessionProjectionSummaryExcluded<TodoItem[]> | null`) so the pre-first-write value stays expressible. The keys currently marked are `turnOutline`, `sessionStats`, `todos`, and `imageLimits`: an outline grows with the whole log, no list row renders the stats or todo list, and the image limits are one host-wide boot constant every Session shares.
|
||||
The mark decides the client view type; `wire.summary` decides the runtime key set, and omitting it means `true`. Declare `wire.summary: false` on a key whose map entry carries the mark so the two agree; the field is a plain boolean, not a type-level interlock. A nullable entry keeps `null` outside the wrapper (`SessionProjectionSummaryExcluded<TodoItem[]> | null`) so the pre-first-write value stays expressible. The keys currently marked are `turnOutline`, `sessionStats`, `todos`, and `imageLimits`: an outline grows with the whole log, no list row renders the stats or todo list, and the image limits are one host-wide boot constant every Session shares.
|
||||
|
||||
Summaries serve the Session list rows, which list Sessions without opening them. The `@` reference lists Sessions the same way but reads only its `title` and `subagent` label keys through the key-taking `snapshot(session, keys)` and `cachedSnapshot(header, keys)` faces. `snapshot`, `cachedSnapshot`, and `viewCheckpoint` still serve every wire key when called without a key subset, so an opened Session receives every projected value.
|
||||
|
||||
|
||||
@@ -70,9 +70,9 @@ const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
|
||||
|
||||
### Session 摘要
|
||||
|
||||
Session 摘要包含每个已注册客户端视图,但排除 `SessionProjectionMap` 条目(非空值)带 `SessionProjectionSummaryExcluded<T>` 标记的 key。`summaryKeys()` 返回该集合,从实时注册派生;在注册或注销改变 key 表之前,它保持同一实例。摘要消费方把它传给既有的带 key 读取面 —— 实时 cell 用 `cachedSnapshot(session, keys)`,checkpoint 行用 `viewCheckpoint(rows, keys)`,持久化缓存用 `cachedSnapshot(header, keys)` —— 这些读取面把 Set 视为已选好的集合,因此不存在摘要专用读取面、每次读取无需复制,集合也只有一处定义。由于集合由注册表派生,消费方既无法请求被排除的 key,也不会与各 owning unit 声明的标记漂移。
|
||||
Session 摘要包含每个已注册客户端视图,但排除 `SessionProjectionMap` 条目(非空值)带 `SessionProjectionSummaryExcluded<T>` 标记的 key。`summaryKeys()` 返回该集合,从实时注册派生;在注册或注销改变 key 表之前,它保持同一实例。摘要消费方把它传给既有的带 key 读取面 —— 实时 cell 用 `cachedSnapshot(session, keys)`,checkpoint 行用 `viewCheckpoint(rows, keys)`,持久化缓存用 `cachedSnapshot(header, keys)` —— 这些读取面把 Set 视为已选好的集合,因此不存在摘要专用读取面、每次读取无需复制,集合也只有一处定义。由于集合由注册表派生,摘要消费方无法通过该集合请求被排除的 key;完整读取面仍会按显式命名的 key 提供值,打开的 Session 因此保留全部取值。列表行携带的是摘要投影,绝不是全量投影。
|
||||
|
||||
标记与注册是一道互锁:map 条目带标记的 key 必须声明 `wire.summary: false`,没有标记的 key 不能声明。可空条目把 `null` 放在包装之外(`SessionProjectionSummaryExcluded<TodoItem[]> | null`),以便首次写入前的值仍可表达。当前被标记的 key 是 `turnOutline`、`sessionStats`、`todos` 和 `imageLimits`:outline 随整份日志增长,列表行不渲染统计或 todo 清单,而图片上限是每个 Session 共享的同一个宿主级启动常量。
|
||||
标记决定客户端视图类型,`wire.summary` 决定运行时键集合,省略即 `true`。map 条目带标记的 key 应声明 `wire.summary: false` 以保持一致;该字段就是普通布尔值,而不是类型层的互锁。可空条目把 `null` 放在包装之外(`SessionProjectionSummaryExcluded<TodoItem[]> | null`),以便首次写入前的值仍可表达。当前被标记的 key 是 `turnOutline`、`sessionStats`、`todos` 和 `imageLimits`:outline 随整份日志增长,列表行不渲染统计或 todo 清单,而图片上限是每个 Session 共享的同一个宿主级启动常量。
|
||||
|
||||
摘要服务于 Session 列表行,列表无需打开 Session。`@` 引用同样以列表方式枚举 Session,但只通过带 key 的 `snapshot(session, keys)` 与 `cachedSnapshot(header, keys)` 读取其 `title` 和 `subagent` 标签 key。`snapshot`、`cachedSnapshot` 和 `viewCheckpoint` 在不传 key 子集时仍返回每个 wire key,因此打开的 Session 仍会收到全部投影值。
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ declare module '@deepseek-ai/cordis' {
|
||||
import type {
|
||||
SessionProjectionMap,
|
||||
SessionProjectionStateMap,
|
||||
SessionProjectionSummaryExcludedKey,
|
||||
SessionProjectionViewValue,
|
||||
} from './types.ts'
|
||||
|
||||
@@ -95,15 +94,15 @@ export interface ProjectionDefinition<
|
||||
* @returns the whole current value for this unit's key.
|
||||
*/
|
||||
view(state: NoInfer<S>): SessionProjectionViewValue<SessionProjectionMap[K]>
|
||||
} & (K extends SessionProjectionSummaryExcludedKey
|
||||
? {
|
||||
/** Required by this key's summary-exclusion mark: a Session summary omits it. */
|
||||
summary: false
|
||||
}
|
||||
: {
|
||||
/** Set only by a key whose `SessionProjectionMap` entry carries the summary-exclusion mark. */
|
||||
summary?: never
|
||||
}) : never
|
||||
/**
|
||||
* Whether a Session summary carries this unit; omitted means `true`.
|
||||
* Declare `false` on a key whose map entry carries the
|
||||
* `SessionProjectionSummaryExcluded` mark: the runtime key set follows this
|
||||
* field, while the client view type follows the mark.
|
||||
* @default true
|
||||
*/
|
||||
summary?: boolean
|
||||
} : never
|
||||
/**
|
||||
* Persisted-cache invalidation version: bump whenever the serialized state fields or the
|
||||
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
|
||||
@@ -125,8 +124,12 @@ export type ProjectionChangeListener = (
|
||||
seq: SessionSeq,
|
||||
) => void
|
||||
|
||||
/** One read's client-visible key selection: an explicit key list or a derived key set. */
|
||||
type ProjectionKeySelection =
|
||||
/**
|
||||
* One read's client-visible key selection: an explicit key list, or a derived
|
||||
* set such as {@link SessionProjectionRegistry.summaryKeys}. A set is used as
|
||||
* given; only a list is copied into one.
|
||||
*/
|
||||
export type ProjectionKeySelection =
|
||||
| readonly Extract<keyof SessionProjectionMap, string>[]
|
||||
| ReadonlySet<Extract<keyof SessionProjectionMap, string>>
|
||||
|
||||
@@ -308,8 +311,10 @@ export class SessionProjectionRegistry extends Service {
|
||||
stateSchema: definition.stateSchema,
|
||||
init: (header, inheritedEventCount) => definition.init(header, inheritedEventCount),
|
||||
apply: (state, event) => definition.apply(state as S, event),
|
||||
// The one runtime erasure of the type-only summary mark: the wire view
|
||||
// authors plain JSON, and the erased unit carries the interlock outcome.
|
||||
// The summary mark has no runtime representation: `wire.view` and
|
||||
// `viewSchema` are already typed with the mark stripped, so this erase
|
||||
// only widens the state type. `summary` carries the interlock outcome
|
||||
// into the erased unit, where the key set reads it.
|
||||
wire: wire === undefined
|
||||
? undefined
|
||||
: { viewSchema: wire.viewSchema, view: state => wire.view(state as S), summary: wire.summary !== false },
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
/**
|
||||
* Type-only marker of a `SessionProjectionMap` entry that a Session summary
|
||||
* leaves out. The symbol never exists at runtime: a marked entry keeps its
|
||||
* plain JSON value, and the only cast that erases the mark is the wire
|
||||
* erasure in `SessionProjectionRegistry.register`. A marked key therefore must
|
||||
* declare `wire.summary: false` at registration, and summary reads skip exactly
|
||||
* those keys.
|
||||
* plain JSON value, `SessionProjectionViewValue` strips the mark in the wire
|
||||
* and client view types, and the only runtime fact is the `summary: false`
|
||||
* declared at registration. A marked key must declare it, and summary reads
|
||||
* skip exactly those keys.
|
||||
*/
|
||||
declare const sessionProjectionSummaryExcluded: unique symbol
|
||||
|
||||
@@ -35,6 +35,7 @@ export type SessionProjectionSummaryExcluded<T> = T & {
|
||||
* cells, and React hooks. Domain packages merge their client-visible key here;
|
||||
* values are wire-JSON whole values. How a value is rendered is the slot
|
||||
* system's business, never this layer's.
|
||||
*
|
||||
*/
|
||||
export interface SessionProjectionMap {}
|
||||
|
||||
|
||||
@@ -2481,6 +2481,36 @@
|
||||
"doc": "docs/subsystems/boot.md",
|
||||
"symbol": "PendingModule",
|
||||
"source": "packages/boot/hmr/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
"symbol": "SessionProjectionSummaryExcluded",
|
||||
"source": "packages/session/session-projection/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
"symbol": "SessionProjectionViewValue",
|
||||
"source": "packages/session/session-projection/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
"symbol": "SessionProjectionViewValues",
|
||||
"source": "packages/session/session-projection/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
"symbol": "SessionProjectionSummaryExcludedKey",
|
||||
"source": "packages/session/session-projection/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
"symbol": "SessionProjectionSummaryValues",
|
||||
"source": "packages/session/session-projection/src/types.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/session-projection.md",
|
||||
"symbol": "ProjectionKeySelection",
|
||||
"source": "packages/session/session-projection/src/index.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user