@qilin-agent/session-projection 0.0.0-stage → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +58 -0
- package/README.md +142 -2
- package/README.zh.md +143 -0
- package/lib/index.js +437 -0
- package/lib/types/index.d.ts +292 -0
- package/lib/types/index.js +480 -0
- package/lib/types/types.d.ts +25 -0
- package/lib/types/types.js +11 -0
- package/package.json +45 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/session/session-projection/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 85229e402add27cf
|
|
7
|
+
zh: 66473d34393c44db
|
|
8
|
+
/qilin-session-projection:
|
|
9
|
+
en: a15ec1e979bd29b7
|
|
10
|
+
zh: 926ca59324e21fee
|
|
11
|
+
/qilin-session-projection/summary:
|
|
12
|
+
en: 6a551683a454e074
|
|
13
|
+
zh: 67cf75bc676c3d3c
|
|
14
|
+
/qilin-session-projection/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/qilin-session-projection/use-this-package:
|
|
18
|
+
en: b5b21529bff8092b
|
|
19
|
+
zh: b2bcd77db68672bd
|
|
20
|
+
/qilin-session-projection/use-this-package/when-to-choose-it:
|
|
21
|
+
en: 90b7c26cb45be52a
|
|
22
|
+
zh: 386e294ec09607c1
|
|
23
|
+
/qilin-session-projection/use-this-package/define-a-projection-unit:
|
|
24
|
+
en: 09758d1475fad86e
|
|
25
|
+
zh: 473637cb34aebe72
|
|
26
|
+
/qilin-session-projection/use-this-package/register-and-read:
|
|
27
|
+
en: 1ee07a6c1958def3
|
|
28
|
+
zh: 1c20493328ac4dbe
|
|
29
|
+
/qilin-session-projection/use-this-package/persisted-checkpoints:
|
|
30
|
+
en: bf5349012ae6dd0c
|
|
31
|
+
zh: b1b941935b1cc773
|
|
32
|
+
/qilin-session-projection/understand-the-implementation:
|
|
33
|
+
en: 3397f58d6f49bfda
|
|
34
|
+
zh: efb85d382e25574d
|
|
35
|
+
/qilin-session-projection/understand-the-implementation/design-concept:
|
|
36
|
+
en: 8afa0fdb49898f6d
|
|
37
|
+
zh: 6b2da34455963935
|
|
38
|
+
/qilin-session-projection/understand-the-implementation/source-map:
|
|
39
|
+
en: b0263d26c5a65d7a
|
|
40
|
+
zh: 2e58d3181dde901c
|
|
41
|
+
/qilin-session-projection/understand-the-implementation/drive-and-checkpoint-flow:
|
|
42
|
+
en: 00136880bb586403
|
|
43
|
+
zh: bd35f645111c068c
|
|
44
|
+
/qilin-session-projection/further-exploration:
|
|
45
|
+
en: f712858f4221e4ad
|
|
46
|
+
zh: 3f215a2a018502be
|
|
47
|
+
/qilin-session-projection/model-experience:
|
|
48
|
+
en: a24045627fb43695
|
|
49
|
+
zh: a8465196c3948796
|
|
50
|
+
/qilin-session-projection/model-experience/kv-cache-effect:
|
|
51
|
+
en: 8a592dae9bc946ab
|
|
52
|
+
zh: eca939995bb5f8e0
|
|
53
|
+
/qilin-session-projection/known-limitations-and-deferred-work:
|
|
54
|
+
en: 3dad1fbb8061fac8
|
|
55
|
+
zh: bd5cc28816085953
|
|
56
|
+
/qilin-session-projection/known-limitations-and-deferred-work/dev-note:
|
|
57
|
+
en: e67f633dc6fe7773
|
|
58
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,143 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The session-projection registry for developers serving whole current values of log-derived per-session state to client carriers, and for maintainers of the drive contract."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/session-projection
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Use `qilin-session-projection` when clients need current per-session state—such as todos, goals, or conversation statistics—without replaying the raw event log. Domains define synchronous projections from committed session events, and clients receive complete, schema-validated JSON values through snapshots and change notifications. Snapshots identify the last event reflected by every returned value, so carriers can pair state with the matching history cut. Projection state can be checkpointed for faster cold reads, while host-only projections remain private to the host.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Mount `qilin-session-projection` wherever client carriers need current values of log-derived session state. Domain plugins register units; carriers read snapshots and subscribe to the change feed; neither knows the other.
|
|
29
|
+
|
|
30
|
+
### When to choose it
|
|
31
|
+
|
|
32
|
+
Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. Skip it for host-only bookkeeping that no client reads: a unit without a `wire` block stays host-only. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject(['sessionProjections'], ...)`.
|
|
33
|
+
|
|
34
|
+
### Define a projection unit
|
|
35
|
+
|
|
36
|
+
A domain contributes one `ProjectionDefinition` per state key: a key, a state schema, an initial state, a synchronous fold `apply(state, event)`, an optional `wire` block that projects state to a client view, and a `stateVersion` that bumps whenever the state fields or fold semantics change:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
const definition = {
|
|
40
|
+
key: 'todo',
|
|
41
|
+
stateSchema: todoStateSchema,
|
|
42
|
+
stateVersion: 1,
|
|
43
|
+
init: (_header, _inheritedEventCount) => ({ items: [] }),
|
|
44
|
+
apply: (state, event) => event.type === 'todo/upsert'
|
|
45
|
+
? { items: event.data.items }
|
|
46
|
+
: state,
|
|
47
|
+
wire: {
|
|
48
|
+
viewSchema: todoViewSchema,
|
|
49
|
+
view: state => ({ items: state.items }),
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`init(header, inheritedEventCount)` receives both lightweight metadata and the exact fork-inherited cut; it must not infer that cut from `firstLiveSeq` or `session/end-seed`. `apply` must be synchronous and must return the same state reference for events that do not concern the unit — an unchanged reference means zero downstream work. The registry compares consecutive raw `wire.view` results with `Object.is`; an object or array view must reuse its reference to suppress publication across internal-only state changes, while a structurally equal new object is still a change. A state-carrying log event must carry the complete post-change state, never a bare delta.
|
|
55
|
+
|
|
56
|
+
### Register and read
|
|
57
|
+
|
|
58
|
+
`register(definition)` installs the unit; registrants with the same key and `stateVersion` share its cells, while an incompatible version or invalid `stateVersion` throws. Registration is an effect on the calling fiber, so the last unload removes the key and its cached cells. Carriers read a consistent synchronous cut over every client-visible unit with `snapshot(session)` — `{ asOfSeq, values }`, where `asOfSeq` is the seq of the last event every value reflects — and subscribe to per-change notifications with `onChanged(listener)`. `stateOf(session, key)` reads one unit's live read-only host state without computing unrelated views.
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
const dispose = ctx.sessionProjections.register(definition)
|
|
62
|
+
const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A domain that requires projected state declares `sessionProjections` as a Kylin service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent.
|
|
66
|
+
|
|
67
|
+
### Persisted checkpoints
|
|
68
|
+
|
|
69
|
+
Every unit's state is checkpointed — client-visible and host-only alike — through `checkpoint(session)`, and the sibling [session-projection-cache](../session-projection-cache/README.md) persists those checkpoints so cold reads skip full log loads. Checkpoint watermarks use `SessionSeqCursor` (`-1` for an empty log), while replay starts use `SessionLogOffset`; `restoreFloor` and `restore` implement the read recipe without conflating an existing event with a log gap.
|
|
70
|
+
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
<a id="understand-the-implementation"></a>
|
|
74
|
+
## Understand the implementation
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>Implementation internals — click to expand</summary>
|
|
78
|
+
|
|
79
|
+
This section explains the drive machinery and the unit contract; the observable contract is covered in [Use this package](#use-this-package).
|
|
80
|
+
|
|
81
|
+
### Design concept
|
|
82
|
+
|
|
83
|
+
The package is the Service Definition and drive role of a capability seam: the framework drives, the domain computes. The registry subscribes to `session/event` once; every committed event passes every registered unit's `apply` eagerly (cells build lazily on first touch). The first `Object.is` gate skips view work when the state reference is unchanged; a two-slot live-drive cache reuses the previous raw view and a second `Object.is` gate suppresses publication while the raw view reference is unchanged. Carriers read `snapshot()` in the same tick as their page slice, which is what makes `asOfSeq` one consistent cut; an accidentally async view returns a Promise and fails `wire.viewSchema.parse`.
|
|
84
|
+
|
|
85
|
+
### Source map
|
|
86
|
+
|
|
87
|
+
| File | Role |
|
|
88
|
+
|---|---|
|
|
89
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: `SessionProjectionRegistry` service, `ProjectionDefinition`, snapshot and checkpoint machinery |
|
|
90
|
+
| [`src/types.ts`](src/types.ts) | The merge-extensible `SessionProjectionMap` and `SessionProjectionStateMap` type tables |
|
|
91
|
+
|
|
92
|
+
### Drive and checkpoint flow
|
|
93
|
+
|
|
94
|
+
One committed event drives every registered unit in registration order; a client-visible unit whose raw view changes by `Object.is` notifies the change feed with its schema-validated view and the causing seq. The live drive retains its previous and current raw views; snapshots and cold reads remain complete independent reads. `checkpoint(session)` returns one detached `(key → {ver, seq, val})` row per unit for the persisted cache; `restoreFloor` anchors a tail read one event below the lowest usable watermark so a shrunk log is detected, and `restore` refolds persisted rows over a stored suffix, discarding any row whose `ver` does not match or that claims events past the stored end.
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
<a id="further-exploration"></a>
|
|
101
|
+
## Further Exploration
|
|
102
|
+
|
|
103
|
+
Read these pages when the package-level contract is not enough. They move from the unit contract to the read-model subsystem and the persisted cache.
|
|
104
|
+
|
|
105
|
+
- [Session projections subsystem](../../../docs/subsystems/session-projection.md) — the projection unit contract, drive semantics, and generated service API.
|
|
106
|
+
- [Session persistence subsystem](../../../docs/subsystems/persistence.md) — the event log projections fold over.
|
|
107
|
+
- [Session projection cache](../session-projection-cache/README.md) — the persisted checkpoints that make cold reads skip full log loads.
|
|
108
|
+
- [Session package map](../README.md) — adjacent persistence, title, and telemetry packages.
|
|
109
|
+
- [Session-projection RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md) — the design rationale for projections and the command log.
|
|
110
|
+
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
<a id="model-experience"></a>
|
|
114
|
+
## Model Experience
|
|
115
|
+
|
|
116
|
+
None, as the projection registry serves client-facing read models of already-logged session state and registers nothing model-facing.
|
|
117
|
+
|
|
118
|
+
#### KV Cache effect
|
|
119
|
+
|
|
120
|
+
None; projections never assemble or send provider requests.
|
|
121
|
+
|
|
122
|
+
## Known Limitations and Deferred Work
|
|
123
|
+
|
|
124
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
These limits define where the projection registry needs care at scale. They are current package constraints, not a task backlog.
|
|
128
|
+
|
|
129
|
+
- **Every tail page carries every client-visible key** — there is no per-key opt-out or lazy-key request shape yet; acceptable while values are UI-scale whole states, revisit if a domain's value grows large.
|
|
130
|
+
- **The unit table is process-wide, so key presence is not a per-session capability signal** — a key registered by any agent preset appears in every session's snapshot; a client must read the value rather than treat an absent key as absence of the feature.
|
|
131
|
+
- **Eager drive touches every unit per event** — cheap by construction (whole-value rule and state/view reference gates), but a hot path would justify per-unit event-type prefilters.
|
|
132
|
+
- **Registry cells live in memory only** — a restart rebuilds by folding the log on first touch; compositions that mount `qilin-session-projection-cache` seed that fold from persisted rows instead.
|
|
133
|
+
- **Synchronous unit discipline is only partially mechanical** — `wire.viewSchema.parse` rejects a Promise-returning view, but an `apply` that blocks or reads torn non-session state is a review concern.
|
|
134
|
+
|
|
135
|
+
<a id="dev-note"></a>
|
|
136
|
+
### Dev Note
|
|
137
|
+
|
|
138
|
+
<details>
|
|
139
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
140
|
+
|
|
141
|
+
None.
|
|
142
|
+
|
|
143
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向开发者与维护者的会话投影注册表说明,用于向客户端载体提供日志派生逐会话状态的完整当前值,或维护驱动约定。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/session-projection
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
当客户端需要当前的逐会话状态(例如待办事项、目标或对话统计)而不应自行重放原始事件日志时,使用 `qilin-session-projection`。领域根据已提交的会话事件定义同步投影,客户端则通过快照与变更通知接收经过 schema 校验的完整 JSON 值。快照标明所有返回值共同反映到的最后一个事件,因此载体可以把状态与对应的历史切面配对。投影状态可以通过检查点加快冷读,而仅供 host 使用的投影不会暴露给客户端。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
在客户端载体需要日志派生会话状态的当前值处挂载 `qilin-session-projection`。领域插件注册单元;载体读取快照并订阅变更流;两侧互不相识。
|
|
29
|
+
|
|
30
|
+
### 何时选择
|
|
31
|
+
|
|
32
|
+
当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。当维护的是无客户端读取的 host-only 记账时跳过:不带 `wire` 块的单元保持 host-only。host 读取方要么在插件 `inject` 中声明 `sessionProjections`,要么在注册表或必需 key 缺席时明确失败。贡献方可以继续通过 `ctx.inject(['sessionProjections'], ...)` 保持可选注册。
|
|
33
|
+
|
|
34
|
+
### 定义投影单元
|
|
35
|
+
|
|
36
|
+
领域为每个状态键贡献一个 `ProjectionDefinition`:一个 key、状态 schema、初始状态、同步折叠 `apply(state, event)`、可选的 `wire` 块(把状态投影为客户端视图),以及状态字段或折叠语义变化时递增的 `stateVersion`:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
const definition = {
|
|
40
|
+
key: 'todo',
|
|
41
|
+
stateSchema: todoStateSchema,
|
|
42
|
+
stateVersion: 1,
|
|
43
|
+
init: (_header, _inheritedEventCount) => ({ items: [] }),
|
|
44
|
+
apply: (state, event) => event.type === 'todo/upsert'
|
|
45
|
+
? { items: event.data.items }
|
|
46
|
+
: state,
|
|
47
|
+
wire: {
|
|
48
|
+
viewSchema: todoViewSchema,
|
|
49
|
+
view: state => ({ items: state.items }),
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`init(header, inheritedEventCount)` 同时接收轻量元数据与精确的 fork 继承切点;它不得从 `firstLiveSeq` 或 `session/end-seed` 推断该切点。`apply` 必须同步,且对与单元无关的事件必须返回同一个状态引用——引用不变意味着零下游工作。注册表用 `Object.is` 比较相邻的 `wire.view` 原始结果;对象或数组 view 若要在仅内部 state 变化时抑制发布,就必须复用引用,结构相同的新对象仍算变化。携带状态的日志事件必须携带变更后的完整状态,绝不携带裸增量。
|
|
55
|
+
|
|
56
|
+
### 注册与读取
|
|
57
|
+
|
|
58
|
+
`register(definition)` 安装单元;具有相同 key 和 `stateVersion` 的注册方共享其 cell,版本不兼容或 `stateVersion` 非法时会 throw。注册是挂在调用方 fiber 上的 effect,因此最后一个注册方卸载后会移除 key 及其缓存 cell。载体用 `snapshot(session)` 对每个客户端可见单元读取一致的同步切面——`{ asOfSeq, values }`,其中 `asOfSeq` 是所有值共同反映到的最后一个事件的 seq——并用 `onChanged(listener)` 订阅逐变更通知。`stateOf(session, key)` 读取一个单元的实时只读 host 状态,不计算无关视图。
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
const dispose = ctx.sessionProjections.register(definition)
|
|
62
|
+
const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
必须使用投影状态的领域把 `sessionProjections` 声明为 Kylin 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。
|
|
66
|
+
|
|
67
|
+
### 持久检查点
|
|
68
|
+
|
|
69
|
+
系统通过 `checkpoint(session)` 为每个单元的状态创建检查点,client-visible 与 host-only 一视同仁;同级包 [session-projection-cache](../session-projection-cache/README.zh.md) 持久化这些检查点,使冷读跳过全量日志加载。检查点水位使用 `SessionSeqCursor`(空日志为 `-1`),回放起点使用 `SessionLogOffset`;`restoreFloor` 与 `restore` 实现读取流程,且不会混淆已有事件与日志间隙。
|
|
70
|
+
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
<a id="understand-the-implementation"></a>
|
|
74
|
+
## 理解实现
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>实现细节——点击展开</summary>
|
|
78
|
+
|
|
79
|
+
本节说明驱动机制与单元约定;可观察约定已在[使用本包](#use-this-package)中说明。
|
|
80
|
+
|
|
81
|
+
### 设计理念
|
|
82
|
+
|
|
83
|
+
本包是能力 seam 的 Service Definition 与驱动角色:框架负责驱动,领域负责计算。注册表只订阅一次 `session/event`;每个已提交事件都会主动经过每个已注册单元的 `apply`(cell 在首次触达时惰性构建)。第一层 `Object.is` 闸门在 state 引用不变时跳过 view 工作;live drive 的双槽缓存复用前一个原始 view,第二层 `Object.is` 闸门在原始 view 引用不变时抑制发布。载体在切出页面切片的同一 tick 内读取 `snapshot()`,`asOfSeq` 之所以是一个一致切面正系于此;误写成异步的 view 会返回 Promise,并被 `wire.viewSchema.parse` 拒绝。
|
|
84
|
+
|
|
85
|
+
### 源码地图
|
|
86
|
+
|
|
87
|
+
| 文件 | 职责 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:`SessionProjectionRegistry` 服务、`ProjectionDefinition`、快照与检查点机制 |
|
|
90
|
+
| [`src/types.ts`](src/types.ts) | 可合并扩展的 `SessionProjectionMap` 与 `SessionProjectionStateMap` 类型表 |
|
|
91
|
+
|
|
92
|
+
### 驱动与检查点流程
|
|
93
|
+
|
|
94
|
+
一个已提交事件按注册顺序驱动每个已注册单元;原始 view 通过 `Object.is` 判定为变化的客户端可见单元会以经 schema 校验的视图与致因 seq 通知变更流。live drive 保留前后两个原始 view;snapshot 与冷读仍是彼此独立的完整读取。`checkpoint(session)` 为持久缓存返回每个单元一份独立的 `(key → {ver, seq, val})` 行;`restoreFloor` 把尾部读取锚定在最低可用水位之前一个事件处,使缩短的日志可被检出;`restore` 把持久行在存储后缀上重新折叠,丢弃任何 `ver` 不匹配或声称越过存储末尾的行。
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
<a id="further-exploration"></a>
|
|
101
|
+
## 进一步探索
|
|
102
|
+
|
|
103
|
+
当包级约定不够用时阅读以下页面。它们从单元约定逐步进入读模型子系统与持久缓存。
|
|
104
|
+
|
|
105
|
+
- [会话投影子系统](../../../docs/subsystems/session-projection.zh.md)——投影单元约定、驱动语义与生成的服务 API。
|
|
106
|
+
- [会话持久化子系统](../../../docs/subsystems/persistence.zh.md)——投影折叠其上的事件日志。
|
|
107
|
+
- [会话投影缓存](../session-projection-cache/README.zh.md)——让冷读跳过全量日志加载的持久检查点。
|
|
108
|
+
- [会话包映射](../README.zh.md)——相邻的持久化、标题与遥测包。
|
|
109
|
+
- [会话投影 RFC](../../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)——投影与命令日志的设计理由。
|
|
110
|
+
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
<a id="model-experience"></a>
|
|
114
|
+
## 模型体验
|
|
115
|
+
|
|
116
|
+
无——注册表只为已入日志的会话状态提供面向客户端的读模型,不注册任何模型可见内容。
|
|
117
|
+
|
|
118
|
+
#### KV Cache 影响
|
|
119
|
+
|
|
120
|
+
无;投影从不组装或发送提供方请求。
|
|
121
|
+
|
|
122
|
+
## 已知限制与延期工作
|
|
123
|
+
|
|
124
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
这些限制说明投影注册表在大规模下何时需要特别处理。它们是当前包约束,不是任务积压。
|
|
128
|
+
|
|
129
|
+
- **每个尾页携带每个 client-visible key**——尚无逐 key 的 opt-out 或惰性 key 请求形状;在值都是 UI 量级的全量状态时可以接受,若某领域的值变大再重议。
|
|
130
|
+
- **单元表是进程级的,因此 key 是否存在不能当作逐会话的能力信号**——任何 agent preset 注册的 key 都会出现在每个会话的快照里;客户端必须读值,不能把 key 缺席当作功能缺席。
|
|
131
|
+
- **主动驱动逐事件触达每个单元**——按构造开销很低(全量值规则与 state/view 引用闸门),但若出现热点路径,可加按单元的事件类型预过滤。
|
|
132
|
+
- **注册表 cell 只活在内存里**——重启后首次触达时靠折叠日志重建;挂载了 `qilin-session-projection-cache` 的组合改由持久行播种该折叠。
|
|
133
|
+
- **单元同步纪律只有部分可机械把关**——`wire.viewSchema.parse` 能拒绝返回 Promise 的 view,但阻塞的 `apply`、或读取撕裂的非会话状态的 `apply`,只能靠评审把关。
|
|
134
|
+
|
|
135
|
+
<a id="dev-note"></a>
|
|
136
|
+
### 开发备注
|
|
137
|
+
|
|
138
|
+
<details>
|
|
139
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
140
|
+
|
|
141
|
+
无。
|
|
142
|
+
|
|
143
|
+
</details>
|