@deepseek-ai/dsh-session-turn-outline 0.1.2-alpha.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
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,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/session/session-turn-outline/README.md
5
+ README.md: e02345d01ed4d31a3e20fd3e28d5b90d451618bb
6
+ README.zh.md: d60e70e4b6f72da076f8fca6c9942815448f92e4
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ ---
2
+ description: "Whole-log turn outline for clients and maintainers composing or debugging the turnOutline projection unit behind full-session turn navigation."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-session-turn-outline
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `dsh-session-turn-outline` serves the whole-log turn outline — every started turn with its `turn/start` seq and bounded prompt and final-response previews — as the `turnOutline` projection unit. A client that pages history in windows reads the outline to offer every turn of the session (loaded or not) and to target its backwards paging at the exact seq that brings a turn's events in. Choose it in compositions that already mount the projection registry, such as the web app bundle whose chat turn rail is the reference consumer; assemblies without the registry are unaffected and their consumers fall back to loaded-window navigation. Setup and entry semantics come first; the fold internals live in a collapsible developer section below.
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 the plugin beside the session store and the projection registry when clients should navigate every turn of a session without holding its complete event log. The unit registers only when the registry is present.
29
+
30
+ ### Composition
31
+
32
+ ```yaml
33
+ - name: '@deepseek-ai/dsh-session'
34
+ - name: '@deepseek-ai/dsh-session-projection'
35
+ - name: '@deepseek-ai/dsh-session-turn-outline'
36
+ ```
37
+
38
+ ### What an entry means
39
+
40
+ | Field | Meaning |
41
+ |---|---|
42
+ | `turn` | Host-assigned turn number from the `turn/start` payload |
43
+ | `seq` | The turn's `turn/start` event seq — paging a window back through this seq loads the whole turn |
44
+ | `prompt` | Preview of the turn's first human prompt (space-joined text blocks, collapsed whitespace, 50-character cap with a trailing ellipsis when clipped — one rail-card line); `''` until an eligible prompt lands |
45
+ | `response` | Preview of the turn's final text-bearing assistant message (same normalization, 120-character cap — up to three rail-card lines); `''` until the turn ends with assistant text |
46
+
47
+ The wire value is the complete entry array, strictly increasing by `turn` (whole-value rule): consumers replace, never merge. Prompts fill only from `user/message` events with the human `user` source, so injected context and tool results never leak into navigation; a turn whose prompt is images-only keeps `''` and consumers label it by number. The response buffers as a draft while its turn streams and commits at `turn/end`; the change feed's raw-view identity gate keeps draft-only changes quiet, so the outline pushes at most three times per turn — boundary, prompt, settled response. Preview budgets match the chat rail's loaded-turn previews, so a turn shows the same words before and after its events load.
48
+
49
+ ### Failures and recovery
50
+
51
+ The unit is inert without the projection registry: `inject` keeps the fiber pending and nothing registers, so other assemblies lack the `turnOutline` key. Unmounting the plugin removes the key, because registrations are effects on the mounting fiber. Persisted-cache rows are schema-validated on restore — including the strictly-increasing turn order — so a corrupt row is discarded instead of seeding a broken fold.
52
+
53
+ -----
54
+
55
+ <a id="understand-the-implementation"></a>
56
+ ## Understand the implementation
57
+
58
+ <details>
59
+ <summary>Implementation internals — click to expand</summary>
60
+
61
+ This section explains the fold behind the outline; the observable behavior is fully covered in [Use this package](#use-this-package).
62
+
63
+ ### Design concept
64
+
65
+ The unit is a pure fold over committed session events. `turn/start` — not the prompt `user/message` — anchors each entry because its seq is the load-through target for a jump: the agent loop logs `turn/start` before the turn's prompt and steps, so a window paged back through that seq contains the whole turn. The prompt fills from the first human `user/message`, and only while the newest entry is still empty — later human messages in the same turn (steering) keep the first preview. The response cannot fill the same way (`turn/end` carries no text), so each text-bearing `assistant/message` overwrites a state draft and `turn/end` commits the survivor — the newest text, which is the loaded rail's `findLast` semantic.
66
+
67
+ ### Source map
68
+
69
+ | File | Role |
70
+ |---|---|
71
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `inject`, unit registration on the mounting fiber |
72
+ | [`src/projection.ts`](src/projection.ts) | The fold: entry append, preview fill, wire view |
73
+ | [`src/types.ts`](src/types.ts) | One home of the `turnOutline` projection-key declaration and entry types |
74
+
75
+ ### Fold rules
76
+
77
+ - Uninteresting events return the same state reference, and draft-only changes keep the `turns` array's identity; the registry's two `Object.is` gates then hold the feed to at most three pushes per turn.
78
+ - A `turn/start` that does not advance the turn number is skipped, keeping the outline sorted; a retried boundary's previews then land on the standing entry.
79
+ - The wire view projects `state.turns`; the persisted-cache state schema wraps the wire schema with the draft field.
80
+
81
+ </details>
82
+
83
+ -----
84
+
85
+ <a id="further-exploration"></a>
86
+ ## Further Exploration
87
+
88
+ Read these pages when the unit's contract is not enough. They move from the registry that drives units to adjacent session packages.
89
+
90
+ - [Session projection subsystem](../../../docs/subsystems/session-projection.md) — the registry that drives units and serves snapshot and change-feed values.
91
+ - [Session projection registry package](../session-projection/README.md) — the registry contract units register against.
92
+ - [Session package map](../README.md) — adjacent persistence, projection, title, and telemetry packages.
93
+
94
+ -----
95
+
96
+ <a id="model-experience"></a>
97
+ ## Model Experience
98
+
99
+ None, as the turnOutline unit folds already-logged turn boundaries into a client-facing read model and registers nothing model-facing.
100
+
101
+ #### KV Cache effect
102
+
103
+ None; the package never assembles or sends provider requests.
104
+
105
+ ## Known Limitations and Deferred Work
106
+
107
+ <a id="known-limitations-and-deferred-work"></a>
108
+
109
+
110
+ These limits define what the outline describes and when the unit is absent. They are current package constraints.
111
+
112
+ - **The wire value grows with the session** — every push carries the complete outline (whole-value rule), up to ~600 bytes per turn at full CJK budgets and typically far less; splitting previews into an on-demand read is deferred until sessions with many thousands of turns need it.
113
+ - **The response previews only settled turns** — it commits at `turn/end`, so an open turn (or one whose end never logged) shows a prompt-only preview until the boundary lands.
114
+ - **A turn without eligible text keeps `''`** — images-only and command-only turns are navigable but labeled by number, and a turn whose steps emit no text gets no response preview.
115
+ - **Mounted only where the projection registry is composed** — other assemblies serve no `turnOutline` key, and their consumers fall back to loaded-window navigation.
116
+
117
+ <a id="dev-note"></a>
118
+ ### Dev Note
119
+
120
+ <details>
121
+ <summary>Working context for maintainers — click to expand</summary>
122
+
123
+ None.
124
+
125
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,125 @@
1
+ ---
2
+ description: "面向组合或调试 turnOutline 投影单元的客户端与维护者的全量轮次大纲说明,支撑整会话轮次导航。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-session-turn-outline
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `dsh-session-turn-outline` 以 `turnOutline` 投影单元提供全日志的轮次大纲——每个已开始的轮次连同其 `turn/start` seq 以及有界的提示词与最终回复预览。按窗口分页历史的客户端读取大纲即可提供会话的每一轮(无论是否已加载),并把向后分页精确定位到能载入某轮事件的 seq。在已挂载投影注册表的组合中选择它,例如以聊天轮次导航栏为参考消费者的 Web 应用包;没有注册表的装配不受影响,其消费者回退到仅按已加载窗口导航。用法与条目语义在前;折叠内部细节放在下方可折叠的开发者章节中。
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
+ 当客户端需要在不持有完整事件日志的情况下导航会话的每一轮时,在会话存储与投影注册表旁挂载此插件。只有存在注册表时单元才会注册。
29
+
30
+ ### 组合
31
+
32
+ ```yaml
33
+ - name: '@deepseek-ai/dsh-session'
34
+ - name: '@deepseek-ai/dsh-session-projection'
35
+ - name: '@deepseek-ai/dsh-session-turn-outline'
36
+ ```
37
+
38
+ ### 各字段含义
39
+
40
+ | 字段 | 含义 |
41
+ |---|---|
42
+ | `turn` | `turn/start` 载荷里的宿主分配轮次号 |
43
+ | `seq` | 该轮 `turn/start` 事件的 seq——窗口向后分页越过此 seq 即载入整轮 |
44
+ | `prompt` | 该轮首条人类提示词的预览(文本块以空格连接、空白折叠、50 字符封顶且截断时补省略号——即导航卡片一行);合格提示词落日志前为 `''` |
45
+ | `response` | 该轮最后一条带文本的助手消息的预览(同样的归一化、120 字符封顶——即卡片至多三行);轮次带着助手文本结束前为 `''` |
46
+
47
+ wire 值是按 `turn` 严格递增的完整条目数组(整值规则):消费者整体替换,从不合并。提示词只从带人类 `user` 来源的 `user/message` 事件填充,注入的上下文与工具结果绝不进入导航;纯图片提示词的轮次保持 `''`,消费者按轮次号标注。回复在轮次流式期间缓冲为草稿、在 `turn/end` 落定;变更流的原始视图身份门让纯草稿变化保持安静,因此大纲每轮至多推送三次——开轮、提示词、落定回复。预览预算与聊天导航栏已加载轮次的预览一致,同一轮在事件载入前后显示相同的文字。
48
+
49
+ ### 失败与恢复
50
+
51
+ 没有投影注册表时单元是惰性的:`inject` 使 fiber 保持挂起,不注册任何内容,因此其他装配缺少 `turnOutline` 键。卸载插件会移除该键,因为注册是挂载 fiber 上的 effect。持久缓存行在恢复时经受 schema 校验——包括轮次严格递增的顺序——损坏的行被丢弃而不会喂坏折叠。
52
+
53
+ -----
54
+
55
+ <a id="understand-the-implementation"></a>
56
+ ## 理解实现
57
+
58
+ <details>
59
+ <summary>实现细节——点击展开</summary>
60
+
61
+ 本节解释大纲背后的折叠;可观察行为已在[使用本包](#use-this-package)中完整说明。
62
+
63
+ ### 设计理念
64
+
65
+ 该单元是对已提交会话事件的纯折叠。锚定每个条目的是 `turn/start` 而非提示词 `user/message`,因为它的 seq 就是跳转的载入目标:agent loop 先记 `turn/start` 再记该轮的提示词与步骤,窗口向后分页越过该 seq 即包含整轮。提示词由首条人类 `user/message` 填充,且仅当最新条目仍为空时——同一轮内后续的人类消息(steering)保留首个预览。回复无法同样填充(`turn/end` 不带文本),所以每条带文本的 `assistant/message` 覆写状态里的草稿,`turn/end` 提交幸存者——最新的文本,与已加载导航栏 `findLast` 的语义一致。
66
+
67
+ ### 源码地图
68
+
69
+ | 文件 | 职责 |
70
+ |---|---|
71
+ | [`src/index.ts`](src/index.ts) | 插件入口:`inject`、在挂载 fiber 上注册单元 |
72
+ | [`src/projection.ts`](src/projection.ts) | 折叠:条目追加、预览填充、wire 视图 |
73
+ | [`src/types.ts`](src/types.ts) | `turnOutline` 投影键声明与条目类型的唯一归属 |
74
+
75
+ ### 折叠规则
76
+
77
+ - 不相关事件返回同一状态引用,纯草稿变化保持 `turns` 数组身份不变;注册表的两道 `Object.is` 门由此把变更流压到每轮至多三次推送。
78
+ - 未推进轮次号的 `turn/start` 被跳过,保持大纲有序;重试边界的预览随后落在既有条目上。
79
+ - wire 视图投影 `state.turns`;持久缓存的状态 schema 在 wire schema 外再包一个草稿字段。
80
+
81
+ </details>
82
+
83
+ -----
84
+
85
+ <a id="further-exploration"></a>
86
+ ## 进一步探索
87
+
88
+ 当单元约定不够用时阅读以下页面。它们从驱动单元的注册表逐步进入相邻的会话包。
89
+
90
+ - [会话投影子系统](../../../docs/subsystems/session-projection.zh.md)——驱动单元并提供快照与变更流值的注册表。
91
+ - [会话投影注册表包](../session-projection/README.zh.md)——单元注册所依据的注册表约定。
92
+ - [会话包映射](../README.zh.md)——相邻的持久化、投影、标题与遥测包。
93
+
94
+ -----
95
+
96
+ <a id="model-experience"></a>
97
+ ## 模型体验
98
+
99
+ 无,因为 turnOutline 单元把已写入日志的轮次边界折叠成面向客户端的读模型,不注册任何面向模型的内容。
100
+
101
+ #### KV Cache 影响
102
+
103
+ 无;本包从不组装或发送提供方请求。
104
+
105
+ ## 已知限制与延期工作
106
+
107
+ <a id="known-limitations-and-deferred-work"></a>
108
+
109
+
110
+ 这些限制说明大纲描述什么、单元何时缺失。它们是当前包约束。
111
+
112
+ - **wire 值随会话增长**——每次推送携带完整大纲(整值规则),全中文预算下每轮上限约 600 字节、通常远小于此;把预览拆成按需读取推迟到数千轮量级的会话真正需要时。
113
+ - **回复只预览已落定的轮次**——它在 `turn/end` 提交,进行中的轮次(或从未记下结束边界的轮次)在边界落地前只有提示词预览。
114
+ - **没有合格文本的轮次保持 `''`**——纯图片、纯命令的轮次可导航但按轮次号标注,步骤全程不产文本的轮次没有回复预览。
115
+ - **仅在组合了投影注册表时挂载**——其他装配不提供 `turnOutline` 键,其消费者回退到仅按已加载窗口导航。
116
+
117
+ <a id="dev-note"></a>
118
+ ### 开发备注
119
+
120
+ <details>
121
+ <summary>维护者的工作上下文——点击展开</summary>
122
+
123
+ 无。
124
+
125
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ //#region lib/types/projection.js
3
+ /**
4
+ * The `turnOutline` projection unit: a pure fold of `turn/start` boundaries,
5
+ * first human prompts, and final assistant responses into the whole-log turn
6
+ * outline the chat rail renders for turns outside a client's paged event
7
+ * window.
8
+ *
9
+ * `turn/start` — not the prompt `user/message` — anchors each entry because
10
+ * its seq is the load-through target for a jump: the loop logs `turn/start`
11
+ * before the turn's prompt and steps, so a window paged back through that seq
12
+ * contains the whole turn. Previews mirror the rail's loaded-turn previews
13
+ * (space-joined text blocks, collapsed whitespace, an ellipsis when clipped)
14
+ * with budgets sized to the rail card's clamps — one prompt line, up to three
15
+ * response lines — so a turn shows the same words before and after its events
16
+ * load. The response commits at `turn/end` from a draft of the newest
17
+ * text-bearing assistant message; draft-only applies keep the `turns` array's
18
+ * identity, so the identity-gated change feed pushes at most three times per
19
+ * turn (boundary, prompt, response).
20
+ *
21
+ * @module @deepseek-ai/dsh-session-turn-outline/projection
22
+ */
23
+ /** Prompt budget: one rail-card line (13px over ~276px), ASCII worst case included. */
24
+ const PROMPT_PREVIEW_LIMIT = 50;
25
+ /** Response budget: three rail-card lines (12px over ~276px). */
26
+ const RESPONSE_PREVIEW_LIMIT = 120;
27
+ /** Space-join text blocks, collapse whitespace, and cap at `limit` with a trailing ellipsis when clipped. */
28
+ function preview(content, limit) {
29
+ let text = "";
30
+ let unread = false;
31
+ for (const block of content) {
32
+ if (block.type !== "text") continue;
33
+ if (text.length >= limit * 2) {
34
+ unread = true;
35
+ break;
36
+ }
37
+ const clipped = block.text.length > limit * 2;
38
+ const chunk = clipped ? block.text.slice(0, limit * 2) : block.text;
39
+ text += text === "" ? chunk : ` ${chunk}`;
40
+ if (clipped) {
41
+ unread = true;
42
+ break;
43
+ }
44
+ }
45
+ const normalized = text.replace(/\s+/g, " ").trim();
46
+ if (normalized.length > limit - 1) return `${normalized.slice(0, limit - 1).trimEnd()}…`;
47
+ return unread ? `${normalized}…` : normalized;
48
+ }
49
+ const turnOutlineEntriesSchema = z.array(z.object({
50
+ turn: z.number().int().nonnegative(),
51
+ seq: z.number().int().nonnegative(),
52
+ prompt: z.string().max(PROMPT_PREVIEW_LIMIT),
53
+ response: z.string().max(RESPONSE_PREVIEW_LIMIT)
54
+ }).strict()).superRefine((turns, context) => {
55
+ let previous = -1;
56
+ for (const entry of turns) {
57
+ if (entry.turn <= previous) {
58
+ context.addIssue({
59
+ code: "custom",
60
+ message: "turn outline entries must be strictly increasing by turn"
61
+ });
62
+ return;
63
+ }
64
+ previous = entry.turn;
65
+ }
66
+ });
67
+ const turnOutlineStateSchema = z.object({
68
+ turns: turnOutlineEntriesSchema,
69
+ draft: z.string().max(RESPONSE_PREVIEW_LIMIT)
70
+ }).strict();
71
+ const EMPTY_OUTLINE = {
72
+ turns: [],
73
+ draft: ""
74
+ };
75
+ /** The `turnOutline` unit registered on `ctx.sessionProjections` (exported for the unit spec). */
76
+ const turnOutlineProjectionDefinition = {
77
+ key: "turnOutline",
78
+ stateVersion: 2,
79
+ stateSchema: turnOutlineStateSchema,
80
+ init: () => EMPTY_OUTLINE,
81
+ apply: (state, event) => {
82
+ switch (event.type) {
83
+ case "turn/start": {
84
+ const last = state.turns.at(-1);
85
+ if (last !== void 0 && event.data.turn <= last.turn) return state;
86
+ return {
87
+ turns: [...state.turns, {
88
+ turn: event.data.turn,
89
+ seq: event.seq,
90
+ prompt: "",
91
+ response: ""
92
+ }],
93
+ draft: ""
94
+ };
95
+ }
96
+ case "user/message": {
97
+ if (event.data.source.kind !== "user") return state;
98
+ const last = state.turns.at(-1);
99
+ if (last === void 0 || last.prompt !== "") return state;
100
+ const prompt = preview(event.data.content, PROMPT_PREVIEW_LIMIT);
101
+ if (prompt === "") return state;
102
+ return {
103
+ turns: [...state.turns.slice(0, -1), {
104
+ ...last,
105
+ prompt
106
+ }],
107
+ draft: state.draft
108
+ };
109
+ }
110
+ case "assistant/message": {
111
+ const draft = preview(event.data.message.content, RESPONSE_PREVIEW_LIMIT);
112
+ if (draft === "" || draft === state.draft) return state;
113
+ return {
114
+ turns: state.turns,
115
+ draft
116
+ };
117
+ }
118
+ case "turn/end": {
119
+ if (state.draft === "") return state;
120
+ const last = state.turns.at(-1);
121
+ if (last === void 0 || last.response === state.draft) return {
122
+ turns: state.turns,
123
+ draft: ""
124
+ };
125
+ return {
126
+ turns: [...state.turns.slice(0, -1), {
127
+ ...last,
128
+ response: state.draft
129
+ }],
130
+ draft: ""
131
+ };
132
+ }
133
+ default: return state;
134
+ }
135
+ },
136
+ wire: {
137
+ viewSchema: turnOutlineEntriesSchema,
138
+ view: (state) => state.turns
139
+ }
140
+ };
141
+ //#endregion
142
+ //#region lib/types/index.js
143
+ /**
144
+ * Function plugin registering the `turnOutline` projection unit: the
145
+ * whole-log turn outline (turn number, `turn/start` seq, bounded prompt
146
+ * preview) served through the session-projection seam — registry snapshot,
147
+ * change feed, and every projection carrier — so a client can offer every
148
+ * turn of a session and target history paging at exact seqs without holding
149
+ * the events. The plugin owns only the fold; delivery is the seam's.
150
+ *
151
+ * @module @deepseek-ai/dsh-session-turn-outline
152
+ */
153
+ /** Cordis plugin name. */
154
+ const name = "session-turn-outline";
155
+ /** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */
156
+ const inject = ["sessionProjections"];
157
+ /**
158
+ * Register the `turnOutline` unit; the registration is an effect on this
159
+ * plugin's fiber, so unloading removes the key.
160
+ * @param ctx - registrant context carrying the projection registry.
161
+ */
162
+ function apply(ctx) {
163
+ ctx.sessionProjections.register(turnOutlineProjectionDefinition);
164
+ }
165
+ //#endregion
166
+ export { apply, inject, name };
@@ -0,0 +1,28 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-session-turn-outline`.
4
+ * @module @deepseek-ai/dsh-session-turn-outline/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-session-turn-outline";
7
+ /** Cordis companion plugin name. */
8
+ const name = "session-turn-outline-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: the package owns a single pure projection fold whose
13
+ * wire payload is schema-validated by the projection registry at every
14
+ * snapshot and change-feed emission (including the strictly-increasing turn
15
+ * order the fold maintains), and the event relations the fold relies on
16
+ * (host-assigned monotonic turn numbers on `turn/start`, the turn's prompt
17
+ * `user/message` logged after its boundary) are owned and runtime-checked by
18
+ * dsh-agent-loop and the session surface, not here.
19
+ */
20
+ const install = () => {};
21
+ /**
22
+ * Register this package's invariant companion.
23
+ * @param ctx - Cordis context carrying the invariant service.
24
+ * @returns the installed registration's disposer after setup succeeds.
25
+ */
26
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
27
+ //#endregion
28
+ export { apply, inject, name };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Client-namespace projection of the turn-outline domain: a pure re-export
3
+ * of the package's types outlet. Client code imports ONLY the client
4
+ * namespace (repo discipline), so `./client` projects the same single-source
5
+ * content `./types` serves to host consumers — zero duplication.
6
+ *
7
+ * @module @deepseek-ai/dsh-session-turn-outline/client
8
+ */
9
+ export type * from './types.ts';
10
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Client-namespace projection of the turn-outline domain: a pure re-export
3
+ * of the package's types outlet. Client code imports ONLY the client
4
+ * namespace (repo discipline), so `./client` projects the same single-source
5
+ * content `./types` serves to host consumers — zero duplication.
6
+ *
7
+ * @module @deepseek-ai/dsh-session-turn-outline/client
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Function plugin registering the `turnOutline` projection unit: the
3
+ * whole-log turn outline (turn number, `turn/start` seq, bounded prompt
4
+ * preview) served through the session-projection seam — registry snapshot,
5
+ * change feed, and every projection carrier — so a client can offer every
6
+ * turn of a session and target history paging at exact seqs without holding
7
+ * the events. The plugin owns only the fold; delivery is the seam's.
8
+ *
9
+ * @module @deepseek-ai/dsh-session-turn-outline
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ export type * from './types.ts';
13
+ /** Cordis plugin name. */
14
+ export declare const name = "session-turn-outline";
15
+ /** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */
16
+ export declare const inject: string[];
17
+ /**
18
+ * Register the `turnOutline` unit; the registration is an effect on this
19
+ * plugin's fiber, so unloading removes the key.
20
+ * @param ctx - registrant context carrying the projection registry.
21
+ */
22
+ export declare function apply(ctx: Context): void;
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Function plugin registering the `turnOutline` projection unit: the
3
+ * whole-log turn outline (turn number, `turn/start` seq, bounded prompt
4
+ * preview) served through the session-projection seam — registry snapshot,
5
+ * change feed, and every projection carrier — so a client can offer every
6
+ * turn of a session and target history paging at exact seqs without holding
7
+ * the events. The plugin owns only the fold; delivery is the seam's.
8
+ *
9
+ * @module @deepseek-ai/dsh-session-turn-outline
10
+ */
11
+ import { turnOutlineProjectionDefinition } from "./projection.js";
12
+ /** Cordis plugin name. */
13
+ export const name = 'session-turn-outline';
14
+ /** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */
15
+ export const inject = ['sessionProjections'];
16
+ /**
17
+ * Register the `turnOutline` unit; the registration is an effect on this
18
+ * plugin's fiber, so unloading removes the key.
19
+ * @param ctx - registrant context carrying the projection registry.
20
+ */
21
+ export function apply(ctx) {
22
+ ctx.sessionProjections.register(turnOutlineProjectionDefinition);
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-session-turn-outline`.
3
+ * @module @deepseek-ai/dsh-session-turn-outline/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "session-turn-outline-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-session-turn-outline`.
3
+ * @module @deepseek-ai/dsh-session-turn-outline/invariant
4
+ */
5
+ const PACKAGE_NAME = '@deepseek-ai/dsh-session-turn-outline';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'session-turn-outline-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * No runtime invariant: the package owns a single pure projection fold whose
12
+ * wire payload is schema-validated by the projection registry at every
13
+ * snapshot and change-feed emission (including the strictly-increasing turn
14
+ * order the fold maintains), and the event relations the fold relies on
15
+ * (host-assigned monotonic turn numbers on `turn/start`, the turn's prompt
16
+ * `user/message` logged after its boundary) are owned and runtime-checked by
17
+ * dsh-agent-loop and the session surface, not here.
18
+ */
19
+ const install = () => { };
20
+ /**
21
+ * Register this package's invariant companion.
22
+ * @param ctx - Cordis context carrying the invariant service.
23
+ * @returns the installed registration's disposer after setup succeeds.
24
+ */
25
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
26
+ /* jscpd:ignore-end */
27
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The `turnOutline` projection unit: a pure fold of `turn/start` boundaries,
3
+ * first human prompts, and final assistant responses into the whole-log turn
4
+ * outline the chat rail renders for turns outside a client's paged event
5
+ * window.
6
+ *
7
+ * `turn/start` — not the prompt `user/message` — anchors each entry because
8
+ * its seq is the load-through target for a jump: the loop logs `turn/start`
9
+ * before the turn's prompt and steps, so a window paged back through that seq
10
+ * contains the whole turn. Previews mirror the rail's loaded-turn previews
11
+ * (space-joined text blocks, collapsed whitespace, an ellipsis when clipped)
12
+ * with budgets sized to the rail card's clamps — one prompt line, up to three
13
+ * response lines — so a turn shows the same words before and after its events
14
+ * load. The response commits at `turn/end` from a draft of the newest
15
+ * text-bearing assistant message; draft-only applies keep the `turns` array's
16
+ * identity, so the identity-gated change feed pushes at most three times per
17
+ * turn (boundary, prompt, response).
18
+ *
19
+ * @module @deepseek-ai/dsh-session-turn-outline/projection
20
+ */
21
+ import { z } from 'zod';
22
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
23
+ import type { TurnOutlineEntry, TurnOutlineState } from './types.ts';
24
+ /** The `turnOutline` unit registered on `ctx.sessionProjections` (exported for the unit spec). */
25
+ export declare const turnOutlineProjectionDefinition: {
26
+ key: "turnOutline";
27
+ stateVersion: number;
28
+ stateSchema: z.ZodType<TurnOutlineState, unknown, z.core.$ZodTypeInternals<TurnOutlineState, unknown>>;
29
+ init: () => TurnOutlineState;
30
+ apply: (state: NoInfer<TurnOutlineState>, event: SessionEvent) => TurnOutlineState;
31
+ wire: {
32
+ viewSchema: z.ZodType<readonly TurnOutlineEntry[], unknown, z.core.$ZodTypeInternals<readonly TurnOutlineEntry[], unknown>>;
33
+ view: (state: NoInfer<TurnOutlineState>) => readonly TurnOutlineEntry[];
34
+ };
35
+ };
36
+ //# sourceMappingURL=projection.d.ts.map
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The `turnOutline` projection unit: a pure fold of `turn/start` boundaries,
3
+ * first human prompts, and final assistant responses into the whole-log turn
4
+ * outline the chat rail renders for turns outside a client's paged event
5
+ * window.
6
+ *
7
+ * `turn/start` — not the prompt `user/message` — anchors each entry because
8
+ * its seq is the load-through target for a jump: the loop logs `turn/start`
9
+ * before the turn's prompt and steps, so a window paged back through that seq
10
+ * contains the whole turn. Previews mirror the rail's loaded-turn previews
11
+ * (space-joined text blocks, collapsed whitespace, an ellipsis when clipped)
12
+ * with budgets sized to the rail card's clamps — one prompt line, up to three
13
+ * response lines — so a turn shows the same words before and after its events
14
+ * load. The response commits at `turn/end` from a draft of the newest
15
+ * text-bearing assistant message; draft-only applies keep the `turns` array's
16
+ * identity, so the identity-gated change feed pushes at most three times per
17
+ * turn (boundary, prompt, response).
18
+ *
19
+ * @module @deepseek-ai/dsh-session-turn-outline/projection
20
+ */
21
+ import { z } from 'zod';
22
+ /** Prompt budget: one rail-card line (13px over ~276px), ASCII worst case included. */
23
+ const PROMPT_PREVIEW_LIMIT = 50;
24
+ /** Response budget: three rail-card lines (12px over ~276px). */
25
+ const RESPONSE_PREVIEW_LIMIT = 120;
26
+ /** Space-join text blocks, collapse whitespace, and cap at `limit` with a trailing ellipsis when clipped. */
27
+ function preview(content, limit) {
28
+ let text = '';
29
+ let unread = false;
30
+ for (const block of content) {
31
+ if (block.type !== 'text')
32
+ continue;
33
+ if (text.length >= limit * 2) {
34
+ unread = true;
35
+ break;
36
+ }
37
+ // Per-block bound: the fold runs on every message event, so a single
38
+ // multi-megabyte block must not be concatenated (and regex-normalized)
39
+ // whole for a preview this short.
40
+ const clipped = block.text.length > limit * 2;
41
+ const chunk = clipped ? block.text.slice(0, limit * 2) : block.text;
42
+ text += text === '' ? chunk : ` ${chunk}`;
43
+ if (clipped) {
44
+ unread = true;
45
+ break;
46
+ }
47
+ }
48
+ const normalized = text.replace(/\s+/g, ' ').trim();
49
+ if (normalized.length > limit - 1)
50
+ return `${normalized.slice(0, limit - 1).trimEnd()}…`;
51
+ return unread ? `${normalized}…` : normalized;
52
+ }
53
+ const turnOutlineEntriesSchema = z.array(z.object({
54
+ turn: z.number().int().nonnegative(),
55
+ seq: z.number().int().nonnegative(),
56
+ prompt: z.string().max(PROMPT_PREVIEW_LIMIT),
57
+ response: z.string().max(RESPONSE_PREVIEW_LIMIT),
58
+ }).strict()).superRefine((turns, context) => {
59
+ let previous = -1;
60
+ for (const entry of turns) {
61
+ if (entry.turn <= previous) {
62
+ context.addIssue({ code: 'custom', message: 'turn outline entries must be strictly increasing by turn' });
63
+ return;
64
+ }
65
+ previous = entry.turn;
66
+ }
67
+ });
68
+ const turnOutlineStateSchema = z.object({
69
+ turns: turnOutlineEntriesSchema,
70
+ draft: z.string().max(RESPONSE_PREVIEW_LIMIT),
71
+ }).strict();
72
+ const EMPTY_OUTLINE = { turns: [], draft: '' };
73
+ /** The `turnOutline` unit registered on `ctx.sessionProjections` (exported for the unit spec). */
74
+ export const turnOutlineProjectionDefinition = {
75
+ key: 'turnOutline',
76
+ stateVersion: 2,
77
+ stateSchema: turnOutlineStateSchema,
78
+ init: () => EMPTY_OUTLINE,
79
+ apply: (state, event) => {
80
+ // Every uninteresting event returns the same reference (Object.is gates
81
+ // the drive), and draft-only changes keep `turns` identity (the raw-view
82
+ // identity gate then keeps the change feed quiet).
83
+ switch (event.type) {
84
+ case 'turn/start': {
85
+ const last = state.turns.at(-1);
86
+ // Order guard: a boundary that does not advance the turn number keeps
87
+ // the outline sorted, and a retried turn's previews land on the
88
+ // standing entry.
89
+ if (last !== undefined && event.data.turn <= last.turn)
90
+ return state;
91
+ return {
92
+ turns: [...state.turns, { turn: event.data.turn, seq: event.seq, prompt: '', response: '' }],
93
+ draft: '',
94
+ };
95
+ }
96
+ case 'user/message': {
97
+ // Only the newest turn can still be waiting for its opening human
98
+ // prompt; later human messages in the same turn (steering) keep the
99
+ // first preview.
100
+ if (event.data.source.kind !== 'user')
101
+ return state;
102
+ const last = state.turns.at(-1);
103
+ if (last === undefined || last.prompt !== '')
104
+ return state;
105
+ const prompt = preview(event.data.content, PROMPT_PREVIEW_LIMIT);
106
+ if (prompt === '')
107
+ return state;
108
+ return { turns: [...state.turns.slice(0, -1), { ...last, prompt }], draft: state.draft };
109
+ }
110
+ case 'assistant/message': {
111
+ // Newest text-bearing message wins; the buffer commits at turn/end.
112
+ const draft = preview(event.data.message.content, RESPONSE_PREVIEW_LIMIT);
113
+ if (draft === '' || draft === state.draft)
114
+ return state;
115
+ return { turns: state.turns, draft };
116
+ }
117
+ case 'turn/end': {
118
+ if (state.draft === '')
119
+ return state;
120
+ const last = state.turns.at(-1);
121
+ if (last === undefined || last.response === state.draft)
122
+ return { turns: state.turns, draft: '' };
123
+ return { turns: [...state.turns.slice(0, -1), { ...last, response: state.draft }], draft: '' };
124
+ }
125
+ default:
126
+ return state;
127
+ }
128
+ },
129
+ wire: {
130
+ viewSchema: turnOutlineEntriesSchema,
131
+ view: state => state.turns,
132
+ },
133
+ };
134
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure types of the turn-outline domain: the ONE home of the `turnOutline`
3
+ * projection-key declaration, free of this package's host-side value imports
4
+ * (zod, the projection definition). Host consumers import `./types`; client
5
+ * aggregates import `./client`, which re-exports this module.
6
+ *
7
+ * @module @deepseek-ai/dsh-session-turn-outline/types
8
+ */
9
+ export {};
10
+ /** One started turn's outline facts, independent of what a client has paged in. */
11
+ export interface TurnOutlineEntry {
12
+ /** Host-assigned turn number (the `turn/start` payload). */
13
+ readonly turn: number;
14
+ /** The turn's `turn/start` event seq — paging a window back through this seq loads the whole turn. */
15
+ readonly seq: number;
16
+ /** Bounded first-human-prompt preview (one rail-card line); `''` until an eligible prompt lands. */
17
+ readonly prompt: string;
18
+ /** Bounded final-response preview (up to three rail-card lines); `''` until the turn ends with assistant text. */
19
+ readonly response: string;
20
+ }
21
+ /**
22
+ * Fold state: the served entries plus the open turn's response draft. The
23
+ * draft buffers the newest text-bearing assistant message until `turn/end`
24
+ * commits it, and the wire view projects only `turns` — draft-only applies
25
+ * keep that array's identity, so the change feed stays quiet between turn
26
+ * boundaries.
27
+ */
28
+ export interface TurnOutlineState {
29
+ /** Started turns in ascending turn order. */
30
+ readonly turns: readonly TurnOutlineEntry[];
31
+ /** Newest text-bearing assistant preview of the open turn; `''` outside one. */
32
+ readonly draft: string;
33
+ }
34
+ declare module '@deepseek-ai/dsh-session-projection/types' {
35
+ interface SessionProjectionStateMap {
36
+ /** Whole-log turn outline fold state (entries plus the open turn's response draft). */
37
+ turnOutline: TurnOutlineState;
38
+ }
39
+ interface SessionProjectionMap {
40
+ /** Every started turn with its `turn/start` seq and bounded previews, strictly increasing by turn; see {@link TurnOutlineEntry}. */
41
+ turnOutline: readonly TurnOutlineEntry[];
42
+ }
43
+ }
44
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Pure types of the turn-outline domain: the ONE home of the `turnOutline`
3
+ * projection-key declaration, free of this package's host-side value imports
4
+ * (zod, the projection definition). Host consumers import `./types`; client
5
+ * aggregates import `./client`, which re-exports this module.
6
+ *
7
+ * @module @deepseek-ai/dsh-session-turn-outline/types
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-session-turn-outline",
3
+ "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness",
4
+ "version": "0.1.2-alpha.3",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/session/session-turn-outline"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./types": {
26
+ "types": "./lib/types/types.d.ts",
27
+ "default": "./lib/types/types.js"
28
+ },
29
+ "./client": {
30
+ "types": "./lib/types/client.d.ts",
31
+ "default": "./lib/types/client.js"
32
+ },
33
+ "./src/*": "./src/*",
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "lib/index.js",
38
+ "lib/invariant.js",
39
+ "lib/types/**/*.js",
40
+ "lib/types/**/*.d.ts"
41
+ ],
42
+ "license": "MIT",
43
+ "peerDependencies": {
44
+ "@deepseek-ai/cordis": "^4.0.2",
45
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
46
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.3",
47
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
48
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3"
49
+ },
50
+ "dependencies": {
51
+ "zod": "^4.4.3"
52
+ },
53
+ "devDependencies": {
54
+ "@deepseek-ai/cordis": "^4.0.2",
55
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
56
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
57
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
58
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.3",
59
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
60
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3"
61
+ }
62
+ }