@lingxi-ai-cn/dsh-session-export 0.1.0-rc.8 → 0.1.0-rc.9
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.i18n.yaml +6 -0
- package/README.md +4 -1
- package/README.zh.md +4 -1
- package/lib/index.js +251 -1
- package/lib/types/index.d.ts +14 -0
- package/lib/types/markdown.d.ts +56 -0
- package/package.json +2 -1
package/README.i18n.yaml
ADDED
|
@@ -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/host/session-export/README.md
|
|
5
|
+
README.md: 31e7b0daddc83309a3b5207fc904b61e38a91894
|
|
6
|
+
README.zh.md: 9cc58b5f6055601e0a4b6c981e89100717f7b5f5
|
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
Host-owned Session-log ZIP producer 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
|
|
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.
|
|
6
6
|
|
|
7
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.
|
|
8
8
|
|
|
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.
|
|
10
|
+
|
|
9
11
|
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.
|
|
10
12
|
|
|
11
13
|
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.
|
|
@@ -27,5 +29,6 @@ None. Export does not assemble or send provider requests.
|
|
|
27
29
|
## Known Limitations and Deferred Work
|
|
28
30
|
|
|
29
31
|
- 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.
|
|
30
33
|
- 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.
|
|
31
34
|
- 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.
|
package/README.zh.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
宿主侧拥有的 Session 日志 ZIP
|
|
5
|
+
宿主侧拥有的 Session 日志 ZIP 生产器、人类可读 Markdown projection 与原生路径写入器。`SessionLogExporter` 服务(`ctx.sessionLogExporter`)会在读取持久化后端的原始工件前 flush 每个实时 Session,通过有界 fflate 压缩流式输出根工件、可选后代和引用的图片,并且绝不从解析事件重建原始归档。`stream()` 向宿主传输层提供归档;`writeToDirectory()` 把同一归档写入一个既有的绝对目录,并返回精确的最终路径。`writeMarkdownToDirectory()` 则独立读取已验证的逻辑事件并发布仅供摘要阅读的 Markdown projection。
|
|
6
6
|
|
|
7
7
|
原生写入器先创建一个仅所有者可读写的随机同级文件,把取消转发到根工件准备、血缘、持久化、附件读取、压缩和文件写入,随后同步并关闭完整文件,最后通过排他硬链接发布。既有导出永远不会被覆盖:第一次冲突使用 `-2`,随后递增后缀。发布前的失败会删除局部同级文件,不留下最终归档。目标文件系统必须支持同目录硬链接。
|
|
8
8
|
|
|
9
|
+
Markdown 导出明确是人类可读 projection,不替代原始归档。它包含根 Session 及可选后代的当前表层、有界的人类/助手文本、工具名称、有界结果摘要,以及明确的 seq/time 事实。工具参数值默认省略并明确标记;导出器不会提供静默的敏感详情模式。图片附件只使用 `attachment:<id>` 的仅引用链接,绝不复制到 Markdown 文件。同一私有同级临时文件、取消、`0600` 权限、冲突后缀和排他硬链接发布规则也适用于 `.md` 输出。
|
|
10
|
+
|
|
9
11
|
预期的准备失败使用 `SessionLogExportError`:`services-unavailable`、`raw-artifacts-unsupported`、`session-not-found` 和 `prepare-failed`。目标与输出失败使用 `destination-invalid` 和 `write-failed`。面向操作者的消息不会泄露后端准备错误;原始错误保留为 `cause`,供宿主诊断。signal 取消会保留 signal 的 reason,而不会改写成导出失败。
|
|
10
12
|
|
|
11
13
|
该归档是逐字包含所存 Session 工件的诊断材料。提供方凭据存储和 transient OAuth 进度位于 Session 持久化之外,绝不会包含在内;但已经进入持久日志的 prompt 与工具参数仍会保留,因此 Consumer 必须选择由操作者控制的目标位置,并把结果视为敏感材料。
|
|
@@ -27,5 +29,6 @@
|
|
|
27
29
|
## 已知限制与暂缓事项
|
|
28
30
|
|
|
29
31
|
- 不提供逐 Session 原始工件的持久化后端无法导出;随附的 JSONL 后端支持明文与 zstd 工件,SQLite 尚不支持。
|
|
32
|
+
- 即使原始工件 ZIP 不可用,Markdown projection 仍可使用后端已验证的逻辑 `inspect()` API;附件策略仍固定为仅引用。
|
|
30
33
|
- 树导出是一系列逐 Session 的持久性屏障与读取,并非覆盖整条血缘的一次原子快照;实时后代可能在自身工件被读取后继续追加。
|
|
31
34
|
- 原生发布要求所选目录支持硬链接。文件系统拒绝该操作时,服务会失败且不会发布局部最终文件。
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,8 @@ 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
6
|
import { Zip, ZipDeflate } from "fflate";
|
|
7
|
+
import { foldSurface } from "@deepseek-ai/dsh-session";
|
|
8
|
+
import { extractSessionEventText } from "@deepseek-ai/dsh-session-query";
|
|
7
9
|
//#region lib/types/zip.js
|
|
8
10
|
/**
|
|
9
11
|
* Host-side Session ZIP producer: streams one archive whose files are the
|
|
@@ -360,6 +362,207 @@ function streamSessionLogZip(deps, root, sessionId, includeDescendants, compress
|
|
|
360
362
|
});
|
|
361
363
|
}
|
|
362
364
|
//#endregion
|
|
365
|
+
//#region lib/types/markdown.js
|
|
366
|
+
/** Human-readable Markdown projection for durable Session history. */
|
|
367
|
+
/**
|
|
368
|
+
* Resolve the Markdown projection services from a composed Host context.
|
|
369
|
+
* @param ctx - composed Host context containing Session query and persistence services.
|
|
370
|
+
* @returns the services used by Markdown projection.
|
|
371
|
+
*/
|
|
372
|
+
function sessionMarkdownExportDeps(ctx) {
|
|
373
|
+
const sessionQuery = ctx.get("sessionQuery");
|
|
374
|
+
const sessionPersistence = ctx.get("sessionPersistence");
|
|
375
|
+
if (sessionQuery === void 0 || sessionPersistence === void 0) throw new Error("session Markdown export requires session-query and session-persistence services");
|
|
376
|
+
return {
|
|
377
|
+
sessionQuery,
|
|
378
|
+
sessionPersistence,
|
|
379
|
+
sessions: ctx.get("sessions")
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Build a safe suggested filename for one Markdown projection.
|
|
384
|
+
* @param sessionId - Session identity included in the filename.
|
|
385
|
+
* @returns a sanitized Markdown filename.
|
|
386
|
+
*/
|
|
387
|
+
function sessionMarkdownFilename(sessionId) {
|
|
388
|
+
return `dsh-session-${sessionId.replace(/[^A-Za-z0-9_-]/gu, "_")}.md`;
|
|
389
|
+
}
|
|
390
|
+
const MAX_MARKDOWN_OUTPUT_CHARS = 4194304;
|
|
391
|
+
const MAX_BLOCK_CHARS = 16384;
|
|
392
|
+
/**
|
|
393
|
+
* Render a bounded, summary-only Markdown document from one Session lineage.
|
|
394
|
+
*
|
|
395
|
+
* Raw event artifacts remain the source of truth in the ZIP export. This
|
|
396
|
+
* projection intentionally omits tool argument values, includes bounded tool
|
|
397
|
+
* result summaries, and emits attachment references without copying bytes.
|
|
398
|
+
* @param deps - services used to load and query the Session lineage.
|
|
399
|
+
* @param request - projection scope and attachment policy.
|
|
400
|
+
* @param signal - cancellation signal for persistence and lineage reads.
|
|
401
|
+
* @returns the bounded human-readable Markdown projection.
|
|
402
|
+
*/
|
|
403
|
+
async function renderSessionMarkdown(deps, request, signal) {
|
|
404
|
+
const attachmentPolicy = request.attachmentPolicy;
|
|
405
|
+
if (attachmentPolicy !== void 0 && attachmentPolicy !== "reference") throw new Error("unsupported Session Markdown attachment policy");
|
|
406
|
+
const sources = [await loadMarkdownSource(deps, request.sessionId, signal)];
|
|
407
|
+
if (request.includeDescendants) {
|
|
408
|
+
const lineage = await deps.sessionQuery.traceSession(request.sessionId, signal);
|
|
409
|
+
const seen = /* @__PURE__ */ new Set([request.sessionId]);
|
|
410
|
+
const append = async (nodes) => {
|
|
411
|
+
for (const node of nodes) {
|
|
412
|
+
signal.throwIfAborted();
|
|
413
|
+
const id = node.session.header.id;
|
|
414
|
+
if (seen.has(id)) continue;
|
|
415
|
+
seen.add(id);
|
|
416
|
+
sources.push(await loadMarkdownSource(deps, id, signal));
|
|
417
|
+
await append(node.descendants);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
await append(lineage.descendants);
|
|
421
|
+
}
|
|
422
|
+
const lines = [
|
|
423
|
+
"# DeepSeek Harness Session",
|
|
424
|
+
"",
|
|
425
|
+
`- Root Session: \`${safeInline(String(request.sessionId))}\``,
|
|
426
|
+
`- Included descendants: ${request.includeDescendants ? "yes" : "no"}`,
|
|
427
|
+
"- Export: human-readable Markdown projection; the raw ZIP remains the source-of-truth archive.",
|
|
428
|
+
"- Tool arguments: omitted by default; only bounded result summaries are shown.",
|
|
429
|
+
"- Attachments: reference-only (`attachment:<id>`); attachment bytes are not copied.",
|
|
430
|
+
""
|
|
431
|
+
];
|
|
432
|
+
for (const [index, source] of sources.entries()) {
|
|
433
|
+
signal.throwIfAborted();
|
|
434
|
+
if (index > 0) lines.push("");
|
|
435
|
+
lines.push(`## Session \`${safeInline(String(source.header.id))}\``);
|
|
436
|
+
lines.push(`- Workspace: \`${safeInline(source.header.cwd ?? "(no workspace)")}\``);
|
|
437
|
+
lines.push(`- Created: ${formatTime(source.header.createdAt)}`);
|
|
438
|
+
lines.push("");
|
|
439
|
+
appendSurface(lines, source.events);
|
|
440
|
+
}
|
|
441
|
+
const output = `${lines.join("\n").trimEnd()}\n`;
|
|
442
|
+
if (output.length > MAX_MARKDOWN_OUTPUT_CHARS) return `${output.slice(0, MAX_MARKDOWN_OUTPUT_CHARS).trimEnd()}\n\n[Older Markdown content omitted at the export bound.]\n`;
|
|
443
|
+
return output;
|
|
444
|
+
}
|
|
445
|
+
async function loadMarkdownSource(deps, sessionId, signal) {
|
|
446
|
+
signal.throwIfAborted();
|
|
447
|
+
if (deps.sessions !== void 0) {
|
|
448
|
+
const live = deps.sessions.get(sessionId);
|
|
449
|
+
if (live !== void 0) await deps.sessions.flush(live);
|
|
450
|
+
}
|
|
451
|
+
signal.throwIfAborted();
|
|
452
|
+
const loaded = await deps.sessionPersistence.inspect(sessionId, signal);
|
|
453
|
+
signal.throwIfAborted();
|
|
454
|
+
return {
|
|
455
|
+
header: structuredClone(loaded.meta),
|
|
456
|
+
events: loaded.events.map((event) => structuredClone(event))
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function appendSurface(lines, events) {
|
|
460
|
+
const bySeq = new Map(events.map((event) => [event.seq, event]));
|
|
461
|
+
const calls = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const event of events) if (event.type === "tool/call") calls.set(String(event.data.callId), {
|
|
463
|
+
name: event.data.name,
|
|
464
|
+
arguments: event.data.arguments
|
|
465
|
+
});
|
|
466
|
+
const surface = foldSurface(events).nodes;
|
|
467
|
+
let rendered = 0;
|
|
468
|
+
for (const seq of surface) {
|
|
469
|
+
const event = bySeq.get(seq);
|
|
470
|
+
if (event === void 0) continue;
|
|
471
|
+
const renderedLines = renderSurfaceEvent(event, calls);
|
|
472
|
+
if (renderedLines.length === 0) continue;
|
|
473
|
+
if (rendered > 0) lines.push("");
|
|
474
|
+
lines.push(...renderedLines);
|
|
475
|
+
rendered += 1;
|
|
476
|
+
}
|
|
477
|
+
if (rendered === 0) lines.push("_No human-visible surface events._");
|
|
478
|
+
}
|
|
479
|
+
function renderSurfaceEvent(event, calls) {
|
|
480
|
+
if (event.type === "user/message") return renderMessage(event.data.source.kind === "user" ? "User" : "Context", event.seq, event.time, extractSessionEventText(event), event.data.content);
|
|
481
|
+
if (event.type === "assistant/message") return renderMessage("Assistant", event.seq, event.time, extractSessionEventText(event), event.data.message.content);
|
|
482
|
+
if (event.type !== "tool/result") return [];
|
|
483
|
+
const callId = String(event.data.message.source.callId);
|
|
484
|
+
const call = calls.get(callId);
|
|
485
|
+
const result = boundedText(extractSessionEventText(event), MAX_BLOCK_CHARS);
|
|
486
|
+
const lines = [
|
|
487
|
+
`### Tool: ${safeInline(call?.name ?? `call ${callId}`)}`,
|
|
488
|
+
`- Sequence: ${event.seq}`,
|
|
489
|
+
`- Time: ${formatTime(event.time)}`,
|
|
490
|
+
"- Arguments: omitted in summary export.",
|
|
491
|
+
`- Outcome: ${event.data.error === void 0 ? "completed" : "error"}`,
|
|
492
|
+
"Result summary:",
|
|
493
|
+
...fenced(result.text)
|
|
494
|
+
];
|
|
495
|
+
if (result.truncated) lines.push("[Tool result summary truncated at the export bound.]");
|
|
496
|
+
appendAttachmentReferences(lines, event.data.message.content);
|
|
497
|
+
return lines;
|
|
498
|
+
}
|
|
499
|
+
function renderMessage(role, seq, time, text, content) {
|
|
500
|
+
const bounded = boundedText(text, MAX_BLOCK_CHARS);
|
|
501
|
+
if (bounded.text === "" && attachmentIds(content).length === 0) return [];
|
|
502
|
+
const lines = [
|
|
503
|
+
`### ${role}`,
|
|
504
|
+
`- Sequence: ${seq}`,
|
|
505
|
+
`- Time: ${formatTime(time)}`
|
|
506
|
+
];
|
|
507
|
+
if (bounded.text !== "") lines.push(...blockquote(bounded.text));
|
|
508
|
+
if (bounded.truncated) lines.push("[Message content truncated at the export bound.]");
|
|
509
|
+
appendAttachmentReferences(lines, content);
|
|
510
|
+
return lines;
|
|
511
|
+
}
|
|
512
|
+
function appendAttachmentReferences(lines, content) {
|
|
513
|
+
const ids = attachmentIds(content);
|
|
514
|
+
if (ids.length === 0) return;
|
|
515
|
+
lines.push(`- Attachments (reference-only): ${ids.map((id) => `\`attachment:${safeInline(id)}\``).join(", ")}`);
|
|
516
|
+
}
|
|
517
|
+
function attachmentIds(value) {
|
|
518
|
+
const ids = /* @__PURE__ */ new Set();
|
|
519
|
+
const visit = (current) => {
|
|
520
|
+
if (Array.isArray(current)) {
|
|
521
|
+
for (const item of current) visit(item);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (typeof current !== "object" || current === null) return;
|
|
525
|
+
const record = current;
|
|
526
|
+
if (record.type === "image" && typeof record.attachment === "object" && record.attachment !== null) {
|
|
527
|
+
const id = record.attachment.attachmentId;
|
|
528
|
+
if (typeof id === "string" || typeof id === "number") ids.add(String(id));
|
|
529
|
+
}
|
|
530
|
+
visit(record.content);
|
|
531
|
+
};
|
|
532
|
+
visit(value);
|
|
533
|
+
return [...ids];
|
|
534
|
+
}
|
|
535
|
+
function boundedText(value, maxChars) {
|
|
536
|
+
const safe = value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu, "�").replace(/\r\n?/gu, "\n").trim();
|
|
537
|
+
if (safe.length <= maxChars) return {
|
|
538
|
+
text: safe,
|
|
539
|
+
truncated: false
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
text: `${safe.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`,
|
|
543
|
+
truncated: true
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function blockquote(value) {
|
|
547
|
+
return value.split("\n").map((line) => line === "" ? ">" : `> ${line}`);
|
|
548
|
+
}
|
|
549
|
+
function fenced(value) {
|
|
550
|
+
const longest = Math.max(0, ...value.match(/`+/gu)?.map((match) => match.length) ?? []);
|
|
551
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
552
|
+
return [
|
|
553
|
+
fence,
|
|
554
|
+
value === "" ? "[empty]" : value,
|
|
555
|
+
fence
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
function safeInline(value) {
|
|
559
|
+
return value.replace(/[\u0000-\u001F\u007F-\u009F]/gu, "�").replace(/`/gu, "'").replace(/\s+/gu, " ").trim() || "(unknown)";
|
|
560
|
+
}
|
|
561
|
+
function formatTime(value) {
|
|
562
|
+
const date = new Date(value);
|
|
563
|
+
return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
363
566
|
//#region lib/types/index.js
|
|
364
567
|
/**
|
|
365
568
|
* Host session-log export service: prepares the canonical stored-artifact ZIP,
|
|
@@ -531,6 +734,53 @@ var SessionLogExporter = class extends Service {
|
|
|
531
734
|
reader.releaseLock();
|
|
532
735
|
}
|
|
533
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* 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.
|
|
742
|
+
* @param request - root identity, descendant policy, and explicit attachment policy.
|
|
743
|
+
* @param directory - existing absolute host directory chosen by the operator.
|
|
744
|
+
* @param signal - complete projection, writing, and publication lifetime.
|
|
745
|
+
* @returns the exact published Markdown path and filename.
|
|
746
|
+
*/
|
|
747
|
+
async writeMarkdownToDirectory(request, directory, signal) {
|
|
748
|
+
signal.throwIfAborted();
|
|
749
|
+
if (!isAbsolute(directory)) throw new SessionLogExportError("destination-invalid", "Session Markdown export destination must be an absolute directory");
|
|
750
|
+
try {
|
|
751
|
+
if (!(await stat(directory)).isDirectory()) throw new Error("destination is not a directory");
|
|
752
|
+
} catch (error) {
|
|
753
|
+
throw new SessionLogExportError("destination-invalid", `Session Markdown export destination is not an accessible directory: ${directory}`, { cause: error });
|
|
754
|
+
}
|
|
755
|
+
let markdown;
|
|
756
|
+
try {
|
|
757
|
+
markdown = await renderSessionMarkdown(sessionMarkdownExportDeps(this.ctx), request, signal);
|
|
758
|
+
signal.throwIfAborted();
|
|
759
|
+
} catch (error) {
|
|
760
|
+
signal.throwIfAborted();
|
|
761
|
+
throw new SessionLogExportError("prepare-failed", "Session Markdown export failed to prepare the projection", { cause: error });
|
|
762
|
+
}
|
|
763
|
+
const filename = sessionMarkdownFilename(request.sessionId);
|
|
764
|
+
const tempPath = join(directory, `.${filename}.${randomUUID()}.tmp`);
|
|
765
|
+
let file;
|
|
766
|
+
try {
|
|
767
|
+
file = await open(tempPath, "wx", 384);
|
|
768
|
+
await writeChunk(file, new TextEncoder().encode(markdown), signal);
|
|
769
|
+
signal.throwIfAborted();
|
|
770
|
+
await file.sync();
|
|
771
|
+
await file.close();
|
|
772
|
+
file = void 0;
|
|
773
|
+
return await publishExclusive(tempPath, directory, filename, signal);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
try {
|
|
776
|
+
await file?.close();
|
|
777
|
+
} catch {}
|
|
778
|
+
await rm(tempPath, { force: true });
|
|
779
|
+
signal.throwIfAborted();
|
|
780
|
+
if (error instanceof SessionLogExportError) throw error;
|
|
781
|
+
throw new SessionLogExportError("write-failed", "Session Markdown export failed while writing the projection", { cause: error });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
534
784
|
};
|
|
535
785
|
//#endregion
|
|
536
|
-
export { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, SessionLogExportError, SessionLogExporter as default, flushLiveSessionLog, prepareSessionLogExport, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, streamSessionLogZip };
|
|
786
|
+
export { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, SessionLogExportError, SessionLogExporter as default, flushLiveSessionLog, prepareSessionLogExport, renderSessionMarkdown, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, sessionMarkdownExportDeps, sessionMarkdownFilename, streamSessionLogZip };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,8 +9,11 @@ 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
11
|
import { type SessionLogCompressionLevel, type SessionLogExportReady } from './zip.ts';
|
|
12
|
+
import type { SessionMarkdownExportRequest } from './markdown.ts';
|
|
12
13
|
export { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, streamSessionLogZip, } from './zip.ts';
|
|
13
14
|
export type { SessionLogCompressionLevel, SessionLogExportDeps, SessionLogExportReady, SessionLogZipEntry, } from './zip.ts';
|
|
15
|
+
export { renderSessionMarkdown, sessionMarkdownExportDeps, sessionMarkdownFilename, } from './markdown.ts';
|
|
16
|
+
export type { SessionMarkdownAttachmentPolicy, SessionMarkdownExportDeps, SessionMarkdownExportRequest, SessionMarkdownSessionStore, } from './markdown.ts';
|
|
14
17
|
/** Stable failure categories shared by the native writer and host transports. */
|
|
15
18
|
export type SessionLogExportErrorCode = 'services-unavailable' | 'raw-artifacts-unsupported' | 'session-not-found' | 'prepare-failed' | 'destination-invalid' | 'write-failed';
|
|
16
19
|
/** Typed session-log export failure with a consumer-safe message. */
|
|
@@ -100,5 +103,16 @@ export default class SessionLogExporter extends Service {
|
|
|
100
103
|
* @returns the exact published path and filename.
|
|
101
104
|
*/
|
|
102
105
|
writeToDirectory(request: SessionLogExportRequest, directory: string, signal: AbortSignal): Promise<SessionLogExportFile>;
|
|
106
|
+
/**
|
|
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.
|
|
111
|
+
* @param request - root identity, descendant policy, and explicit attachment policy.
|
|
112
|
+
* @param directory - existing absolute host directory chosen by the operator.
|
|
113
|
+
* @param signal - complete projection, writing, and publication lifetime.
|
|
114
|
+
* @returns the exact published Markdown path and filename.
|
|
115
|
+
*/
|
|
116
|
+
writeMarkdownToDirectory(request: SessionMarkdownExportRequest, directory: string, signal: AbortSignal): Promise<SessionLogExportFile>;
|
|
103
117
|
}
|
|
104
118
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Human-readable Markdown projection for durable Session history. */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import { type SessionId } from '@deepseek-ai/dsh-session';
|
|
4
|
+
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence';
|
|
5
|
+
import type { SessionQueryEngine } from '@deepseek-ai/dsh-session-query';
|
|
6
|
+
/** Attachment policy supported by the summary Markdown export. */
|
|
7
|
+
export type SessionMarkdownAttachmentPolicy = 'reference';
|
|
8
|
+
/** Request for one human-readable Session projection. */
|
|
9
|
+
export interface SessionMarkdownExportRequest {
|
|
10
|
+
/** Root Session identity. */
|
|
11
|
+
readonly sessionId: SessionId;
|
|
12
|
+
/** Whether all known durable descendants are included. */
|
|
13
|
+
readonly includeDescendants: boolean;
|
|
14
|
+
/** Attachment handling; references are explicit and bytes are never copied. */
|
|
15
|
+
readonly attachmentPolicy?: SessionMarkdownAttachmentPolicy;
|
|
16
|
+
}
|
|
17
|
+
/** Services required to build a Markdown projection. */
|
|
18
|
+
export interface SessionMarkdownExportDeps {
|
|
19
|
+
readonly sessionQuery: SessionQueryEngine;
|
|
20
|
+
readonly sessionPersistence: SessionPersistence;
|
|
21
|
+
readonly sessions: SessionMarkdownSessionStore | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Minimal live-session surface needed by the flush barrier. */
|
|
24
|
+
export interface SessionMarkdownSessionStore {
|
|
25
|
+
get(id: SessionId): {
|
|
26
|
+
id: SessionId;
|
|
27
|
+
} | undefined;
|
|
28
|
+
flush(session: {
|
|
29
|
+
id: SessionId;
|
|
30
|
+
}): Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the Markdown projection services from a composed Host context.
|
|
34
|
+
* @param ctx - composed Host context containing Session query and persistence services.
|
|
35
|
+
* @returns the services used by Markdown projection.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sessionMarkdownExportDeps(ctx: Context): SessionMarkdownExportDeps;
|
|
38
|
+
/**
|
|
39
|
+
* Build a safe suggested filename for one Markdown projection.
|
|
40
|
+
* @param sessionId - Session identity included in the filename.
|
|
41
|
+
* @returns a sanitized Markdown filename.
|
|
42
|
+
*/
|
|
43
|
+
export declare function sessionMarkdownFilename(sessionId: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* Render a bounded, summary-only Markdown document from one Session lineage.
|
|
46
|
+
*
|
|
47
|
+
* Raw event artifacts remain the source of truth in the ZIP export. This
|
|
48
|
+
* projection intentionally omits tool argument values, includes bounded tool
|
|
49
|
+
* result summaries, and emits attachment references without copying bytes.
|
|
50
|
+
* @param deps - services used to load and query the Session lineage.
|
|
51
|
+
* @param request - projection scope and attachment policy.
|
|
52
|
+
* @param signal - cancellation signal for persistence and lineage reads.
|
|
53
|
+
* @returns the bounded human-readable Markdown projection.
|
|
54
|
+
*/
|
|
55
|
+
export declare function renderSessionMarkdown(deps: SessionMarkdownExportDeps, request: SessionMarkdownExportRequest, signal: AbortSignal): Promise<string>;
|
|
56
|
+
//# sourceMappingURL=markdown.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingxi-ai-cn/dsh-session-export",
|
|
3
3
|
"description": "Host-owned streaming Session ZIP producer and atomic path writer",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.9",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@deepseek-ai/cordis": "4.0.1",
|
|
47
47
|
"@deepseek-ai/dsh-attachment": "0.1.0-rc.8",
|
|
48
48
|
"@deepseek-ai/dsh-invariants": "0.1.0-rc.8",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
49
50
|
"@deepseek-ai/dsh-session": "0.1.0-rc.8",
|
|
50
51
|
"@deepseek-ai/dsh-session-persistence": "0.1.0-rc.8",
|
|
51
52
|
"@deepseek-ai/dsh-session-query": "0.1.0-rc.8",
|