@qilin-agent/session-projection 0.0.0-stage → 3.1.4

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 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.
@@ -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-agent-session-projection:
9
+ en: ddc6501449093fcc
10
+ zh: f4a9cf15b1ab40d2
11
+ /qilin-agent-session-projection/summary:
12
+ en: 6a551683a454e074
13
+ zh: 67cf75bc676c3d3c
14
+ /qilin-agent-session-projection/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /qilin-agent-session-projection/use-this-package:
18
+ en: b5b21529bff8092b
19
+ zh: b2bcd77db68672bd
20
+ /qilin-agent-session-projection/use-this-package/when-to-choose-it:
21
+ en: 90b7c26cb45be52a
22
+ zh: 386e294ec09607c1
23
+ /qilin-agent-session-projection/use-this-package/define-a-projection-unit:
24
+ en: 09758d1475fad86e
25
+ zh: 473637cb34aebe72
26
+ /qilin-agent-session-projection/use-this-package/register-and-read:
27
+ en: 1ee07a6c1958def3
28
+ zh: 1c20493328ac4dbe
29
+ /qilin-agent-session-projection/use-this-package/persisted-checkpoints:
30
+ en: bf5349012ae6dd0c
31
+ zh: b1b941935b1cc773
32
+ /qilin-agent-session-projection/understand-the-implementation:
33
+ en: 3397f58d6f49bfda
34
+ zh: efb85d382e25574d
35
+ /qilin-agent-session-projection/understand-the-implementation/design-concept:
36
+ en: 8afa0fdb49898f6d
37
+ zh: 6b2da34455963935
38
+ /qilin-agent-session-projection/understand-the-implementation/source-map:
39
+ en: b0263d26c5a65d7a
40
+ zh: 2e58d3181dde901c
41
+ /qilin-agent-session-projection/understand-the-implementation/drive-and-checkpoint-flow:
42
+ en: 00136880bb586403
43
+ zh: bd35f645111c068c
44
+ /qilin-agent-session-projection/further-exploration:
45
+ en: f712858f4221e4ad
46
+ zh: 3f215a2a018502be
47
+ /qilin-agent-session-projection/model-experience:
48
+ en: a24045627fb43695
49
+ zh: a8465196c3948796
50
+ /qilin-agent-session-projection/model-experience/kv-cache-effect:
51
+ en: 8a592dae9bc946ab
52
+ zh: eca939995bb5f8e0
53
+ /qilin-agent-session-projection/known-limitations-and-deferred-work:
54
+ en: 3dad1fbb8061fac8
55
+ zh: bd5cc28816085953
56
+ /qilin-agent-session-projection/known-limitations-and-deferred-work/dev-note:
57
+ en: e67f633dc6fe7773
58
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
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
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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>