@lingxi-ai-cn/dsh-session-export 0.1.6-rc.8 → 0.1.7-alpha.2

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/README.md CHANGED
@@ -1,17 +1,31 @@
1
+ ---
2
+ description: "Native atomic Session archive and Markdown export over official durable persistence owners."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # `@lingxi-ai-cn/dsh-session-export`
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Host-owned Session-log ZIP producer, human-readable Markdown projection, and native path writer. The `SessionLogExporter` service (`ctx.sessionLogExporter`) flushes each live Session before reading its persistence backend's raw artifact, streams the root artifact, optional descendants, and referenced images through bounded fflate compression, and never reconstructs the raw archive from parsed events. `stream()` exposes the archive to a host transport; `writeToDirectory()` writes the same archive to an existing absolute directory and returns its exact final path. `writeMarkdownToDirectory()` separately reads the validated logical events and publishes a summary-only Markdown projection.
10
+ ## Summary
11
+
12
+ Native Session archive writer and human-readable projection over the official [`@deepseek-ai/dsh-session-log-export`](../../session-query/session-log-export/README.md) producer. The `SessionLogExporter` service (`ctx.sessionLogExporter`) reuses that package's root preflight, raw-artifact ZIP stream, descendant ordering, attachment collection, compression, backpressure, and cancellation. `stream()` exposes the official archive stream to a native consumer; `writeToDirectory()` writes it into an existing absolute host directory and returns the exact final path. `writeMarkdownToDirectory()` separately reads validated logical events and publishes a summary-only Markdown projection.
6
13
 
7
- The native writer creates an owner-only random sibling, forwards cancellation through root preparation, lineage, persistence, attachment reads, compression, and file writes, syncs and closes the complete file, then publishes it with an exclusive hard link. Existing exports are never overwritten: the first collision uses `-2`, followed by increasing suffixes. A failure before publication removes the partial sibling and leaves no final archive. The destination filesystem must support same-directory hard links.
14
+ The adapter adds only native publication: it creates an owner-only random sibling, forwards cancellation through the official producer and file writes, syncs and closes the complete file, then publishes it with an exclusive hard link. Existing exports are never overwritten: the first collision uses `-2`, followed by increasing suffixes. A failure before publication removes the partial sibling and leaves no final archive. The destination filesystem must support same-directory hard links.
8
15
 
9
- Markdown export is intentionally a human-readable projection, not a replacement for the raw archive. It includes the current surface of the root and optional descendants, bounded user/assistant text, tool names and bounded result summaries, and explicit sequence/time facts. Tool argument values are omitted by default and marked as omitted; the exporter does not offer a silent sensitive-detail mode. Image attachments use `attachment:<id>` reference-only links and are never copied into the Markdown file. The same private sibling, cancellation, `0600` mode, collision suffix, and exclusive hard-link publication rules apply to `.md` output.
16
+ Markdown export is intentionally a human-readable projection, not a replacement for the official raw archive. It includes the current surface of the root and optional descendants, bounded user/assistant text, tool names and bounded result summaries, and explicit sequence/time facts. Tool argument values are omitted and marked as omitted. Image attachments use `attachment:<id>` references and their bytes are never copied into the Markdown file. The same private sibling, cancellation, `0600` mode, collision suffix, and exclusive hard-link publication rules apply to `.md` output.
10
17
 
11
18
  Expected preparation failures use `SessionLogExportError`: `services-unavailable`, `raw-artifacts-unsupported`, `session-not-found`, and `prepare-failed`. Destination and output failures use `destination-invalid` and `write-failed`. The operator-facing messages do not expose backend preparation errors; the original error remains attached as `cause` for host diagnostics. Signal cancellation preserves the signal's reason instead of becoming an export failure.
12
19
 
13
20
  The archive is diagnostic material containing the stored Session artifacts verbatim. Provider credential stores and transient OAuth progress are outside Session persistence and are never included, but prompts and tool arguments already present in the durable log remain present; consumers must choose an operator-controlled destination and treat the result as sensitive.
14
21
 
22
+ ## Table of Contents
23
+
24
+ - [Configuration](#configuration)
25
+ - [Model Experience](#model-experience)
26
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
27
+ - [Dev Note](#dev-note)
28
+
15
29
  ## Configuration
16
30
 
17
31
  | Key | Default | Behavior |
@@ -29,6 +43,11 @@ None. Export does not assemble or send provider requests.
29
43
  ## Known Limitations and Deferred Work
30
44
 
31
45
  - Persistence backends that do not expose per-Session raw artifacts cannot export; the shipped JSONL backend supports plaintext and zstd artifacts, while SQLite does not.
32
- - Markdown projection can use a backend's validated logical `inspect()` API even when raw-artifact ZIP export is unavailable; its attachment policy remains reference-only.
46
+ - Markdown projection can use a backend's validated logical `inspect()` API even when official raw-artifact ZIP export is unavailable; its attachment policy remains reference-only.
33
47
  - A tree export is a sequence of per-Session durability barriers and reads, not one atomic snapshot across the whole lineage; a live descendant may append after its artifact was read.
34
48
  - Native publication requires hard-link support in the selected directory. The service fails without publishing a partial final file when the filesystem rejects that operation.
49
+
50
+ <a id="dev-note"></a>
51
+ ### Dev Note
52
+
53
+ None.
package/README.zh.md CHANGED
@@ -1,23 +1,39 @@
1
+ ---
2
+ description: "基于官方 durable persistence owner 的原生原子 Session archive 与 Markdown 导出。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # `@lingxi-ai-cn/dsh-session-export`
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 宿主侧拥有的 Session 日志 ZIP 生产器、人类可读 Markdown projection 与原生路径写入器。`SessionLogExporter` 服务(`ctx.sessionLogExporter`)会在读取持久化后端的原始工件前 flush 每个实时 Session,通过有界 fflate 压缩流式输出根工件、可选后代和引用的图片,并且绝不从解析事件重建原始归档。`stream()` 向宿主传输层提供归档;`writeToDirectory()` 把同一归档写入一个既有的绝对目录,并返回精确的最终路径。`writeMarkdownToDirectory()` 则独立读取已验证的逻辑事件并发布仅供摘要阅读的 Markdown projection。
10
+ ## 概述
11
+
12
+ 基于官方 [`@deepseek-ai/dsh-session-log-export`](../../session-query/session-log-export/README.zh.md) 生产器的原生 Session 归档写入器与人类可读 projection。`SessionLogExporter` 服务(`ctx.sessionLogExporter`)复用该包的根工件预检、原始工件 ZIP 流、后代顺序、附件收集、压缩、背压与取消。`stream()` 向原生消费方暴露官方归档流;`writeToDirectory()` 把它写入既有的绝对宿主目录,并返回精确的最终路径。`writeMarkdownToDirectory()` 则独立读取已验证的逻辑事件并发布仅供摘要阅读的 Markdown projection。
6
13
 
7
- 原生写入器先创建一个仅所有者可读写的随机同级文件,把取消转发到根工件准备、血缘、持久化、附件读取、压缩和文件写入,随后同步并关闭完整文件,最后通过排他硬链接发布。既有导出永远不会被覆盖:第一次冲突使用 `-2`,随后递增后缀。发布前的失败会删除局部同级文件,不留下最终归档。目标文件系统必须支持同目录硬链接。
14
+ 该适配器只增加原生发布:先创建一个仅所有者可读写的随机同级文件,把取消转发到官方生产器和文件写入,随后同步并关闭完整文件,最后通过排他硬链接发布。既有导出永远不会被覆盖:第一次冲突使用 `-2`,随后递增后缀。发布前的失败会删除局部同级文件,不留下最终归档。目标文件系统必须支持同目录硬链接。
8
15
 
9
- Markdown 导出明确是人类可读 projection,不替代原始归档。它包含根 Session 及可选后代的当前表层、有界的人类/助手文本、工具名称、有界结果摘要,以及明确的 seq/time 事实。工具参数值默认省略并明确标记;导出器不会提供静默的敏感详情模式。图片附件只使用 `attachment:<id>` 的仅引用链接,绝不复制到 Markdown 文件。同一私有同级临时文件、取消、`0600` 权限、冲突后缀和排他硬链接发布规则也适用于 `.md` 输出。
16
+ Markdown 导出明确是人类可读 projection,不替代官方原始归档。它包含根 Session 及可选后代的当前表层、有界的用户/助手文本、工具名称、有界结果摘要,以及明确的 seq/time 事实。工具参数值会被省略并明确标记。图片附件只使用 `attachment:<id>` 引用,其字节绝不复制到 Markdown 文件。同一私有同级临时文件、取消、`0600` 权限、冲突后缀和排他硬链接发布规则也适用于 `.md` 输出。
10
17
 
11
18
  预期的准备失败使用 `SessionLogExportError`:`services-unavailable`、`raw-artifacts-unsupported`、`session-not-found` 和 `prepare-failed`。目标与输出失败使用 `destination-invalid` 和 `write-failed`。面向操作者的消息不会泄露后端准备错误;原始错误保留为 `cause`,供宿主诊断。signal 取消会保留 signal 的 reason,而不会改写成导出失败。
12
19
 
13
20
  该归档是逐字包含所存 Session 工件的诊断材料。提供方凭据存储和 transient OAuth 进度位于 Session 持久化之外,绝不会包含在内;但已经进入持久日志的 prompt 与工具参数仍会保留,因此 Consumer 必须选择由操作者控制的目标位置,并把结果视为敏感材料。
14
21
 
22
+ ## 目录
23
+
24
+ - [配置](#configuration)
25
+ - [模型体验](#model-experience)
26
+ - [已知限制与暂缓事项](#known-limitations-and-deferred-work)
27
+ - [开发备注](#dev-note)
28
+
29
+ <a id="configuration"></a>
15
30
  ## 配置
16
31
 
17
32
  | 键 | 默认值 | 行为 |
18
33
  |---|---:|---|
19
34
  | `compressionLevel` | `6` | 从 `0`(仅存储)到 `9`(最小归档)的整数 fflate DEFLATE 级别。 |
20
35
 
36
+ <a id="model-experience"></a>
21
37
  ## 模型体验
22
38
 
23
39
  无,因为导出只读取持久工件并写入宿主文件,不增加 Session 事件或模型可见内容。
@@ -26,9 +42,15 @@ Markdown 导出明确是人类可读 projection,不替代原始归档。它包
26
42
 
27
43
  无。导出不会组装或发送提供方请求。
28
44
 
45
+ <a id="known-limitations-and-deferred-work"></a>
29
46
  ## 已知限制与暂缓事项
30
47
 
31
48
  - 不提供逐 Session 原始工件的持久化后端无法导出;随附的 JSONL 后端支持明文与 zstd 工件,SQLite 尚不支持。
32
- - 即使原始工件 ZIP 不可用,Markdown projection 仍可使用后端已验证的逻辑 `inspect()` API;附件策略仍固定为仅引用。
49
+ - 即使官方原始工件 ZIP 不可用,Markdown projection 仍可使用后端已验证的逻辑 `inspect()` API;附件策略仍固定为仅引用。
33
50
  - 树导出是一系列逐 Session 的持久性屏障与读取,并非覆盖整条血缘的一次原子快照;实时后代可能在自身工件被读取后继续追加。
34
51
  - 原生发布要求所选目录支持硬链接。文件系统拒绝该操作时,服务会失败且不会发布局部最终文件。
52
+
53
+ <a id="dev-note"></a>
54
+ ### 开发备注
55
+
56
+ 无。
package/lib/index.js CHANGED
@@ -3,365 +3,9 @@ import { link, open, rm, stat } from "node:fs/promises";
3
3
  import { basename, extname, isAbsolute, join } from "node:path";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { Zip, ZipDeflate } from "fflate";
6
+ import { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, DEFAULT_SESSION_LOG_COMPRESSION_LEVEL as DEFAULT_SESSION_LOG_COMPRESSION_LEVEL$1, flushLiveSessionLog, flushLiveSessionLog as flushLiveSessionLog$1, sessionLogExportDeps, sessionLogExportDeps as sessionLogExportDeps$1, sessionLogZipEntries, sessionLogZipFilename, sessionLogZipFilename as sessionLogZipFilename$1, streamSessionLogZip, streamSessionLogZip as streamSessionLogZip$1 } from "@deepseek-ai/dsh-session-log-export";
7
7
  import { foldSurface } from "@deepseek-ai/dsh-session";
8
8
  import { extractSessionEventText } from "@deepseek-ai/dsh-session-query";
9
- //#region lib/types/zip.js
10
- /**
11
- * Host-side Session ZIP producer: streams one archive whose files are the
12
- * sessions' stored artifact text verbatim plus every referenced media object.
13
- * The root artifact sits under its original base name (`session.jsonl`); each
14
- * subagent descendant under `subagents/<id>/<filename>`; each image referenced
15
- * by any included log under `media/<attachmentId>.<ext>` (content-addressed,
16
- * so one archive never duplicates a shared image). No manifest is written —
17
- * every file is byte-identical to the backend's durable artifact or attachment
18
- * store and self-describing through its own header line or media type. Before
19
- * each live session's artifact read, the SessionStore flush barrier makes the
20
- * current in-memory log durable; cold sessions need no barrier. Request abort
21
- * and response-consumer cancellation share one producer signal and terminate
22
- * the active compressor.
23
- * Compression runs on the host with fflate's streaming Zip API, so the archive
24
- * bytes are produced incrementally and the host never holds the whole archive
25
- * in one buffer; production waits for consumer pull whenever the response queue
26
- * reaches its byte high-water mark, so a slow consumer bounds accumulation to
27
- * the fixed 64 KiB response queue plus one synchronous fflate push.
28
- * @module @lingxi-ai-cn/dsh-session-export/zip
29
- */
30
- /** Balanced default used when a deployment omits compression config. */
31
- const DEFAULT_SESSION_LOG_COMPRESSION_LEVEL = 6;
32
- /**
33
- * Resolve the persistence, session-query, and attachment services a log export needs.
34
- * @param ctx - the composed host context.
35
- * @returns the export services (absent when the deployment does not mount them).
36
- */
37
- function sessionLogExportDeps(ctx) {
38
- return {
39
- sessionQuery: ctx.get("sessionQuery"),
40
- sessionPersistence: ctx.get("sessionPersistence"),
41
- attachments: ctx.get("attachments"),
42
- sessions: ctx.get("sessions")
43
- };
44
- }
45
- /**
46
- * Flush one currently live session through the store's authoritative durability
47
- * barrier immediately before its raw artifact is read. A cold or absent id has
48
- * no in-memory work to flush.
49
- * @param deps - export services, including the optional live-session store.
50
- * @param id - the session whose artifact is about to be read.
51
- * @param signal - optional cancellation observed around the flush barrier.
52
- */
53
- async function flushLiveSessionLog(deps, id, signal) {
54
- signal?.throwIfAborted();
55
- const sessions = deps.sessions;
56
- if (sessions === void 0) return;
57
- const session = sessions.get(id);
58
- if (session === void 0) return;
59
- await sessions.flush(session);
60
- signal?.throwIfAborted();
61
- }
62
- /** Zip extension for each accepted raster media type. */
63
- const MEDIA_TYPE_EXTENSIONS = {
64
- "image/png": "png",
65
- "image/jpeg": "jpg",
66
- "image/webp": "webp",
67
- "image/gif": "gif"
68
- };
69
- /**
70
- * The zip path for one media object: content-addressed by the opaque
71
- * attachment id so shared images land once and the id in the log maps back to
72
- * the archive entry without a manifest.
73
- * @param ref - the durable reference from a session log.
74
- * @returns the archive path.
75
- */
76
- function mediaEntryPath(ref) {
77
- return `media/${String(ref.attachmentId)}.${MEDIA_TYPE_EXTENSIONS[ref.mediaType]}`;
78
- }
79
- /**
80
- * Collect every image reference inside one content array, descending into
81
- * nested tool results the way the live attachment route does.
82
- * @param content - an event content array (or nested tool-result content).
83
- * @param refs - the dedupe map being filled (keyed by attachment id).
84
- */
85
- function collectImageRefs(content, refs) {
86
- if (!Array.isArray(content)) return;
87
- const pending = [];
88
- for (const item of content) pending.push(item);
89
- while (pending.length > 0) {
90
- const value = pending.pop();
91
- if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
92
- const block = value;
93
- if (block.type === "image" && typeof block.attachment === "object" && block.attachment !== null) {
94
- const ref = block.attachment;
95
- refs.set(String(ref.attachmentId), ref);
96
- }
97
- if (Array.isArray(block.content)) for (const item of block.content) pending.push(item);
98
- }
99
- }
100
- /**
101
- * Collect every image reference one session event carries, across the same
102
- * carriers the live attachment route scans (direct content, message content,
103
- * inserted messages, and completed assistant chunk blocks).
104
- * @param event - one parsed JSONL event object.
105
- * @param refs - the dedupe map being filled (keyed by attachment id).
106
- */
107
- function collectEventImageRefs(event, refs) {
108
- const data = event.data;
109
- if (typeof data !== "object" || data === null) return;
110
- const carrier = data;
111
- collectImageRefs(carrier.content, refs);
112
- if (carrier.message !== void 0) collectImageRefs(carrier.message.content, refs);
113
- if (carrier.inserted !== void 0) for (const message of carrier.inserted) collectImageRefs(message.content, refs);
114
- if (carrier.chunk?.type === "block-end") collectImageRefs([carrier.chunk.block], refs);
115
- }
116
- /**
117
- * Collect the distinct media references one stored artifact text names.
118
- * Lines that fail to parse cannot reference media and are skipped (the
119
- * artifact text itself is exported verbatim regardless).
120
- * @param content - the stored artifact text.
121
- * @returns the dedupe map keyed by attachment id.
122
- */
123
- function imageRefsInArtifact(content) {
124
- const refs = /* @__PURE__ */ new Map();
125
- for (const line of content.split("\n")) {
126
- if (line === "") continue;
127
- let event;
128
- try {
129
- event = JSON.parse(line);
130
- } catch {
131
- continue;
132
- }
133
- collectEventImageRefs(event, refs);
134
- }
135
- return refs;
136
- }
137
- /**
138
- * One safe zip path segment from an untrusted session id. Session ids are
139
- * host-controlled, but the brand allows any non-empty string, so `../`, dot
140
- * segments, and separator characters are neutralized before they can shape
141
- * archive entries. Distinct ids may collapse onto one segment (id collision
142
- * is impossible for the host-minted UUIDs, so no uniqueness suffix is kept).
143
- * @param id - the raw session id.
144
- * @returns a filesystem-safe single path segment.
145
- */
146
- function safeSessionIdSegment(id) {
147
- return id.replace(/[^A-Za-z0-9_-]/g, "_");
148
- }
149
- /**
150
- * The export archive filename for one root session.
151
- * @param sessionId - the root session id (sanitized to one safe path segment).
152
- * @returns the attachment filename for the session's export archive.
153
- */
154
- function sessionLogZipFilename(sessionId) {
155
- return `dsh-session-${safeSessionIdSegment(sessionId)}.zip`;
156
- }
157
- /**
158
- * Yield the export entries in zip order: the preloaded root artifact first,
159
- * then every subagent descendant in lineage order (each flushed when live,
160
- * read from the persistence backend right before it is yielded, and dropped
161
- * after the consumer moves on), then every distinct media object referenced by any of
162
- * the included logs (read and verified from the attachment store, one archive
163
- * entry per attachment id). The host holds at most one descendant's artifact
164
- * text and one media object at a time beyond the root.
165
- * @param deps - the mounted export services (the caller answered 500 before this runs).
166
- * @param root - the already-read root artifact (read by the caller so the
167
- * missing-session path can answer cleanly before streaming starts).
168
- * @param sessionId - the root session id.
169
- * @param includeDescendants - whether to include every subagent descendant.
170
- * @param signal - optional cancellation forwarded to lineage, persistence, and attachment reads.
171
- * @returns the export entries in zip order.
172
- */
173
- async function* sessionLogZipEntries(deps, root, sessionId, includeDescendants, signal) {
174
- const media = /* @__PURE__ */ new Map();
175
- const rememberMedia = (content) => {
176
- for (const [id, ref] of imageRefsInArtifact(content)) media.set(id, ref);
177
- };
178
- rememberMedia(root.content);
179
- yield {
180
- path: root.filename,
181
- content: root.content
182
- };
183
- if (includeDescendants) {
184
- const seen = /* @__PURE__ */ new Set([sessionId]);
185
- const collect = async function* (nodes) {
186
- for (const node of nodes) {
187
- signal?.throwIfAborted();
188
- const id = node.session.header.id;
189
- if (seen.has(id)) continue;
190
- seen.add(id);
191
- await flushLiveSessionLog(deps, id, signal);
192
- const raw = await deps.sessionPersistence.readRaw(id, signal);
193
- signal?.throwIfAborted();
194
- if (raw === void 0) throw new Error(`subagent "${id}" has no stored log artifact`);
195
- rememberMedia(raw.content);
196
- yield {
197
- path: `subagents/${safeSessionIdSegment(id)}/${raw.filename}`,
198
- content: raw.content
199
- };
200
- yield* collect(node.descendants);
201
- }
202
- };
203
- const lineage = await deps.sessionQuery.traceSession(sessionId, signal);
204
- signal?.throwIfAborted();
205
- yield* collect(lineage.descendants);
206
- }
207
- for (const ref of media.values()) {
208
- signal?.throwIfAborted();
209
- const stored = await deps.attachments.readImage(ref, signal);
210
- signal?.throwIfAborted();
211
- yield {
212
- path: mediaEntryPath(ref),
213
- data: stored.data
214
- };
215
- }
216
- }
217
- /** How many code units of artifact text one zip push carries (bounded encode memory). */
218
- const PUSH_CHUNK_CODE_UNITS = 65536;
219
- /** How many bytes of media one zip push carries (bounded memory; images are already size-capped). */
220
- const PUSH_CHUNK_BYTES = 65536;
221
- /** Byte capacity retained by the response stream before ZIP production waits for pull. */
222
- const RESPONSE_HIGH_WATER_MARK_BYTES = 65536;
223
- /** One producer waiter released only when ReadableStream pull restores capacity. */
224
- var ResponseCapacityGate = class {
225
- releasePending;
226
- /**
227
- * Wait until the response queue has positive byte capacity or cancellation wins.
228
- * @param controller - response controller whose desired size owns capacity.
229
- * @param signal - combined request/consumer cancellation.
230
- */
231
- async wait(controller, signal) {
232
- signal.throwIfAborted();
233
- if (controller.desiredSize === null || controller.desiredSize > 0) return;
234
- await new Promise((resolve) => {
235
- const release = () => {
236
- this.releasePending = void 0;
237
- signal.removeEventListener("abort", release);
238
- resolve();
239
- };
240
- this.releasePending = release;
241
- signal.addEventListener("abort", release, { once: true });
242
- });
243
- signal.throwIfAborted();
244
- }
245
- /** Release the current producer waiter after a consumer pull. */
246
- pulled() {
247
- this.releasePending?.();
248
- }
249
- };
250
- /**
251
- * Push one media object's bytes into a deflate stream in bounded chunks,
252
- * waiting for consumer capacity between chunks like the artifact path does.
253
- * @param deflate - the zip entry's deflate stream.
254
- * @param data - the stored image bytes.
255
- * @param controller - response queue controller.
256
- * @param capacity - pull-driven response-capacity gate.
257
- * @param signal - cancellation; throws when aborted.
258
- */
259
- async function pushBinaryChunks(deflate, data, controller, capacity, signal) {
260
- let offset = 0;
261
- do {
262
- signal.throwIfAborted();
263
- const end = Math.min(offset + PUSH_CHUNK_BYTES, data.byteLength);
264
- const finalChunk = end >= data.byteLength;
265
- deflate.push(data.subarray(offset, end), finalChunk);
266
- offset = end;
267
- await capacity.wait(controller, signal);
268
- } while (offset < data.byteLength);
269
- }
270
- /**
271
- * Push one artifact's text into a deflate stream in bounded chunks, never
272
- * splitting a surrogate pair across a chunk boundary (a lone high surrogate
273
- * re-encodes as U+FFFD and would silently corrupt the exported artifact).
274
- * @param deflate - the zip entry's deflate stream.
275
- * @param content - the artifact text verbatim.
276
- * @param controller - response queue controller.
277
- * @param capacity - pull-driven response-capacity gate.
278
- * @param signal - cancellation; throws when aborted.
279
- */
280
- async function pushArtifactChunks(deflate, content, controller, capacity, signal) {
281
- const encoder = new TextEncoder();
282
- let offset = 0;
283
- let finalChunk;
284
- do {
285
- signal.throwIfAborted();
286
- let end = Math.min(offset + PUSH_CHUNK_CODE_UNITS, content.length);
287
- if (end < content.length && end - offset > 1) {
288
- const last = content.charCodeAt(end - 1);
289
- if (last >= 55296 && last <= 56319) end -= 1;
290
- }
291
- finalChunk = end >= content.length;
292
- deflate.push(encoder.encode(content.slice(offset, end)), finalChunk);
293
- offset = end;
294
- await capacity.wait(controller, signal);
295
- } while (!finalChunk);
296
- }
297
- /**
298
- * Stream one session-log ZIP as a WHATWG ReadableStream. The root artifact is
299
- * read and validated by the caller before this is called (missing root or
300
- * missing services answer cleanly before any byte is produced); each entry is
301
- * then encoded and deflated in bounded chunks as it is produced, so the
302
- * archive bytes arrive incrementally. A descendant that fails to read errors
303
- * the stream (fail-loud, never silent under-export).
304
- * @param deps - the mounted export services (the caller answered 500 before this runs).
305
- * @param root - the already-read root artifact (first zip entry).
306
- * @param sessionId - the root session id.
307
- * @param includeDescendants - whether to include every subagent descendant.
308
- * @param compressionLevel - validated fflate DEFLATE level for every ZIP entry.
309
- * @param signal - request cancellation combined with response-consumer cancellation.
310
- * @returns the zip byte stream.
311
- */
312
- function streamSessionLogZip(deps, root, sessionId, includeDescendants, compressionLevel, signal) {
313
- const consumerAbort = new AbortController();
314
- const producerSignal = AbortSignal.any([signal, consumerAbort.signal]);
315
- let zip;
316
- let zipTerminated = false;
317
- const capacity = new ResponseCapacityGate();
318
- const terminateZip = () => {
319
- if (zip === void 0 || zipTerminated) return;
320
- zipTerminated = true;
321
- zip.terminate();
322
- };
323
- return new ReadableStream({
324
- start(controller) {
325
- const archive = new Zip((error, data, final) => {
326
- /* v8 ignore next 3 -- fflate reports only internal zip failures, unreachable for valid inputs */
327
- if (error) {
328
- controller.error(error);
329
- return;
330
- }
331
- /* v8 ignore next -- fflate may emit empty chunks; not controllable from tests */
332
- if (data.byteLength > 0) controller.enqueue(data);
333
- if (final) controller.close();
334
- });
335
- zip = archive;
336
- (async () => {
337
- try {
338
- for await (const entry of sessionLogZipEntries(deps, root, sessionId, includeDescendants, producerSignal)) {
339
- const deflate = new ZipDeflate(entry.path, { level: compressionLevel });
340
- archive.add(deflate);
341
- if ("content" in entry) await pushArtifactChunks(deflate, entry.content, controller, capacity, producerSignal);
342
- else await pushBinaryChunks(deflate, entry.data, controller, capacity, producerSignal);
343
- }
344
- archive.end();
345
- } catch (error) {
346
- /* v8 ignore next -- typed backends reject with Error, and DOMException is one in Node */
347
- terminateZip();
348
- controller.error(error instanceof Error ? error : new Error(String(error)));
349
- }
350
- })();
351
- },
352
- pull() {
353
- capacity.pulled();
354
- },
355
- cancel(reason) {
356
- consumerAbort.abort(reason instanceof Error ? reason : /* @__PURE__ */ new Error("session log export stream cancelled"));
357
- terminateZip();
358
- }
359
- }, {
360
- highWaterMark: RESPONSE_HIGH_WATER_MARK_BYTES,
361
- size: (chunk) => chunk.byteLength
362
- });
363
- }
364
- //#endregion
365
9
  //#region lib/types/markdown.js
366
10
  /** Human-readable Markdown projection for durable Session history. */
367
11
  /**
@@ -565,9 +209,9 @@ function formatTime(value) {
565
209
  //#endregion
566
210
  //#region lib/types/index.js
567
211
  /**
568
- * Host session-log export service: prepares the canonical stored-artifact ZIP,
569
- * exposes it as a bounded byte stream, and publishes complete archives into a
570
- * host directory without exposing a partial final file.
212
+ * Native Session-log export service: reuses the official canonical ZIP stream
213
+ * and publishes complete archives into a host directory without exposing a
214
+ * partial final file.
571
215
  * @module @lingxi-ai-cn/dsh-session-export
572
216
  */
573
217
  /** Typed session-log export failure with a consumer-safe message. */
@@ -595,7 +239,7 @@ var SessionLogExportError = class extends Error {
595
239
  */
596
240
  async function prepareSessionLogExport(ctx, sessionId, signal) {
597
241
  signal.throwIfAborted();
598
- const deps = sessionLogExportDeps(ctx);
242
+ const deps = sessionLogExportDeps$1(ctx);
599
243
  if (deps.sessionQuery === void 0 || deps.sessionPersistence === void 0 || deps.attachments === void 0) throw new SessionLogExportError("services-unavailable", "session log export is unavailable: missing session-query, session-persistence, or attachments service");
600
244
  if (!deps.sessionPersistence.supportsRawArtifacts) throw new SessionLogExportError("raw-artifacts-unsupported", "session log export is unavailable: the persistence backend does not expose per-session raw artifacts");
601
245
  const ready = {
@@ -606,7 +250,7 @@ async function prepareSessionLogExport(ctx, sessionId, signal) {
606
250
  };
607
251
  let root;
608
252
  try {
609
- await flushLiveSessionLog(deps, sessionId, signal);
253
+ await flushLiveSessionLog$1(deps, sessionId, signal);
610
254
  root = await deps.sessionPersistence.readRaw(sessionId, signal);
611
255
  signal.throwIfAborted();
612
256
  } catch (error) {
@@ -665,11 +309,11 @@ var SessionLogExporter = class extends Service {
665
309
  "sessionQuery",
666
310
  "sessions"
667
311
  ];
668
- static Config = z.object({ compressionLevel: z.number().step(1).min(0).max(9).default(6) });
312
+ static Config = z.object({ compressionLevel: z.number().step(1).min(0).max(9).default(DEFAULT_SESSION_LOG_COMPRESSION_LEVEL$1) });
669
313
  compressionLevel;
670
314
  constructor(ctx, config) {
671
315
  super(ctx, "sessionLogExporter");
672
- this.compressionLevel = config.compressionLevel ?? 6;
316
+ this.compressionLevel = config.compressionLevel ?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL$1;
673
317
  }
674
318
  /**
675
319
  * Prepare and expose one bounded ZIP byte stream.
@@ -680,8 +324,8 @@ var SessionLogExporter = class extends Service {
680
324
  async stream(request, signal) {
681
325
  const prepared = await prepareSessionLogExport(this.ctx, request.sessionId, signal);
682
326
  return {
683
- filename: sessionLogZipFilename(request.sessionId),
684
- stream: streamSessionLogZip(prepared.ready, prepared.root, request.sessionId, request.includeDescendants, this.compressionLevel, signal)
327
+ filename: sessionLogZipFilename$1(request.sessionId),
328
+ stream: streamSessionLogZip$1(prepared.ready, prepared.root, request.sessionId, request.includeDescendants, this.compressionLevel, signal)
685
329
  };
686
330
  }
687
331
  /**
@@ -736,9 +380,9 @@ var SessionLogExporter = class extends Service {
736
380
  }
737
381
  /**
738
382
  * Render a summary-only human-readable Markdown projection and publish it
739
- * atomically under the first available safe filename. The raw ZIP remains
740
- * the diagnostic source of truth; tool arguments are omitted and attachment
741
- * references are never copied into the Markdown file.
383
+ * atomically under the first available safe filename. The official raw ZIP
384
+ * remains the diagnostic source of truth; tool arguments are omitted and
385
+ * attachment bytes are never copied into the Markdown file.
742
386
  * @param request - root identity, descendant policy, and explicit attachment policy.
743
387
  * @param directory - existing absolute host directory chosen by the operator.
744
388
  * @param signal - complete projection, writing, and publication lifetime.
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Host session-log export service: prepares the canonical stored-artifact ZIP,
3
- * exposes it as a bounded byte stream, and publishes complete archives into a
4
- * host directory without exposing a partial final file.
2
+ * Native Session-log export service: reuses the official canonical ZIP stream
3
+ * and publishes complete archives into a host directory without exposing a
4
+ * partial final file.
5
5
  * @module @lingxi-ai-cn/dsh-session-export
6
6
  */
7
7
  import { Context, Service } from '@deepseek-ai/cordis';
8
8
  import z from '@deepseek-ai/schemastery';
9
9
  import type { SessionId } from '@deepseek-ai/dsh-session';
10
10
  import type { SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence';
11
- import { type SessionLogCompressionLevel, type SessionLogExportReady } from './zip.ts';
11
+ import { type SessionLogCompressionLevel, type SessionLogExportReady } from '@deepseek-ai/dsh-session-log-export';
12
12
  import type { SessionMarkdownExportRequest } from './markdown.ts';
13
- export { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, streamSessionLogZip, } from './zip.ts';
14
- export type { SessionLogCompressionLevel, SessionLogExportDeps, SessionLogExportReady, SessionLogZipEntry, } from './zip.ts';
13
+ export { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, streamSessionLogZip, } from '@deepseek-ai/dsh-session-log-export';
14
+ export type { SessionLogCompressionLevel, SessionLogExportDeps, SessionLogExportReady, SessionLogZipEntry, } from '@deepseek-ai/dsh-session-log-export';
15
15
  export { renderSessionMarkdown, sessionMarkdownExportDeps, sessionMarkdownFilename, } from './markdown.ts';
16
16
  export type { SessionMarkdownAttachmentPolicy, SessionMarkdownExportDeps, SessionMarkdownExportRequest, SessionMarkdownSessionStore, } from './markdown.ts';
17
17
  /** Stable failure categories shared by the native writer and host transports. */
@@ -105,9 +105,9 @@ export default class SessionLogExporter extends Service {
105
105
  writeToDirectory(request: SessionLogExportRequest, directory: string, signal: AbortSignal): Promise<SessionLogExportFile>;
106
106
  /**
107
107
  * Render a summary-only human-readable Markdown projection and publish it
108
- * atomically under the first available safe filename. The raw ZIP remains
109
- * the diagnostic source of truth; tool arguments are omitted and attachment
110
- * references are never copied into the Markdown file.
108
+ * atomically under the first available safe filename. The official raw ZIP
109
+ * remains the diagnostic source of truth; tool arguments are omitted and
110
+ * attachment bytes are never copied into the Markdown file.
111
111
  * @param request - root identity, descendant policy, and explicit attachment policy.
112
112
  * @param directory - existing absolute host directory chosen by the operator.
113
113
  * @param signal - complete projection, writing, and publication lifetime.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lingxi-ai-cn/dsh-session-export",
3
- "description": "Host-owned streaming Session ZIP producer and atomic path writer",
4
- "version": "0.1.6-rc.8",
3
+ "description": "Native atomic path writer over the official Session ZIP producer",
4
+ "version": "0.1.7-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,17 +30,16 @@
30
30
  "lib/types/**/*.d.ts"
31
31
  ],
32
32
  "license": "MIT",
33
- "dependencies": {
34
- "fflate": "^0.8.2"
35
- },
33
+ "dependencies": {},
36
34
  "peerDependencies": {
37
- "@deepseek-ai/cordis": "4.0.1",
38
- "@deepseek-ai/dsh-attachment": "0.1.0-rc.8",
39
- "@deepseek-ai/dsh-invariants": "0.1.0-rc.8",
40
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
41
- "@deepseek-ai/dsh-session-persistence": "0.1.0-rc.8",
42
- "@deepseek-ai/dsh-session-query": "0.1.0-rc.8",
43
- "@deepseek-ai/schemastery": "3.18.1"
35
+ "@deepseek-ai/cordis": "4.0.2",
36
+ "@deepseek-ai/dsh-attachment": "0.1.2-alpha.2",
37
+ "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2",
38
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
39
+ "@deepseek-ai/dsh-session-log-export": "0.1.2-alpha.2",
40
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-alpha.2",
41
+ "@deepseek-ai/dsh-session-query": "0.1.2-alpha.2",
42
+ "@deepseek-ai/schemastery": "3.18.2"
44
43
  },
45
44
  "peerDependenciesMeta": {
46
45
  "@deepseek-ai/cordis": {
@@ -55,6 +54,9 @@
55
54
  "@deepseek-ai/dsh-session": {
56
55
  "optional": true
57
56
  },
57
+ "@deepseek-ai/dsh-session-log-export": {
58
+ "optional": true
59
+ },
58
60
  "@deepseek-ai/dsh-session-persistence": {
59
61
  "optional": true
60
62
  },
@@ -66,13 +68,15 @@
66
68
  }
67
69
  },
68
70
  "devDependencies": {
69
- "@deepseek-ai/cordis": "4.0.1",
70
- "@deepseek-ai/dsh-attachment": "0.1.0-rc.8",
71
- "@deepseek-ai/dsh-invariants": "0.1.0-rc.8",
72
- "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
73
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
74
- "@deepseek-ai/dsh-session-persistence": "0.1.0-rc.8",
75
- "@deepseek-ai/dsh-session-query": "0.1.0-rc.8",
76
- "@deepseek-ai/schemastery": "3.18.1"
71
+ "@deepseek-ai/cordis": "4.0.2",
72
+ "@deepseek-ai/dsh-attachment": "0.1.2-alpha.2",
73
+ "@deepseek-ai/dsh-invariants": "0.1.2-alpha.2",
74
+ "@deepseek-ai/dsh-llm": "0.1.2-alpha.2",
75
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
76
+ "@deepseek-ai/dsh-session-log-export": "0.1.2-alpha.2",
77
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-alpha.2",
78
+ "@deepseek-ai/dsh-session-query": "0.1.2-alpha.2",
79
+ "@deepseek-ai/schemastery": "3.18.2",
80
+ "fflate": "^0.8.2"
77
81
  }
78
82
  }
@@ -1,106 +0,0 @@
1
- /**
2
- * Host-side Session ZIP producer: streams one archive whose files are the
3
- * sessions' stored artifact text verbatim plus every referenced media object.
4
- * The root artifact sits under its original base name (`session.jsonl`); each
5
- * subagent descendant under `subagents/<id>/<filename>`; each image referenced
6
- * by any included log under `media/<attachmentId>.<ext>` (content-addressed,
7
- * so one archive never duplicates a shared image). No manifest is written —
8
- * every file is byte-identical to the backend's durable artifact or attachment
9
- * store and self-describing through its own header line or media type. Before
10
- * each live session's artifact read, the SessionStore flush barrier makes the
11
- * current in-memory log durable; cold sessions need no barrier. Request abort
12
- * and response-consumer cancellation share one producer signal and terminate
13
- * the active compressor.
14
- * Compression runs on the host with fflate's streaming Zip API, so the archive
15
- * bytes are produced incrementally and the host never holds the whole archive
16
- * in one buffer; production waits for consumer pull whenever the response queue
17
- * reaches its byte high-water mark, so a slow consumer bounds accumulation to
18
- * the fixed 64 KiB response queue plus one synchronous fflate push.
19
- * @module @lingxi-ai-cn/dsh-session-export/zip
20
- */
21
- import type { Context } from '@deepseek-ai/cordis';
22
- import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
23
- import type { SessionQueryEngine } from '@deepseek-ai/dsh-session-query';
24
- import type { SessionId, SessionStore } from '@deepseek-ai/dsh-session';
25
- import type { SessionPersistence, SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence';
26
- /** Valid fflate DEFLATE levels accepted by session-log export. */
27
- export type SessionLogCompressionLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
28
- /** Balanced default used when a deployment omits compression config. */
29
- export declare const DEFAULT_SESSION_LOG_COMPRESSION_LEVEL: SessionLogCompressionLevel;
30
- /** The services a session-log export needs (the live-session store is optional). */
31
- export interface SessionLogExportDeps {
32
- readonly sessionQuery: SessionQueryEngine | undefined;
33
- readonly sessionPersistence: SessionPersistence | undefined;
34
- readonly attachments: AttachmentStore | undefined;
35
- readonly sessions: SessionStore | undefined;
36
- }
37
- /** The export services narrowed to the mounted ones streaming actually reads. */
38
- export interface SessionLogExportReady {
39
- readonly sessionQuery: SessionQueryEngine;
40
- readonly sessionPersistence: SessionPersistence;
41
- readonly attachments: AttachmentStore;
42
- readonly sessions: SessionStore | undefined;
43
- }
44
- /**
45
- * Resolve the persistence, session-query, and attachment services a log export needs.
46
- * @param ctx - the composed host context.
47
- * @returns the export services (absent when the deployment does not mount them).
48
- */
49
- export declare function sessionLogExportDeps(ctx: Context): SessionLogExportDeps;
50
- /**
51
- * Flush one currently live session through the store's authoritative durability
52
- * barrier immediately before its raw artifact is read. A cold or absent id has
53
- * no in-memory work to flush.
54
- * @param deps - export services, including the optional live-session store.
55
- * @param id - the session whose artifact is about to be read.
56
- * @param signal - optional cancellation observed around the flush barrier.
57
- */
58
- export declare function flushLiveSessionLog(deps: Pick<SessionLogExportDeps, 'sessions'>, id: SessionId, signal?: AbortSignal): Promise<void>;
59
- /** One exported file: a stored artifact text or one referenced media object. */
60
- export type SessionLogZipEntry = {
61
- readonly path: string;
62
- readonly content: string;
63
- } | {
64
- readonly path: string;
65
- readonly data: Uint8Array;
66
- };
67
- /**
68
- * The export archive filename for one root session.
69
- * @param sessionId - the root session id (sanitized to one safe path segment).
70
- * @returns the attachment filename for the session's export archive.
71
- */
72
- export declare function sessionLogZipFilename(sessionId: string): string;
73
- /**
74
- * Yield the export entries in zip order: the preloaded root artifact first,
75
- * then every subagent descendant in lineage order (each flushed when live,
76
- * read from the persistence backend right before it is yielded, and dropped
77
- * after the consumer moves on), then every distinct media object referenced by any of
78
- * the included logs (read and verified from the attachment store, one archive
79
- * entry per attachment id). The host holds at most one descendant's artifact
80
- * text and one media object at a time beyond the root.
81
- * @param deps - the mounted export services (the caller answered 500 before this runs).
82
- * @param root - the already-read root artifact (read by the caller so the
83
- * missing-session path can answer cleanly before streaming starts).
84
- * @param sessionId - the root session id.
85
- * @param includeDescendants - whether to include every subagent descendant.
86
- * @param signal - optional cancellation forwarded to lineage, persistence, and attachment reads.
87
- * @returns the export entries in zip order.
88
- */
89
- export declare function sessionLogZipEntries(deps: SessionLogExportReady, root: SessionRawArtifact, sessionId: SessionId, includeDescendants: boolean, signal?: AbortSignal): AsyncGenerator<SessionLogZipEntry>;
90
- /**
91
- * Stream one session-log ZIP as a WHATWG ReadableStream. The root artifact is
92
- * read and validated by the caller before this is called (missing root or
93
- * missing services answer cleanly before any byte is produced); each entry is
94
- * then encoded and deflated in bounded chunks as it is produced, so the
95
- * archive bytes arrive incrementally. A descendant that fails to read errors
96
- * the stream (fail-loud, never silent under-export).
97
- * @param deps - the mounted export services (the caller answered 500 before this runs).
98
- * @param root - the already-read root artifact (first zip entry).
99
- * @param sessionId - the root session id.
100
- * @param includeDescendants - whether to include every subagent descendant.
101
- * @param compressionLevel - validated fflate DEFLATE level for every ZIP entry.
102
- * @param signal - request cancellation combined with response-consumer cancellation.
103
- * @returns the zip byte stream.
104
- */
105
- export declare function streamSessionLogZip(deps: SessionLogExportReady, root: SessionRawArtifact, sessionId: SessionId, includeDescendants: boolean, compressionLevel: SessionLogCompressionLevel, signal: AbortSignal): ReadableStream<Uint8Array>;
106
- //# sourceMappingURL=zip.d.ts.map