@x1a0f3n9/dsh-session-turn-outline 0.1.5-rc.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: 58a71c6977aaaae8663ea96651c50317a034bb8c
6
+ README.zh.md: 49c0aa3c4f7a9b4950d855359d8c789e375e0806
package/README.md ADDED
@@ -0,0 +1,126 @@
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
+ # @x1a0f3n9/dsh-session-turn-outline
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ This package gives history clients a whole-session outline of every started turn, including bounded prompt and settled-response previews. Clients can navigate turns that are not yet loaded and page backward from the exact event sequence needed to load a selected turn. It fits assemblies that provide session projections; elsewhere, clients continue using loaded-window navigation. Previews exclude injected context and tool results, and a response appears only after its turn settles.
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: '@x1a0f3n9/dsh-session'
34
+ - name: '@x1a0f3n9/dsh-session-projection'
35
+ - name: '@x1a0f3n9/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
+ | — | No runtime invariant companion is published: the package owns one pure projection fold, `session-projection` schema-validates its served values, and re-folding the same log would duplicate the implementation instead of comparing independently maintained observations; session and agent-loop own turn-boundary ordering. |
75
+
76
+ ### Fold rules
77
+
78
+ - 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.
79
+ - 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.
80
+ - The wire view projects `state.turns`; the persisted-cache state schema wraps the wire schema with the draft field.
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## Further Exploration
88
+
89
+ Read these pages when the unit's contract is not enough. They move from the registry that drives units to adjacent session packages.
90
+
91
+ - [Session projection subsystem](../../../docs/subsystems/session-projection.md) — the registry that drives units and serves snapshot and change-feed values.
92
+ - [Session projection registry package](../session-projection/README.md) — the registry contract units register against.
93
+ - [Session package map](../README.md) — adjacent persistence, projection, title, and telemetry packages.
94
+
95
+ -----
96
+
97
+ <a id="model-experience"></a>
98
+ ## Model Experience
99
+
100
+ None, as the turnOutline unit folds already-logged turn boundaries into a client-facing read model and registers nothing model-facing.
101
+
102
+ #### KV Cache effect
103
+
104
+ None; the package never assembles or sends provider requests.
105
+
106
+ ## Known Limitations and Deferred Work
107
+
108
+ <a id="known-limitations-and-deferred-work"></a>
109
+
110
+
111
+ These limits define what the outline describes and when the unit is absent. They are current package constraints.
112
+
113
+ - **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.
114
+ - **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.
115
+ - **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.
116
+ - **Mounted only where the projection registry is composed** — other assemblies serve no `turnOutline` key, and their consumers fall back to loaded-window navigation.
117
+
118
+ <a id="dev-note"></a>
119
+ ### Dev Note
120
+
121
+ <details>
122
+ <summary>Working context for maintainers — click to expand</summary>
123
+
124
+ None.
125
+
126
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,126 @@
1
+ ---
2
+ description: "面向组合或调试 turnOutline 投影单元的客户端与维护者的全量轮次大纲说明,支撑整会话轮次导航。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-session-turn-outline
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 本包为历史记录客户端提供涵盖完整会话的轮次大纲,其中包含每个已开始轮次的有界提示词预览与落定回复预览。客户端可以导航尚未加载的轮次,并从载入所选轮次所需的准确事件序号向后分页。它适用于提供会话投影的装配;在其他装配中,客户端继续使用仅覆盖已加载窗口的导航。预览排除注入的上下文与工具结果,并且回复仅在所属轮次落定后出现。
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: '@x1a0f3n9/dsh-session'
34
+ - name: '@x1a0f3n9/dsh-session-projection'
35
+ - name: '@x1a0f3n9/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
+ | — | 不发布运行时不变式伴生入口:本包仅拥有一个纯投影折叠,`session-projection` 会对其对外值执行 schema 校验;重新折叠同一日志只会复制实现,无法比较独立维护的观测,而轮次边界顺序由 session 与 agent-loop 负责。 |
75
+
76
+ ### 折叠规则
77
+
78
+ - 不相关事件返回同一状态引用,纯草稿变化保持 `turns` 数组身份不变;注册表的两道 `Object.is` 门由此把变更流压到每轮至多三次推送。
79
+ - 未推进轮次号的 `turn/start` 被跳过,保持大纲有序;重试边界的预览随后落在既有条目上。
80
+ - wire 视图投影 `state.turns`;持久缓存的状态 schema 在 wire schema 外再包一个草稿字段。
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## 进一步探索
88
+
89
+ 当单元约定不够用时阅读以下页面。它们从驱动单元的注册表逐步进入相邻的会话包。
90
+
91
+ - [会话投影子系统](../../../docs/subsystems/session-projection.zh.md)——驱动单元并提供快照与变更流值的注册表。
92
+ - [会话投影注册表包](../session-projection/README.zh.md)——单元注册所依据的注册表约定。
93
+ - [会话包映射](../README.zh.md)——相邻的持久化、投影、标题与遥测包。
94
+
95
+ -----
96
+
97
+ <a id="model-experience"></a>
98
+ ## 模型体验
99
+
100
+ 无,因为 turnOutline 单元把已写入日志的轮次边界折叠成面向客户端的读模型,不注册任何面向模型的内容。
101
+
102
+ #### KV Cache 影响
103
+
104
+ 无;本包从不组装或发送提供方请求。
105
+
106
+ ## 已知限制与延期工作
107
+
108
+ <a id="known-limitations-and-deferred-work"></a>
109
+
110
+
111
+ 这些限制说明大纲描述什么、单元何时缺失。它们是当前包约束。
112
+
113
+ - **wire 值随会话增长**——每次推送携带完整大纲(整值规则),CJK 字符占满预算时每轮上限约 600 字节、通常远小于此;把预览拆成按需读取推迟到数千轮量级的会话真正需要时。
114
+ - **回复只预览已落定的轮次**——它在 `turn/end` 提交,进行中的轮次(或从未记下结束边界的轮次)在边界落地前只有提示词预览。
115
+ - **没有合格文本的轮次保持 `''`**——纯图片、纯命令的轮次可导航但按轮次号标注,步骤全程不产文本的轮次没有回复预览。
116
+ - **仅在组合了投影注册表时挂载**——其他装配不提供 `turnOutline` 键,其消费方回退到仅按已加载窗口导航。
117
+
118
+ <a id="dev-note"></a>
119
+ ### 开发备注
120
+
121
+ <details>
122
+ <summary>维护者的工作上下文——点击展开</summary>
123
+
124
+ 无。
125
+
126
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { SessionSeq } from "@x1a0f3n9/dsh-session";
3
+ //#region lib/types/projection.js
4
+ /**
5
+ * The `turnOutline` projection unit: a pure fold of `turn/start` boundaries,
6
+ * first human prompts, and final assistant responses into the whole-log turn
7
+ * outline the chat rail renders for turns outside a client's paged event
8
+ * window.
9
+ *
10
+ * `turn/start` — not the prompt `user/message` — anchors each entry because
11
+ * its seq is the load-through target for a jump: the loop logs `turn/start`
12
+ * before the turn's prompt and steps, so a window paged back through that seq
13
+ * contains the whole turn. Previews mirror the rail's loaded-turn previews
14
+ * (space-joined text blocks, collapsed whitespace, an ellipsis when clipped)
15
+ * with budgets sized to the rail card's clamps — one prompt line, up to three
16
+ * response lines — so a turn shows the same words before and after its events
17
+ * load. The response commits at `turn/end` from a draft of the newest
18
+ * text-bearing assistant message; draft-only applies keep the `turns` array's
19
+ * identity, so the identity-gated change feed pushes at most three times per
20
+ * turn (boundary, prompt, response).
21
+ *
22
+ * @module @x1a0f3n9/dsh-session-turn-outline/projection
23
+ */
24
+ /** Prompt budget: one rail-card line (13px over ~276px), ASCII worst case included. */
25
+ const PROMPT_PREVIEW_LIMIT = 50;
26
+ /** Response budget: three rail-card lines (12px over ~276px). */
27
+ const RESPONSE_PREVIEW_LIMIT = 120;
28
+ /** Space-join text blocks, collapse whitespace, and cap at `limit` with a trailing ellipsis when clipped. */
29
+ function preview(content, limit) {
30
+ let text = "";
31
+ let unread = false;
32
+ for (const block of content) {
33
+ if (block.type !== "text") continue;
34
+ if (text.length >= limit * 2) {
35
+ unread = true;
36
+ break;
37
+ }
38
+ const clipped = block.text.length > limit * 2;
39
+ const chunk = clipped ? block.text.slice(0, limit * 2) : block.text;
40
+ text += text === "" ? chunk : ` ${chunk}`;
41
+ if (clipped) {
42
+ unread = true;
43
+ break;
44
+ }
45
+ }
46
+ const normalized = text.replace(/\s+/g, " ").trim();
47
+ if (normalized.length > limit - 1) return `${normalized.slice(0, limit - 1).trimEnd()}…`;
48
+ return unread ? `${normalized}…` : normalized;
49
+ }
50
+ const turnOutlineEntriesSchema = z.array(z.object({
51
+ turn: z.number().int().nonnegative(),
52
+ seq: z.number().int().nonnegative().transform(SessionSeq),
53
+ prompt: z.string().max(PROMPT_PREVIEW_LIMIT),
54
+ response: z.string().max(RESPONSE_PREVIEW_LIMIT)
55
+ }).strict()).superRefine((turns, context) => {
56
+ let previous = -1;
57
+ for (const entry of turns) {
58
+ if (entry.turn <= previous) {
59
+ context.addIssue({
60
+ code: "custom",
61
+ message: "turn outline entries must be strictly increasing by turn"
62
+ });
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 = {
73
+ turns: [],
74
+ draft: ""
75
+ };
76
+ /** The `turnOutline` unit registered on `ctx.sessionProjections` (exported for the unit spec). */
77
+ const turnOutlineProjectionDefinition = {
78
+ key: "turnOutline",
79
+ stateVersion: 2,
80
+ stateSchema: turnOutlineStateSchema,
81
+ init: () => EMPTY_OUTLINE,
82
+ apply: (state, event) => {
83
+ switch (event.type) {
84
+ case "turn/start": {
85
+ const last = state.turns.at(-1);
86
+ if (last !== void 0 && event.data.turn <= last.turn) return state;
87
+ return {
88
+ turns: [...state.turns, {
89
+ turn: event.data.turn,
90
+ seq: event.seq,
91
+ prompt: "",
92
+ response: ""
93
+ }],
94
+ draft: ""
95
+ };
96
+ }
97
+ case "user/message": {
98
+ if (event.data.source.kind !== "user") return state;
99
+ const last = state.turns.at(-1);
100
+ if (last === void 0 || last.prompt !== "") return state;
101
+ const prompt = preview(event.data.content, PROMPT_PREVIEW_LIMIT);
102
+ if (prompt === "") return state;
103
+ return {
104
+ turns: [...state.turns.slice(0, -1), {
105
+ ...last,
106
+ prompt
107
+ }],
108
+ draft: state.draft
109
+ };
110
+ }
111
+ case "assistant/message": {
112
+ const draft = preview(event.data.message.content, RESPONSE_PREVIEW_LIMIT);
113
+ if (draft === "" || draft === state.draft) return state;
114
+ return {
115
+ turns: state.turns,
116
+ draft
117
+ };
118
+ }
119
+ case "turn/end": {
120
+ if (state.draft === "") return state;
121
+ const last = state.turns.at(-1);
122
+ if (last === void 0 || last.response === state.draft) return {
123
+ turns: state.turns,
124
+ draft: ""
125
+ };
126
+ return {
127
+ turns: [...state.turns.slice(0, -1), {
128
+ ...last,
129
+ response: state.draft
130
+ }],
131
+ draft: ""
132
+ };
133
+ }
134
+ default: return state;
135
+ }
136
+ },
137
+ wire: {
138
+ viewSchema: turnOutlineEntriesSchema,
139
+ view: (state) => state.turns
140
+ }
141
+ };
142
+ //#endregion
143
+ //#region lib/types/index.js
144
+ /**
145
+ * Function plugin registering the `turnOutline` projection unit: the
146
+ * whole-log turn outline (turn number, `turn/start` seq, bounded prompt
147
+ * preview) served through the session-projection seam — registry snapshot,
148
+ * change feed, and every projection carrier — so a client can offer every
149
+ * turn of a session and target history paging at exact seqs without holding
150
+ * the events. The plugin owns only the fold; delivery is the seam's.
151
+ *
152
+ * @module @x1a0f3n9/dsh-session-turn-outline
153
+ */
154
+ /** Cordis plugin name. */
155
+ const name = "session-turn-outline";
156
+ /** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */
157
+ const inject = ["sessionProjections"];
158
+ /**
159
+ * Register the `turnOutline` unit; the registration is an effect on this
160
+ * plugin's fiber, so unloading removes the key.
161
+ * @param ctx - registrant context carrying the projection registry.
162
+ */
163
+ function apply(ctx) {
164
+ ctx.sessionProjections.register(turnOutlineProjectionDefinition);
165
+ }
166
+ //#endregion
167
+ 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 @x1a0f3n9/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 @x1a0f3n9/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 @x1a0f3n9/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 @x1a0f3n9/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,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 @x1a0f3n9/dsh-session-turn-outline/projection
20
+ */
21
+ import { z } from 'zod';
22
+ import { type SessionEvent } from '@x1a0f3n9/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,135 @@
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 @x1a0f3n9/dsh-session-turn-outline/projection
20
+ */
21
+ import { z } from 'zod';
22
+ import { SessionSeq } from '@x1a0f3n9/dsh-session';
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')
33
+ continue;
34
+ if (text.length >= limit * 2) {
35
+ unread = true;
36
+ break;
37
+ }
38
+ // Per-block bound: the fold runs on every message event, so a single
39
+ // multi-megabyte block must not be concatenated (and regex-normalized)
40
+ // whole for a preview this short.
41
+ const clipped = block.text.length > limit * 2;
42
+ const chunk = clipped ? block.text.slice(0, limit * 2) : block.text;
43
+ text += text === '' ? chunk : ` ${chunk}`;
44
+ if (clipped) {
45
+ unread = true;
46
+ break;
47
+ }
48
+ }
49
+ const normalized = text.replace(/\s+/g, ' ').trim();
50
+ if (normalized.length > limit - 1)
51
+ return `${normalized.slice(0, limit - 1).trimEnd()}…`;
52
+ return unread ? `${normalized}…` : normalized;
53
+ }
54
+ const turnOutlineEntriesSchema = z.array(z.object({
55
+ turn: z.number().int().nonnegative(),
56
+ seq: z.number().int().nonnegative().transform(SessionSeq),
57
+ prompt: z.string().max(PROMPT_PREVIEW_LIMIT),
58
+ response: z.string().max(RESPONSE_PREVIEW_LIMIT),
59
+ }).strict()).superRefine((turns, context) => {
60
+ let previous = -1;
61
+ for (const entry of turns) {
62
+ if (entry.turn <= previous) {
63
+ context.addIssue({ code: 'custom', message: 'turn outline entries must be strictly increasing by turn' });
64
+ return;
65
+ }
66
+ previous = entry.turn;
67
+ }
68
+ });
69
+ const turnOutlineStateSchema = z.object({
70
+ turns: turnOutlineEntriesSchema,
71
+ draft: z.string().max(RESPONSE_PREVIEW_LIMIT),
72
+ }).strict();
73
+ const EMPTY_OUTLINE = { turns: [], draft: '' };
74
+ /** The `turnOutline` unit registered on `ctx.sessionProjections` (exported for the unit spec). */
75
+ export const turnOutlineProjectionDefinition = {
76
+ key: 'turnOutline',
77
+ stateVersion: 2,
78
+ stateSchema: turnOutlineStateSchema,
79
+ init: () => EMPTY_OUTLINE,
80
+ apply: (state, event) => {
81
+ // Every uninteresting event returns the same reference (Object.is gates
82
+ // the drive), and draft-only changes keep `turns` identity (the raw-view
83
+ // identity gate then keeps the change feed quiet).
84
+ switch (event.type) {
85
+ case 'turn/start': {
86
+ const last = state.turns.at(-1);
87
+ // Order guard: a boundary that does not advance the turn number keeps
88
+ // the outline sorted, and a retried turn's previews land on the
89
+ // standing entry.
90
+ if (last !== undefined && event.data.turn <= last.turn)
91
+ return state;
92
+ return {
93
+ turns: [...state.turns, { turn: event.data.turn, seq: event.seq, prompt: '', response: '' }],
94
+ draft: '',
95
+ };
96
+ }
97
+ case 'user/message': {
98
+ // Only the newest turn can still be waiting for its opening human
99
+ // prompt; later human messages in the same turn (steering) keep the
100
+ // first preview.
101
+ if (event.data.source.kind !== 'user')
102
+ return state;
103
+ const last = state.turns.at(-1);
104
+ if (last === undefined || last.prompt !== '')
105
+ return state;
106
+ const prompt = preview(event.data.content, PROMPT_PREVIEW_LIMIT);
107
+ if (prompt === '')
108
+ return state;
109
+ return { turns: [...state.turns.slice(0, -1), { ...last, prompt }], draft: state.draft };
110
+ }
111
+ case 'assistant/message': {
112
+ // Newest text-bearing message wins; the buffer commits at turn/end.
113
+ const draft = preview(event.data.message.content, RESPONSE_PREVIEW_LIMIT);
114
+ if (draft === '' || draft === state.draft)
115
+ return state;
116
+ return { turns: state.turns, draft };
117
+ }
118
+ case 'turn/end': {
119
+ if (state.draft === '')
120
+ return state;
121
+ const last = state.turns.at(-1);
122
+ if (last === undefined || last.response === state.draft)
123
+ return { turns: state.turns, draft: '' };
124
+ return { turns: [...state.turns.slice(0, -1), { ...last, response: state.draft }], draft: '' };
125
+ }
126
+ default:
127
+ return state;
128
+ }
129
+ },
130
+ wire: {
131
+ viewSchema: turnOutlineEntriesSchema,
132
+ view: state => state.turns,
133
+ },
134
+ };
135
+ //# sourceMappingURL=projection.js.map
@@ -0,0 +1,45 @@
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 @x1a0f3n9/dsh-session-turn-outline/types
8
+ */
9
+ import type { SessionSeq } from '@x1a0f3n9/dsh-session/types';
10
+ export {};
11
+ /** One started turn's outline facts, independent of what a client has paged in. */
12
+ export interface TurnOutlineEntry {
13
+ /** Host-assigned turn number (the `turn/start` payload). */
14
+ readonly turn: number;
15
+ /** The turn's `turn/start` event seq — paging a window back through this seq loads the whole turn. */
16
+ readonly seq: SessionSeq;
17
+ /** Bounded first-human-prompt preview (one rail-card line); `''` until an eligible prompt lands. */
18
+ readonly prompt: string;
19
+ /** Bounded final-response preview (up to three rail-card lines); `''` until the turn ends with assistant text. */
20
+ readonly response: string;
21
+ }
22
+ /**
23
+ * Fold state: the served entries plus the open turn's response draft. The
24
+ * draft buffers the newest text-bearing assistant message until `turn/end`
25
+ * commits it, and the wire view projects only `turns` — draft-only applies
26
+ * keep that array's identity, so the change feed stays quiet between turn
27
+ * boundaries.
28
+ */
29
+ export interface TurnOutlineState {
30
+ /** Started turns in ascending turn order. */
31
+ readonly turns: readonly TurnOutlineEntry[];
32
+ /** Newest text-bearing assistant preview of the open turn; `''` outside one. */
33
+ readonly draft: string;
34
+ }
35
+ declare module '@x1a0f3n9/dsh-session-projection/types' {
36
+ interface SessionProjectionStateMap {
37
+ /** Whole-log turn outline fold state (entries plus the open turn's response draft). */
38
+ turnOutline: TurnOutlineState;
39
+ }
40
+ interface SessionProjectionMap {
41
+ /** Every started turn with its `turn/start` seq and bounded previews, strictly increasing by turn; see {@link TurnOutlineEntry}. */
42
+ turnOutline: readonly TurnOutlineEntry[];
43
+ }
44
+ }
45
+ //# 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 @x1a0f3n9/dsh-session-turn-outline/types
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-session-turn-outline",
3
+ "description": "Whole-log turn outline projection (turnOutline) for the DeepSeek Harness",
4
+ "version": "0.1.5-rc.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
+ "./types": {
22
+ "types": "./lib/types/types.d.ts",
23
+ "default": "./lib/types/types.js"
24
+ },
25
+ "./client": {
26
+ "types": "./lib/types/client.d.ts",
27
+ "default": "./lib/types/client.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "lib/index.js",
34
+ "lib/types/**/*.js",
35
+ "lib/types/**/*.d.ts"
36
+ ],
37
+ "license": "MIT",
38
+ "peerDependencies": {
39
+ "@deepseek-ai/cordis": "^4.0.2",
40
+ "@x1a0f3n9/dsh-llm": "^0.1.5-rc.3",
41
+ "@x1a0f3n9/dsh-session": "^0.1.5-rc.3",
42
+ "@x1a0f3n9/dsh-session-projection": "^0.1.5-rc.3"
43
+ },
44
+ "dependencies": {
45
+ "zod": "^4.4.3"
46
+ },
47
+ "devDependencies": {
48
+ "@deepseek-ai/cordis": "^4.0.2",
49
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
50
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
51
+ "@x1a0f3n9/dsh-llm": "^0.1.5-rc.3",
52
+ "@x1a0f3n9/dsh-session": "^0.1.5-rc.3",
53
+ "@x1a0f3n9/dsh-session-projection": "^0.1.5-rc.3"
54
+ }
55
+ }