feat(deepseek): upload incremental session logs

This commit is contained in:
Tianyi Cui committed 2026-08-22 22:55:02 +08:00
1 parent df6e581f58
commit fe72ab42d1
59 files changed
+1345 -58

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md
2026-08-21-deepseek-llm-api-request-extensions.md: d83b53adcfbf41f9addde0b7d4ac9ab1a8572d2f
2026-08-21-deepseek-llm-api-request-extensions.zh.md: 47d7ce574c189f8d71d28963910e948c86169198
2026-08-21-deepseek-llm-api-request-extensions.md: 9353cfc7dcefff3156d1e0be446c6e0e85e94f73
2026-08-21-deepseek-llm-api-request-extensions.zh.md: 77959c037b4658672639f15e0a1da69bb5a49a1f
@@ -1,4 +1,4 @@
# Agent Note: DeepSeek LLM API request extensions for plugin package metadata
# Agent Note: DeepSeek LLM API request extensions for session logs and plugin packages
Status: implemented
@@ -6,17 +6,25 @@ English | [中文](2026-08-21-deepseek-llm-api-request-extensions.zh.md)
## Problem
Provider-side diagnosis needs the exact active plugin package versions that produced an official DeepSeek request. The existing browser-facing plugin inventory reports configured Loader rows and lifecycle phases but owns neither package-manifest resolution nor the requesting agent's standing preset composition.
The canonical Session log contains request boundaries, raw response chunks, assembled messages, tool activity, plugin events, and failure facts that the model message list does not preserve. The OTel session-telemetry path projects and batches that log independently of model requests, uses deployment-selected sharing modes, and intentionally drops most assistant chunks. DeepSeek's official API therefore cannot reconstruct the complete harness trajectory from its ordinary request messages or the telemetry feed.
This metadata belongs only on the official DeepSeek adapter path. Adding it to `GenerateOptions` or the provider-neutral LLM seam would expose a DeepSeek wire concept to pi-ai and every future adapter.
Provider-side diagnosis also needs the exact active plugin package versions that produced a request. The existing browser-facing plugin inventory reports configured Loader rows and lifecycle phases but owns neither package-manifest resolution nor the requesting agent's standing preset composition.
The adapter also needs one plugin-owned extension point. Importing Loader, preset, and package-manifest logic directly into `llm-deepseek` would make the transport own metadata discovery and prevent independent request fields from evolving as plugins.
Both values belong only on the official DeepSeek adapter path. Adding them to `GenerateOptions` or the provider-neutral LLM seam would expose DeepSeek wire concepts to pi-ai and every future adapter.
## Decision
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter.
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: package metadata is enabled by default, while Session-log upload is disabled by default and requires `session-log-deepseek.enabled: true`. Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
Shipped compositions mount the registry and the default-on plugin-package contributor. Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes. The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call.
The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call.
## Incremental session-log field
`@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log` as an explicit opt-in. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store.
Event strings are raw unless exact request-relative references reduce encoded bytes. A reference identifies one serialized DeepSeek message, a path to one parsed string value, and a half-open UTF-8 byte range. The encoder verifies that each candidate range decodes to the exact matched UTF-16 substring; surrogate-splitting and ill-formed UTF-16 candidates stay raw. The decoder reconstructs every literal and cited byte exactly and rejects invalid paths, ranges, or code-point splits. Fuzzy similarity, normalization, and lossy omission are absent.
## Plugin package field
@@ -42,13 +50,17 @@ The process-lifetime manifest-identity cache remains separate because in-process
## Verification
Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, real Loader composition inspects the default metadata field, one credentialed real-API request mounts the production contributor, and pi-ai tests retain their unchanged wire requests.
Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, exact Unicode reconstruction, invalid references, surrogate-safe raw fallback, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests.
## Alternatives considered
**Add generic metadata to `GenerateOptions` or `ctx.llm`.** Rejected because the value and acceptance timing are DeepSeek wire semantics; a provider-neutral request would make every adapter understand or ignore a foreign field.
**Add generic metadata to `GenerateOptions` or `ctx.llm`.** Rejected because the values and acceptance timing are DeepSeek wire semantics; a provider-neutral request would make every adapter understand or ignore foreign fields.
**Hard-wire package discovery into `llm-deepseek`.** Rejected because the adapter would import Loader, preset, and package-manifest logic. The registry keeps transport responsible only for field merge and HTTP acceptance.
**Hard-wire the two producers into `llm-deepseek`.** Rejected because the adapter would import Session, Loader, preset, package-manifest, and cursor logic. The registry keeps transport responsible only for field merge and HTTP acceptance.
**Use fuzzy message similarity or omit overlapping event data.** Rejected because the receiver could not reconstruct the canonical log. Exact byte references with raw fallback preserve every value.
**Keep the upload cursor only in memory.** Rejected because a normal process restart would resend the entire Session. A canonical acceptance event makes restart recovery best-effort durable without another storage backend; the remaining crash window produces allowed duplicates.
**Inventory every live Cordis fiber.** Rejected because programmatic and in-memory fibers have no authoritative npm package provenance. Loader-backed host and preset entries provide exact resolvable package identity.
@@ -58,6 +70,8 @@ Registry tests pin duplicate ownership, effect-scoped disposal, detached field v
## Consequences
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. The field is model-hidden and adds no prompt tokens or KV-cache changes. Manifest resolution, field collision, acceptance handling, or provider schema rejection fails the model request rather than silently dropping metadata.
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Encoding, manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata.
Direct calls without a live Agent still carry the host package inventory. The [DeepSeek request-identity decision](../feature/2026-08-11-deepseek-request-user-id-header.md) continues to own user/session headers, which remain outside the body.
The accepted-watermark event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available.
The [DeepSeek request-identity decision](../feature/2026-08-11-deepseek-request-user-id-header.md) continues to own user/session headers, which remain outside the body. The [session-telemetry decision](../feature/2026-07-23-session-telemetry-otel-revival.md) remains current until a separate change removes that seam and backend; this request path does not alter OTel capture or sharing modes.
@@ -1,4 +1,4 @@
# Agent Note: DeepSeek LLM API 插件包元数据请求扩展
# Agent Note: DeepSeek LLM API 会话日志与插件包请求扩展
Status: implemented
@@ -6,17 +6,25 @@ Status: implemented
## 问题
提供方侧诊断需要产生一条 DeepSeek 官方请求的确切存活插件包版本。现有面向浏览器的插件清单会报告已配置 Loader 配置项与生命周期阶段,但既不拥有包 manifest(元数据清单)解析,也不拥有请求 Agent 的 standing preset 组合。
权威会话日志包含请求边界、原始响应分片、组装后消息、工具活动、插件事件与失败事实,模型消息列表无法保留全部内容。OTel 会话遥测路径独立于模型请求投影和批处理该日志,使用部署方选择的共享模式,并刻意丢弃大多数 assistant 分片。因此,DeepSeek 官方 API 无法从普通请求消息或遥测流重建完整 harness 轨迹。
该元数据只属于 DeepSeek 官方适配器路径。把它加入 `GenerateOptions` 或提供方无关的 LLM seam,会让 pi-ai 与未来每个适配器接触 DeepSeek 协议概念。
提供方侧诊断还需要产生当前请求的确切存活插件包版本。现有面向浏览器的插件清单会报告已配置 Loader 配置项与生命周期阶段,但既不拥有包 manifest(元数据清单)解析,也不拥有请求 Agent 的 standing preset 组合。
适配器还需要一个由插件拥有的扩展点。若 `llm-deepseek` 直接导入 Loader、preset 与包 manifest 逻辑,传输层就会拥有元数据发现,并阻止独立请求字段作为插件分别演进。
两个值都只属于 DeepSeek 官方适配器路径。把它们加入 `GenerateOptions` 或提供方无关的 LLM seam,会让 pi-ai 与未来每个适配器接触 DeepSeek 协议概念。
## 决策
`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。
`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据默认开启,会话日志上传默认关闭,需要设置 `session-log-deepseek.enabled: true`。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
随附组合会挂载注册表与默认开启的插件包贡献方。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。
提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。
## 增量会话日志字段
`@deepseek-ai/dsh-session-log-deepseek` 以显式选择启用的方式拥有 `dsh_session_log`。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。
失败方向为至少一次。传输失败或提供方拒绝不会记录水位。远端接受后、水位持久化前发生崩溃,会在恢复后触发重放,绝不会跳过序列。现有会话检查点会持久化该事件;上传插件不拥有第二份存储。
只有在确切的请求相对引用能够减少编码字节时,事件字符串才不使用原始形式。一个引用会标识一条已序列化 DeepSeek 消息、通向一个解析后字符串值的路径,以及半开 UTF-8 字节范围。编码器会验证每个候选范围都能解码为完全相同的已匹配 UTF-16 子串;拆分 surrogate 或非良构 UTF-16 的候选会保持原始形式。解码器会精确重建每个字面值与引用字节,并拒绝无效路径、范围或码点切分。系统不使用模糊相似度、规范化或有损省略。
## 插件包字段
@@ -42,15 +50,19 @@ Status: implemented
## 验证
注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放固定 2xx 后扩展接受,真实 Loader 组合检查默认元数据字段,一个带凭据的真实 API 请求会挂载生产贡献方;pi-ai 测试保持其协议请求不变。
注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受、确切 Unicode 重建、无效引用、surrogate 安全原始回退与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。
## 考虑过的替代方案
**向 `GenerateOptions` 或 `ctx.llm` 添加通用元数据。** 已否决,因为该值与接受时点属于 DeepSeek 协议语义;提供方无关请求会迫使每个适配器理解或忽略外来字段。
**向 `GenerateOptions` 或 `ctx.llm` 添加通用元数据。** 已否决,因为这些值与接受时点属于 DeepSeek 协议语义;提供方无关请求会迫使每个适配器理解或忽略外来字段。
**把包发现硬编码进 `llm-deepseek`。** 已否决,因为适配器将导入 Loader、preset 与包 manifest 逻辑。注册表让传输只负责字段合并与 HTTP 接受。
**把两个提供方硬编码进 `llm-deepseek`。** 已否决,因为适配器将导入会话、Loader、preset、包 manifest 与游标逻辑。注册表让传输只负责字段合并与 HTTP 接受。
**清点每个存活 Cordis fiber。** 已否决,因为编程式与内存 fiber 没有权威 npm 包来源。Loader 支撑的宿主与 preset 配置项能提供可精确解析的包身份。
**使用模糊消息相似度,或省略重叠事件数据。** 已否决,因为接收方无法重建权威日志。带原始回退的确切字节引用会保留每个值。
**只在内存中保留上传游标。** 已否决,因为普通进程重启会重发完整会话。权威接受事件让重启恢复获得尽力而为的持久性,无需另一存储后端;剩余崩溃窗口只会产生允许的重复。
**清点每个存活 Cordis fiber。** 已否决,因为编程式与内存 fiber 没有权威 NPM 包来源。Loader 支撑的宿主与 preset 配置项能提供可精确解析的包身份。
**cache 一份全进程清单,或按 TTL 使其过期。** 已否决,因为单份不可变清单无法正确反映 Loader 生命周期与逐会话 preset,TTL 则允许元数据在过期边界之间保持陈旧。暂缓的 epoch 设计会根据权威存活状态转换执行失效。
@@ -58,6 +70,8 @@ Status: implemented
## 后果
DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。该字段对模型不可见,不增加提示词 token,也不改变 KV Cache。manifest 解析、字段冲突、接受处理或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。
DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。编码、manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。
缺少存活 Agent 的直接调用仍会携带宿主包清单。[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.zh.md)继续拥有 user/session header,且这些 header 仍位于正文之外。
已接受水位事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。
[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.zh.md)继续拥有 user/session header,且这些 header 仍位于正文之外。[会话遥测决策](../feature/2026-07-23-session-telemetry-otel-revival.zh.md)在另一项变更删除该 seam 与后端之前仍保持当前有效;本请求路径不改变 OTel 捕获或共享模式。
+3
View File
@@ -18,6 +18,8 @@ flowchart LR
cfg --> plugin_dsh_base_deepseek_llm_api_extensions
plugin_dsh_base_session["session<br/>@deepseek-ai/dsh-session"]
cfg --> plugin_dsh_base_session
plugin_dsh_base_session_log_deepseek["session-log-deepseek<br/>@deepseek-ai/dsh-session-log-deepseek"]
cfg --> plugin_dsh_base_session_log_deepseek
plugin_dsh_base_typert["typert<br/>@deepseek-ai/dsh-typert-registry"]
cfg --> plugin_dsh_base_typert
plugin_dsh_base_typert_loader["typert-loader<br/>@deepseek-ai/dsh-typert-loader"]
@@ -177,6 +179,7 @@ flowchart LR
| `llm` | `@deepseek-ai/dsh-llm` |
| `deepseek-llm-api-extensions` | `@deepseek-ai/dsh-deepseek-llm-api-extensions` |
| `session` | `@deepseek-ai/dsh-session` |
| `session-log-deepseek` | `@deepseek-ai/dsh-session-log-deepseek` |
| `typert` | `@deepseek-ai/dsh-typert-registry` |
| `typert-loader` | `@deepseek-ai/dsh-typert-loader` |
| `typert-gateway` | `@deepseek-ai/dsh-api-gateway` |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/capability-seams.md
capability-seams.md: 44ee46a0bc6472c62154df9e4111ed6e6649bacd
capability-seams.zh.md: 62ae7ac460ac9fc1c3df62d38a9dd64956744cec
capability-seams.md: 86760ca4fc83d921bc489b03b5156ab692f04dff
capability-seams.zh.md: 2a44729c0b552bf26a6f51132ceb4ca31c256814
+3 -1
View File
@@ -20,6 +20,7 @@ flowchart LR
pkg_compaction_basic["compaction-basic"]
pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions<br/>Official DeepSeek request extensions"]
pkg_session_log_deepseek["session-log-deepseek"]
pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"]
pkg_token_meter["token-meter"]
svc_tokenMeter["ctx.tokenMeter<br/>Replay token measurement"]
@@ -259,6 +260,7 @@ flowchart LR
pkg_sandbox_local --> svc_sandbox
pkg_sandbox_policy --> svc_sandboxPolicy
pkg_session --> svc_sessions
pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions
pkg_session_persistence --> svc_sessionPersistence
pkg_session_persistence_jsonl --> svc_sessionPersistence
pkg_session_persistence_sqlite --> svc_sessionPersistence
@@ -433,7 +435,7 @@ flowchart LR
| --- | --- | --- | --- | --- | --- | --- |
| `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`, [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. |
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance. |
| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance. |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
| `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
+3 -1
View File
@@ -22,6 +22,7 @@ flowchart LR
pkg_compaction_basic["compaction-basic"]
pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions<br/>Official DeepSeek request extensions"]
pkg_session_log_deepseek["session-log-deepseek"]
pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"]
pkg_token_meter["token-meter"]
svc_tokenMeter["ctx.tokenMeter<br/>Replay token measurement"]
@@ -261,6 +262,7 @@ flowchart LR
pkg_sandbox_local --> svc_sandbox
pkg_sandbox_policy --> svc_sandboxPolicy
pkg_session --> svc_sessions
pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions
pkg_session_persistence --> svc_sessionPersistence
pkg_session_persistence_jsonl --> svc_sessionPersistence
pkg_session_persistence_sqlite --> svc_sessionPersistence
@@ -435,7 +437,7 @@ flowchart LR
| --- | --- | --- | --- | --- | --- | --- |
| `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`, [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 |
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 |
| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 插件准备彼此独立的顶层字段;官方适配器会合并这些字段,并在 HTTP 接受后提交其交付状态。 |
| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 插件准备彼此独立的顶层字段;官方适配器会合并这些字段,并在 HTTP 接受后提交其交付状态。 |
| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 |
| `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/config-catalog.md
config-catalog.md: 003777a66d598edea65796ef01f19540aa212253
config-catalog.zh.md: 16d043b540b64e9ad8ee291bb9454d046c15dadd
config-catalog.md: 77af1ffed349e40f800d0ccd14610b2b2a609a0d
config-catalog.zh.md: d444f337a0a1a9a7419cda8a7eb8774b0666a961
+16
View File
@@ -1724,6 +1724,22 @@ Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`)
Source: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
<a id="deepseek-aidsh-session-log-deepseek"></a>
## `@deepseek-ai/dsh-session-log-deepseek`
Requires: `deepseekLlmApiExtensions` · `sessions`
```ts config-catalog
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `false`. */
enabled?: boolean
}
```
Source: [`packages/session/session-log-deepseek/src/index.ts:25`](../packages/session/session-log-deepseek/src/index.ts)
<a id="deepseek-aidsh-session-persistence-jsonl"></a>
## `@deepseek-ai/dsh-session-persistence-jsonl`
+16
View File
@@ -1726,6 +1726,22 @@ export interface JsonRpcConfig {
来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
<a id="deepseek-aidsh-session-log-deepseek"></a>
## `@deepseek-ai/dsh-session-log-deepseek`
需要:`deepseekLlmApiExtensions` · `sessions`
```ts config-catalog
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `false`. */
enabled?: boolean
}
```
来源:[`packages/session/session-log-deepseek/src/index.ts:25`](../packages/session/session-log-deepseek/src/index.ts)
<a id="deepseek-aidsh-session-persistence-jsonl"></a>
## `@deepseek-ai/dsh-session-persistence-jsonl`
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
event-producer-consumer.md: c3d476fed6e4878e10a81858f918368c75bf7523
event-producer-consumer.zh.md: 82a754094d9bc2daa1fe585d7da5cb33d67aa0a6
event-producer-consumer.md: ee1ab4fe3eba4b6585e980a096a4b50467a7d287
event-producer-consumer.zh.md: f9873bff957e6d141221ff6f5af000a585d25f69
+2 -2
View File
@@ -40,7 +40,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
@@ -71,7 +71,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` |
| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` |
| `internal/status` | - | [`agent`](../packages/core/agent) |
+1 -1
View File
@@ -42,7 +42,7 @@
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`llm`](../packages/llm/llm) |
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/module-graph.md
module-graph.md: 924ec71647a391172d3acb41f53351340330b501
module-graph.zh.md: 594de1d2bd17744e5d611f8b5987d01e27cc1538
module-graph.md: df9b8e1a1fb0a69c2e18ea23773e51a3fecd8a34
module-graph.zh.md: 914daf784b21d14f44bf7001a160b81af1adbd48
+5
View File
@@ -272,6 +272,7 @@ flowchart TD
end
subgraph group_session["packages/session"]
pkg_session_checkpoint_policy["session-checkpoint-policy"]
pkg_session_log_deepseek["session-log-deepseek"]
pkg_session_persistence["session-persistence"]
pkg_session_persistence_jsonl["session-persistence-jsonl"]
pkg_session_persistence_sqlite["session-persistence-sqlite"]
@@ -495,6 +496,9 @@ flowchart TD
pkg_sandbox --> pkg_invariants
pkg_sandbox --> pkg_llm
pkg_sandbox --> pkg_session
pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions
pkg_session_log_deepseek --> pkg_invariants
pkg_session_log_deepseek --> pkg_session
pkg_session_persistence --> pkg_brand
pkg_session_persistence --> pkg_invariants
pkg_session_persistence --> pkg_session
@@ -1560,6 +1564,7 @@ flowchart TD
| [`code-runtime-worker-thread`](../packages/code-runtime/code-runtime-worker-thread) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`acp-snapshot`](../packages/test-support/acp-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+5
View File
@@ -274,6 +274,7 @@ flowchart TD
end
subgraph group_session["packages/session"]
pkg_session_checkpoint_policy["session-checkpoint-policy"]
pkg_session_log_deepseek["session-log-deepseek"]
pkg_session_persistence["session-persistence"]
pkg_session_persistence_jsonl["session-persistence-jsonl"]
pkg_session_persistence_sqlite["session-persistence-sqlite"]
@@ -497,6 +498,9 @@ flowchart TD
pkg_sandbox --> pkg_invariants
pkg_sandbox --> pkg_llm
pkg_sandbox --> pkg_session
pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions
pkg_session_log_deepseek --> pkg_invariants
pkg_session_log_deepseek --> pkg_session
pkg_session_persistence --> pkg_brand
pkg_session_persistence --> pkg_invariants
pkg_session_persistence --> pkg_session
@@ -1562,6 +1566,7 @@ flowchart TD
| [`code-runtime-worker-thread`](../packages/code-runtime/code-runtime-worker-thread) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
| [`acp-snapshot`](../packages/test-support/acp-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
persistence-catalog.md: b505bc4ce1c71198ffc316a4f06d3c00174f3fb7
persistence-catalog.zh.md: 715ae6515f8e7c6df936229becc41d492425b1ef
persistence-catalog.md: 3969ec00068a0b40eb7362a1805313ad35c84e5e
persistence-catalog.zh.md: f0e58af4181c60e1d675f7f5b394accfee0d7cf0
+18
View File
@@ -667,6 +667,24 @@ Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md)
Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts)
### `session-log-deepseek/*`
<a id="session-log-deepseekaccepted--log-only"></a>
#### `session-log-deepseek/accepted` — log-only
```ts persistence-catalog
/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */
'session-log-deepseek/accepted': {
/** Session identity the accepted request carried; inherited fork markers retain the parent's id. */
sessionId: import('@deepseek-ai/dsh-session/types').SessionId
/** Last canonical event included in the accepted request. */
throughSeq: number
}
```
Source: [`packages/session/session-log-deepseek/src/types.ts:54`](../packages/session/session-log-deepseek/src/types.ts)
### `step/*`
<a id="stepend--log-only"></a>
+18
View File
@@ -669,6 +669,24 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
来源:[`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts)
### `session-log-deepseek/*`
<a id="session-log-deepseekaccepted--log-only"></a>
#### `session-log-deepseek/accepted` — log-only
```ts persistence-catalog
/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */
'session-log-deepseek/accepted': {
/** Session identity the accepted request carried; inherited fork markers retain the parent's id. */
sessionId: import('@deepseek-ai/dsh-session/types').SessionId
/** Last canonical event included in the accepted request. */
throughSeq: number
}
```
来源:[`packages/session/session-log-deepseek/src/types.ts:54`](../packages/session/session-log-deepseek/src/types.ts)
### `step/*`
<a id="stepend--log-only"></a>
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md
llm-streaming.md: a0d099bc43d1b6d17cd757e3c6bf9f9ac83f3d4f
llm-streaming.zh.md: 585134620e6b378418ac158af623388a0cfda53d
llm-streaming.md: 4ad7af5673f3894d86af72b04db365fcc0f36608
llm-streaming.zh.md: ff62bccdac018ea57b58d5edec9b7dae448be76a
+1 -1
View File
@@ -666,7 +666,7 @@ interface LlmCallConfigAdapterDefaults {
`ctx.deepseekLlmApiExtensions` is the provider-specific registry for additive top-level fields on `deepseek-official` requests. Contributor plugins use `register(field, provider)` to claim one field; the adapter calls `prepare(request)` after serializing its base body and merges the returned fields before HTTP. The prepared `accept()` transaction runs after 2xx, so a contributor can commit delivery state without treating a transport or provider rejection as acceptance. Preparation, collision, and acceptance failures use `REQUEST_EXTENSION` and fail the model request.
The [wire reference](../deepseek-llm-api-wire-extensions.md) defines the exact request headers, extension transaction, field versions, and receiver obligations. The shipped composition registers [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.md) as the complete active Loader-backed package set. The field remains outside model messages and is absent from the pi-ai adapter path.
The [wire reference](../deepseek-llm-api-wire-extensions.md) defines the exact request headers, extension transaction, field versions, and receiver obligations. The shipped composition registers [`dsh_session_log`](../../packages/session/session-log-deepseek/README.md) as a lossless incremental canonical-log suffix and [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.md) as the complete active Loader-backed package set. These fields remain outside model messages and are absent from the pi-ai adapter path.
## Service and provider contracts
+1 -1
View File
@@ -672,7 +672,7 @@ interface LlmCallConfigAdapterDefaults {
`ctx.deepseekLlmApiExtensions` 是用于向 `deepseek-official` 请求添加顶层字段的提供方特定注册表。贡献插件通过 `register(field, provider)` 认领一个字段;适配器在序列化基础正文后调用 `prepare(request)`,并在 HTTP 前合并返回字段。已准备的 `accept()` 事务会在 2xx 后运行,因此贡献方可以提交交付状态,而不会把传输失败或提供方拒绝当作接受。准备、冲突与接受失败会使用 `REQUEST_EXTENSION`,并使模型请求失败。
[协议参考](../deepseek-llm-api-wire-extensions.zh.md)定义确切的请求标头、扩展事务、字段版本和接收方义务。随附组合会将 [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.zh.md) 注册为完整存活 Loader 包集合。该字段仍位于模型消息之外,也不会进入 pi-ai 适配器路径。
[协议参考](../deepseek-llm-api-wire-extensions.zh.md)定义确切的请求标头、扩展事务、字段版本和接收方义务。随附组合会将 [`dsh_session_log`](../../packages/session/session-log-deepseek/README.zh.md) 注册为无损增量权威日志后缀,并将 [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.zh.md) 注册为完整存活 Loader 包集合。这些字段仍位于模型消息之外,也不会进入 pi-ai 适配器路径。
## 服务与提供方约定
+3
View File
@@ -10,6 +10,8 @@ flowchart LR
cfg["examples/acp-agent<br/>cordis.yml"]
plugin_acp_deepseek_llm_api_extensions["deepseek-llm-api-extensions<br/>@deepseek-ai/dsh-deepseek-llm-api-extensions"]
cfg --> plugin_acp_deepseek_llm_api_extensions
plugin_acp_session_log_deepseek["session-log-deepseek<br/>@deepseek-ai/dsh-session-log-deepseek"]
cfg --> plugin_acp_session_log_deepseek
plugin_acp_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek<br/>@deepseek-ai/dsh-plugin-package-inventory-deepseek"]
cfg --> plugin_acp_plugin_package_inventory_deepseek
plugin_acp_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
@@ -80,6 +82,7 @@ flowchart LR
| Plugin id | Package / module |
| --- | --- |
| `deepseek-llm-api-extensions` | `@deepseek-ai/dsh-deepseek-llm-api-extensions` |
| `session-log-deepseek` | `@deepseek-ai/dsh-session-log-deepseek` |
| `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` |
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
+3
View File
@@ -7,6 +7,9 @@
- id: deepseek-llm-api-extensions
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
+3
View File
@@ -14,6 +14,8 @@ flowchart LR
cfg --> plugin_headless_credentials
plugin_headless_deepseek_llm_api_extensions["deepseek-llm-api-extensions<br/>@deepseek-ai/dsh-deepseek-llm-api-extensions"]
cfg --> plugin_headless_deepseek_llm_api_extensions
plugin_headless_session_log_deepseek["session-log-deepseek<br/>@deepseek-ai/dsh-session-log-deepseek"]
cfg --> plugin_headless_session_log_deepseek
plugin_headless_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek<br/>@deepseek-ai/dsh-plugin-package-inventory-deepseek"]
cfg --> plugin_headless_plugin_package_inventory_deepseek
plugin_headless_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
@@ -69,6 +71,7 @@ flowchart LR
| `settings` | `@deepseek-ai/dsh-settings-file` |
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
| `deepseek-llm-api-extensions` | `@deepseek-ai/dsh-deepseek-llm-api-extensions` |
| `session-log-deepseek` | `@deepseek-ai/dsh-session-log-deepseek` |
| `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` |
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
+3
View File
@@ -18,6 +18,9 @@
- id: deepseek-llm-api-extensions
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
+3
View File
@@ -9,6 +9,9 @@
- id: deepseek-llm-api-extensions
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
@@ -11,6 +11,9 @@
- id: deepseek-llm-api-extensions
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
+1
View File
@@ -43,6 +43,7 @@
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*",
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*",
"@deepseek-ai/dsh-session-log-deepseek": "workspace:*",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
"@deepseek-ai/dsh-llm-replay": "workspace:*",
"@deepseek-ai/dsh-loader-smoke": "workspace:*",
+3
View File
@@ -30,6 +30,9 @@
- id: session
name: '@deepseek-ai/dsh-session'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: typert
name: '@deepseek-ai/dsh-typert-registry'
+1
View File
@@ -74,6 +74,7 @@
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
"@deepseek-ai/dsh-session-log-deepseek": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
@@ -42,6 +42,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'request/header',
'sandbox/mode',
'schedule/change',
'session-log-deepseek/accepted',
'session/end-seed',
'session/title',
'session/title-llm-request',
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/deepseek-llm-api-extensions/README.md
README.md: fd25e1e047316acc4b92c0ea2c5cc3fde85cbe8a
README.zh.md: 9f7d8e75bb6ff042d946a918e746f8b1871b2f08
README.md: 5680b58ee2baff6a6ee2ba0249a7ea145ee7eb78
README.zh.md: 9e7ae393b0cc72328101c89a245b8a3ace173503
@@ -12,7 +12,7 @@ Provider-specific registry for additive top-level fields on official DeepSeek LL
Each provider sees the exact serialized base body, the request `AbortSignal`, plus optional `sessionId` and auxiliary-call `purpose`. It must stop its own work promptly after cancellation and returns `undefined` when its field does not apply to that request. A prepared operation retains the providers it captured even if HMR removes their registrations before HTTP acceptance.
The registry owns addition and lifecycle, not field semantics. `@deepseek-ai/dsh-plugin-package-inventory-deepseek` owns the initial `dsh_plugin_packages` field. The provider-neutral LLM seam and `llm-pi-ai` do not consume this registry.
The registry owns addition and lifecycle, not field semantics. `@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log`; `@deepseek-ai/dsh-plugin-package-inventory-deepseek` owns `dsh_plugin_packages`. The provider-neutral LLM seam and `llm-pi-ai` do not consume this registry.
## Model Experience
@@ -12,7 +12,7 @@
每个提供方都会看到确切的已序列化基础正文、请求 `AbortSignal`,以及可选的 `sessionId` 与辅助调用 `purpose`。提供方必须在取消后迅速停止自身工作;字段不适用于当前请求时返回 `undefined`。即使 HMR(热模块替换)在 HTTP 接受前移除了注册,已准备的操作仍会保留其捕获的提供方。
注册表拥有字段添加与生命周期,不拥有字段语义。`@deepseek-ai/dsh-plugin-package-inventory-deepseek` 拥有首个 `dsh_plugin_packages` 字段。提供方无关的 LLM seam 与 `llm-pi-ai` 都不消费该注册表。
注册表拥有字段添加与生命周期,不拥有字段语义。`@deepseek-ai/dsh-session-log-deepseek` 拥有 `dsh_session_log`;`@deepseek-ai/dsh-plugin-package-inventory-deepseek` 拥有 `dsh_plugin_packages`。提供方无关的 LLM seam 与 `llm-pi-ai` 都不消费该注册表。
## 模型体验
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
README.md: f50de33dfbf7229968c9335972b47ab5a0bd1c41
README.zh.md: 3820972a6cc1eb7a1c7b0855c718c6695b6a3c76
README.md: 7d50a8e99863637a06abf49d26a6eeb419cf1bbd
README.zh.md: c1571149529a2d4e54d67b10f63b60bb2aa1abfd
+1 -1
View File
@@ -88,7 +88,7 @@ The plugin also declares its route in the configurable-provider directory (`ctx.
When `ctx.deepseekLlmApiExtensions` is present, the adapter prepares its registered top-level fields after serializing the exact wire messages and before `fetch`. The same request signal reaches providers, and cancellation stops waiting even when a provider ignores it. Preparation failures and field collisions fail before HTTP with `REQUEST_EXTENSION`. After HTTP 2xx, the adapter awaits the prepared acceptance transaction before consuming the SSE body; an acceptance failure uses the same code, while transport and non-2xx failures do not accept the fields. Fields go to the resolved `baseURL`, including a configured gateway. A composition without the registry sends the base DeepSeek request unchanged.
Shipped profiles and runnable examples mount [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.md) for the complete active `dsh_plugin_packages` field. Package metadata defaults on and remains model-hidden. `llm-pi-ai` neither imports nor calls this provider-specific registry.
Shipped profiles and runnable examples mount [`@deepseek-ai/dsh-session-log-deepseek`](../../session/session-log-deepseek/README.md) for the incremental `dsh_session_log` field and [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.md) for the complete active `dsh_plugin_packages` field. Session-log upload defaults off and requires `session-log-deepseek.enabled: true`; package metadata defaults on. Both remain model-hidden. `llm-pi-ai` neither imports nor calls this provider-specific registry.
## App attribution
+1 -1
View File
@@ -88,7 +88,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
存在 `ctx.deepseekLlmApiExtensions` 时,适配器会在序列化确切协议消息后、`fetch` 前准备其中已注册的顶层字段。提供方会收到同一个请求信号;即使某个提供方忽略信号,取消也会停止等待。准备失败与字段冲突会在 HTTP 前以 `REQUEST_EXTENSION` 失败。HTTP 2xx 后,适配器会先等待已准备的接受事务,再消费 SSE 正文;接受失败使用同一 code,而传输失败与非 2xx 失败不会接受字段。字段会发往解析后的 `baseURL`,包括已配置的网关。未组合该注册表的部署会发送未改变的 DeepSeek 基础请求。
随附 profile 与可运行示例会挂载 [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.zh.md) 以提供完整存活 `dsh_plugin_packages` 字段。插件包元数据默认开启且对模型不可见。`llm-pi-ai` 既不导入也不调用该提供方特定注册表。
随附 profile 与可运行示例会挂载 [`@deepseek-ai/dsh-session-log-deepseek`](../../session/session-log-deepseek/README.zh.md) 以提供增量 `dsh_session_log` 字段,并挂载 [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.zh.md) 以提供完整存活 `dsh_plugin_packages` 字段。会话日志上传默认关闭,需要设置 `session-log-deepseek.enabled: true`;插件包元数据默认开启。两个字段都对模型不可见。`llm-pi-ai` 既不导入也不调用该提供方特定注册表。
## 应用归因
+1
View File
@@ -64,6 +64,7 @@
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-log-deepseek": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-anonymous-user-id": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
@@ -19,8 +19,10 @@ import type {
StoredImageAttachment,
} from '@deepseek-ai/dsh-attachment'
import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import * as PluginPackageInventoryDeepSeek from '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
import * as SessionLogDeepSeek from '@deepseek-ai/dsh-session-log-deepseek'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import type { Config } from '@deepseek-ai/dsh-llm-deepseek'
import { assemble, type AssembledResult } from './assemble.ts'
@@ -184,23 +186,29 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
}
})
it('accepts the plugin-package request extension field', async () => {
it('accepts the session-log and plugin-package request extension fields', async () => {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(Loader)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
await ctx.plugin(SessionLogDeepSeek, { enabled: true })
await ctx.plugin(PluginPackageInventoryDeepSeek)
await ctx.plugin(LlmDeepSeek, { thinking: 'disabled' })
const session = ctx.sessions.create(SessionId('real-extension-fields'))
session.append('turn/start', { turn: 1 })
const result = await assemble(ctx, {
model: FLASH,
messages: ask('Reply with exactly the word: pong'),
maxTokens: 50,
sessionId: session.id,
})
expect(result.finish.kind).toBe('stop')
expect(textOf(result).toLowerCase()).toContain('pong')
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0)
})
it('serves a real request with the key held only by a credentials-local document', async () => {
@@ -18,12 +18,14 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import { credentialRef } from '@deepseek-ai/dsh-credentials'
import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import FileSettingsProvider from '@deepseek-ai/dsh-settings-file'
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'
import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import * as SessionLogDeepSeek from '@deepseek-ai/dsh-session-log-deepseek'
import * as DeepSeekPluginPackageInventory from '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { assemble } from './assemble.ts'
@@ -45,7 +47,7 @@ afterEach(async () => {
})
async function loadComposition(
options: { withDynamic: boolean; baseURL: string; reuseRoot?: string },
options: { withDynamic: boolean; baseURL: string; reuseRoot?: string; enableSessionLog?: boolean },
): Promise<{ ctx: Context; settingsPath: string; credentialsPath: string }> {
// A reused root is the restart case: the same harness home, its documents
// exactly as the previous process left them.
@@ -63,10 +65,17 @@ async function loadComposition(
await writeFile(configPath, [
'- id: llm',
" name: '@deepseek-ai/dsh-llm'",
'- id: session',
" name: '@deepseek-ai/dsh-session'",
'- id: agents',
" name: '@deepseek-ai/dsh-agent'",
'- id: deepseek-llm-api-extensions',
" name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'",
'- id: session-log-deepseek',
" name: '@deepseek-ai/dsh-session-log-deepseek'",
...options.enableSessionLog === true
? [' config:', ' enabled: true']
: [],
'- id: plugin-package-inventory-deepseek',
" name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'",
...options.withDynamic
@@ -97,8 +106,10 @@ async function loadComposition(
ctx.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['@deepseek-ai/dsh-llm', LlmRuntime],
['@deepseek-ai/dsh-session', SessionStore],
['@deepseek-ai/dsh-agent', AgentRegistry],
['@deepseek-ai/dsh-deepseek-llm-api-extensions', DeepSeekLlmApiExtensionRegistry],
['@deepseek-ai/dsh-session-log-deepseek', SessionLogDeepSeek],
['@deepseek-ai/dsh-plugin-package-inventory-deepseek', DeepSeekPluginPackageInventory],
['@deepseek-ai/dsh-settings-file', FileSettingsProvider],
['@deepseek-ai/dsh-credentials-local', LocalCredentialProvider],
@@ -131,19 +142,54 @@ async function loadComposition(
}
describe('llm-deepseek real dynamic composition', () => {
it('sends package inventory by default in the real Loader composition', async () => {
it('keeps session upload off and package inventory on by default in the real Loader composition', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'entry-key')
const server = await mockServer([{ kind: 'sse', events: textEvents }])
const { ctx } = await loadComposition({ withDynamic: false, baseURL: server.url })
const session = ctx.sessions.create(SessionId('extension-composition'))
session.append('turn/start', { turn: 1 })
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], sessionId: session.id })
const request = server.requests[0] as { dsh_plugin_packages: { version: number; packages: unknown[] } }
expect(request).not.toHaveProperty('dsh_session_log')
expect(request.dsh_plugin_packages.packages).toEqual(expect.arrayContaining([
{ name: '@deepseek-ai/dsh-deepseek-llm-api-extensions', version: '0.1.0-rc.8' },
{ name: '@deepseek-ai/dsh-llm-deepseek', version: '0.1.0-rc.8' },
{ name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek', version: '0.1.0-rc.8' },
{ name: '@deepseek-ai/dsh-session-log-deepseek', version: '0.1.0-rc.8' },
]))
expect(request.dsh_plugin_packages.version).toBe(1)
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(-1)
})
it('sends the canonical session suffix when the Loader composition explicitly enables upload', async () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'entry-key')
const server = await mockServer([{ kind: 'sse', events: textEvents }])
const { ctx } = await loadComposition({
withDynamic: false,
baseURL: server.url,
enableSessionLog: true,
})
const session = ctx.sessions.create(SessionId('extension-composition-enabled'))
session.append('turn/start', { turn: 1 })
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], sessionId: session.id })
const request = server.requests[0] as {
dsh_session_log?: {
version: number
session: { id: string }
afterSeq: number
throughSeq: number
events: Array<{ event: { type: string; seq: number } }>
}
}
expect(request.dsh_session_log).toMatchObject({
version: 1,
session: { id: 'extension-composition-enabled' },
afterSeq: -1,
throughSeq: 0,
events: [{ event: { type: 'turn/start', seq: 0 } }],
})
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0)
})
it('boots from cordis.yml and routes the next request after external settings and credential edits', async () => {
@@ -136,6 +136,7 @@ describe('PiAiAdapter provider routing', () => {
thinking: { type: 'enabled' },
reasoning_effort: 'max',
})
expect(server.requests[0]).not.toHaveProperty('dsh_session_log')
expect(server.requests[0]).not.toHaveProperty('dsh_plugin_packages')
})
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/session/session-log-deepseek/README.md
README.md: 5c7d077f641accf37b78cdf59ea2f4bca9cacc55
README.zh.md: 1612441a768ea6e3bdd061fe4da07e4847f22bbe
@@ -0,0 +1,49 @@
# @deepseek-ai/dsh-session-log-deepseek
English | [中文](README.zh.md)
Incremental canonical session-log upload for official DeepSeek LLM API requests. This function plugin injects `ctx.sessions` and `ctx.deepseekLlmApiExtensions`, then owns the `dsh_session_log` request field and the durable `session-log-deepseek/accepted` watermark event.
## Configuration
| Key | Default | Meaning |
|---|---:|---|
| `enabled` | `false` | Register the `dsh_session_log` contribution. Set it to `true` to opt into Session-log upload. |
Shipped profiles mount the plugin so an overlay can enable it, but the default configuration registers no request field and appends no acceptance watermark.
## Request field
For a request carrying a live `sessionId`, the plugin folds the greatest accepted watermark for that exact Session identity, snapshots `Session.events`, and sends the contiguous suffix after the watermark. A process-local fold scans each event once and consumes later appends incrementally; restart and HMR rebuild it from the durable log. The version-1 field contains the immutable `SessionHeader`, `afterSeq`, `throughSeq`, and every complete event envelope in that range. Forked sessions ignore inherited parent watermarks because each watermark records the Session id sent on the accepted request.
Each event is sent raw unless request-relative packing reduces its JSON byte size. The codec traverses the exact serialized DeepSeek `messages`, and a packed string can replace an exact substring with `{ messageIndex, path, utf8Start, utf8End }`. References use half-open UTF-8 offsets into parsed string values; literal fragments retain everything outside the match. The encoder verifies each candidate's local UTF-8 round trip, so a surrogate-splitting or ill-formed UTF-16 match stays raw. The exported decoder rejects missing paths, non-string targets, invalid ranges, and ranges that split a UTF-8 code point. Raw fallback and byte-size comparison make every representation lossless and prevent reference overhead from expanding an event.
## Acceptance and retry
The DeepSeek adapter calls the prepared contribution's `accept()` after HTTP 2xx, before it consumes the SSE body. Acceptance appends `session-log-deepseek/accepted` with the uploaded `throughSeq`; the next request uploads that watermark as part of its new suffix. Transport and non-2xx failures append no watermark, so later requests resend the uncertain range. Concurrent accepted requests may append watermarks out of order; folding their maximum prevents cursor regression.
A crash after server acceptance but before the watermark reaches persistence can replay an accepted range after restart. This is the at-least-once failure direction: uncertainty creates duplicates, never a skipped sequence. The ordinary session checkpoint policy persists the watermark at the next semantic checkpoint; this plugin performs no independent I/O.
Direct requests without a live Session omit `dsh_session_log`. Normal agent, compaction, and session-title calls carry their live Session id.
## Model Experience
### Session-log metadata
#### What the model sees
Nothing. `dsh_session_log` is a sibling of the DeepSeek request's model-input fields and is not inserted into `messages`, the system prompt, or tool schemas.
#### Token effect
Zero model-input tokens; the field only increases HTTP request bytes.
#### KV Cache effect
None; the model-visible request prefix remains unchanged.
## Known Limitations and Deferred Work
- **Crash-window duplicates** — a 2xx followed by process loss before the acceptance watermark persists causes conservative replay on resume.
- **No live Session means no field** — direct or stale-session calls have no canonical log to snapshot; explicit absence semantics remain deferred.
- **No independent request-size cap** — complete delivery is fail-closed; provider rejection leaves the cursor unchanged instead of truncating the log.
@@ -0,0 +1,49 @@
# @deepseek-ai/dsh-session-log-deepseek
[English](README.md) | 中文
用于 DeepSeek 官方 LLM API 请求的增量权威会话日志上传。该函数插件注入 `ctx.sessions` 与 `ctx.deepseekLlmApiExtensions`,并拥有 `dsh_session_log` 请求字段和持久的 `session-log-deepseek/accepted` 水位事件。
## 配置
| 配置键 | 默认值 | 含义 |
|---|---:|---|
| `enabled` | `false` | 注册 `dsh_session_log` 贡献。将其设为 `true` 可选择启用会话日志上传。 |
随附 profile 会挂载该插件,让 overlay 可以启用它;默认配置不会注册请求字段,也不会追加接受水位。
## 请求字段
对于携带存活 `sessionId` 的请求,插件会折叠该确切会话身份的最大已接受水位,对 `Session.events` 取快照,并发送水位之后的连续后缀。进程内 fold 会让每条事件只被扫描一次并增量消费后续追加;重启与 HMR 会从持久日志重建它。版本 1 字段包含不可变的 `SessionHeader`、`afterSeq`、`throughSeq`,以及该范围内每个完整事件信封。每个水位都会记录已接受请求发送的会话 id,因此 fork 会话会忽略从父会话继承的水位。
只有在请求相对打包能够减少 JSON 字节数时,事件才不以原始形式发送。Codec 会遍历确切的已序列化 DeepSeek `messages`,并可在打包字符串中用 `{ messageIndex, path, utf8Start, utf8End }` 替换完全匹配的子字符串。引用使用解析后字符串值的半开 UTF-8 偏移;字面分片保留匹配范围之外的全部内容。编码器会校验每个候选的局部 UTF-8 往返,因此拆分 surrogate 或非良构 UTF-16 的匹配会保持原始形式。导出的解码器会拒绝缺失路径、非字符串目标、无效范围和切断 UTF-8 码点的范围。原始回退与字节数比较让每种表示都保持无损,并阻止引用开销扩张事件。
## 接受与重试
DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文前调用已准备贡献的 `accept()`。接受操作会追加 `session-log-deepseek/accepted` 及已上传的 `throughSeq`;下一次请求再把该水位作为新后缀的一部分上传。传输失败与非 2xx 失败不会追加水位,因此后续请求会重发不确定范围。并发请求可能乱序追加已接受水位;折叠其中最大值可以防止游标回退。
服务端接受后、持久化水位前发生崩溃,可能让恢复后的进程重放已经接受的范围。这是至少一次交付的失败方向:不确定性会制造重复,绝不会跳过序列。普通会话检查点策略会在下一个语义检查点持久化水位;本插件不执行独立 I/O。
缺少存活会话的直接请求会省略 `dsh_session_log`。普通 agent(智能体)、压缩(compaction)与会话标题调用都会携带存活会话 id。
## 模型体验
### 会话日志元数据
#### 模型看到的内容
无。`dsh_session_log` 是 DeepSeek 请求中模型输入字段的同级字段,不会插入 `messages`、系统提示词或工具 schema。
#### Token 影响
模型输入 token 为零;该字段只会增加 HTTP 请求字节数。
#### KV Cache 影响
无;模型可见请求前缀保持不变。
## 已知限制与暂缓事项
- **崩溃窗口重复**——2xx 后、接受水位持久化前进程丢失,会在恢复时触发保守重放。
- **缺少存活会话就没有字段**——直接调用或陈旧会话调用没有可供快照的权威日志;显式缺失语义仍暂缓处理。
- **没有独立请求大小上限**——完整交付会快速失败;提供方拒绝会保持游标不变,而非截断日志。
@@ -0,0 +1,54 @@
{
"name": "@deepseek-ai/dsh-session-log-deepseek",
"description": "Incremental lossless session-log request extension for the official DeepSeek LLM API",
"version": "0.1.1-rc.2",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/session/session-log-deepseek"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./types": {
"types": "./lib/types/types.d.ts",
"default": "./lib/types/types.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}
@@ -0,0 +1,227 @@
/** Lossless request-relative packing for canonical session events. */
import { Buffer } from 'node:buffer'
import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import type { JsonValue, SessionEvent } from '@deepseek-ai/dsh-session'
import type {
DeepSeekMessageStringSlice,
EncodedSessionEvent,
PackedJsonStringPart,
PackedJsonValue,
} from './types.ts'
interface MessageStringSource {
readonly messageIndex: number
readonly path: readonly (string | number)[]
readonly value: string
readonly bytes: number
}
interface PackedCandidate {
readonly value: PackedJsonValue
readonly references: boolean
}
/** Return the serialized UTF-8 size used by the profitability decision. */
function wireBytes(value: unknown): number {
return Buffer.byteLength(JSON.stringify(value), 'utf8')
}
/** Enumerate every string leaf of the exact wire messages. */
function collectSources(
value: DeepSeekLlmApiJson,
messageIndex: number,
path: readonly (string | number)[],
output: MessageStringSource[],
): void {
if (typeof value === 'string') {
if (value.length > 0) output.push({ messageIndex, path, value, bytes: Buffer.byteLength(value, 'utf8') })
return
}
if (Array.isArray(value)) {
value.forEach((child, index) => { collectSources(child, messageIndex, [...path, index], output) })
return
}
if (value === null || typeof value !== 'object') return
for (const [key, child] of Object.entries(value)) collectSources(child, messageIndex, [...path, key], output)
}
/** Build one exact slice from UTF-16 offsets, or reject a lossy UTF-8 conversion. */
function sliceOf(source: MessageStringSource, start: number, end: number): DeepSeekMessageStringSlice | undefined {
const candidate: DeepSeekMessageStringSlice = {
messageIndex: source.messageIndex,
path: source.path,
utf8Start: Buffer.byteLength(source.value.slice(0, start), 'utf8'),
utf8End: Buffer.byteLength(source.value.slice(0, end), 'utf8'),
}
const bytes = Buffer.from(source.value, 'utf8')
const selected = bytes.subarray(candidate.utf8Start, candidate.utf8End)
const decoded = selected.toString('utf8')
return decoded === source.value.slice(start, end) && Buffer.from(decoded, 'utf8').equals(selected)
? candidate
: undefined
}
/** Find a profitable exact whole-string or one-inner-string reference. */
function packString(value: string, sources: readonly MessageStringSource[]): PackedCandidate {
const literal: PackedJsonValue = { kind: 'literal', value }
if (value.length === 0) return { value: literal, references: false }
let best: PackedJsonValue | undefined
let bestBytes = wireBytes(literal)
for (const source of sources) {
const start = source.value.indexOf(value)
if (start < 0) continue
const slice = sliceOf(source, start, start + value.length)
if (slice === undefined) continue
const candidate: PackedJsonValue = {
kind: 'string',
parts: [{ kind: 'message-slice', value: slice }],
}
const bytes = wireBytes(candidate)
if (bytes < bestBytes) {
best = candidate
bestBytes = bytes
}
}
if (best !== undefined) return { value: best, references: true }
for (const source of sources) {
const start = value.indexOf(source.value)
if (start < 0) continue
const slice = sliceOf(source, 0, source.value.length)
if (slice === undefined) continue
const parts: PackedJsonStringPart[] = []
if (start > 0) parts.push({ kind: 'literal', value: value.slice(0, start) })
parts.push({ kind: 'message-slice', value: slice })
const end = start + source.value.length
if (end < value.length) parts.push({ kind: 'literal', value: value.slice(end) })
const candidate: PackedJsonValue = { kind: 'string', parts }
const bytes = wireBytes(candidate)
if (bytes < bestBytes) {
best = candidate
bestBytes = bytes
}
}
return best === undefined
? { value: literal, references: false }
: { value: best, references: true }
}
/** Recursively pack one JSON value, retaining references only when the complete subtree shrinks. */
function packValue(value: JsonValue, sources: readonly MessageStringSource[]): PackedCandidate {
const literal: PackedJsonValue = { kind: 'literal', value }
if (typeof value === 'string') return packString(value, sources)
if (value === null || typeof value !== 'object') return { value: literal, references: false }
if (Array.isArray(value)) {
const children = value.map(child => packValue(child, sources))
if (!children.some(child => child.references)) return { value: literal, references: false }
const candidate: PackedJsonValue = { kind: 'array', items: children.map(child => child.value) }
return { value: candidate, references: true }
}
const children = Object.entries(value).map(([key, child]) => [key, packValue(child, sources)] as const)
if (!children.some(([, child]) => child.references)) return { value: literal, references: false }
const candidate: PackedJsonValue = {
kind: 'object',
entries: children.map(([key, child]) => [key, child.value]),
}
return { value: candidate, references: true }
}
/** Resolve one path in a parsed wire message. */
function valueAt(root: DeepSeekLlmApiJson, path: readonly (string | number)[]): DeepSeekLlmApiJson {
let value = root
for (const segment of path) {
if (typeof segment === 'number') {
if (!Array.isArray(value) || segment < 0 || segment >= value.length) {
throw new Error(`session-log-deepseek: message reference has invalid array segment ${segment}`)
}
value = value[segment] as DeepSeekLlmApiJson
continue
}
if (value === null || typeof value !== 'object' || Array.isArray(value) || !Object.hasOwn(value, segment)) {
throw new Error(`session-log-deepseek: message reference has invalid object segment ${JSON.stringify(segment)}`)
}
value = value[segment] as DeepSeekLlmApiJson
}
return value
}
/** Decode and validate one exact UTF-8 message slice. */
function decodeSlice(messages: readonly DeepSeekLlmApiJson[], slice: DeepSeekMessageStringSlice): string {
const message = messages[slice.messageIndex]
if (message === undefined) throw new Error(`session-log-deepseek: message reference index ${slice.messageIndex} is absent`)
const source = valueAt(message, slice.path)
if (typeof source !== 'string') throw new Error('session-log-deepseek: message reference path does not resolve to a string')
const bytes = Buffer.from(source, 'utf8')
if (!Number.isSafeInteger(slice.utf8Start) || !Number.isSafeInteger(slice.utf8End)
|| slice.utf8Start < 0 || slice.utf8End <= slice.utf8Start || slice.utf8End > bytes.length) {
throw new Error('session-log-deepseek: message reference byte range is invalid')
}
const selected = bytes.subarray(slice.utf8Start, slice.utf8End)
const decoded = selected.toString('utf8')
if (!Buffer.from(decoded, 'utf8').equals(selected)) {
throw new Error('session-log-deepseek: message reference splits a UTF-8 code point')
}
return decoded
}
/**
* Decode one packed JSON value against the containing request's messages.
* @param value - tagged literal/reference value to reconstruct.
* @param messages - serialized messages from the same request.
* @returns the exact reconstructed JSON value.
*/
export function unpackJsonValue(value: PackedJsonValue, messages: readonly DeepSeekLlmApiJson[]): JsonValue {
switch (value.kind) {
case 'literal':
return structuredClone(value.value)
case 'string':
return value.parts.map(part => part.kind === 'literal' ? part.value : decodeSlice(messages, part.value)).join('')
case 'array':
return value.items.map(item => unpackJsonValue(item, messages))
case 'object':
return Object.fromEntries(value.entries.map(([key, child]) => [key, unpackJsonValue(child, messages)]))
default:
throw new Error('session-log-deepseek: unknown packed JSON value')
}
}
/**
* Encode canonical session events against the exact DeepSeek wire messages.
* @param events - immutable canonical event suffix.
* @param messages - serialized request messages that the receiver also holds.
* @returns one raw-or-referenced representation per event.
*/
export function packSessionEvents(
events: readonly SessionEvent[],
messages: readonly DeepSeekLlmApiJson[],
): EncodedSessionEvent[] {
const sources: MessageStringSource[] = []
messages.forEach((message, messageIndex) => { collectSources(message, messageIndex, [], sources) })
sources.sort((left, right) => right.bytes - left.bytes)
return events.map((event) => {
const packed = packValue(event as unknown as JsonValue, sources)
const raw: EncodedSessionEvent = { encoding: 'raw', event }
if (!packed.references) return raw
const referenced: EncodedSessionEvent = { encoding: 'message-references', event: packed.value }
return wireBytes(referenced) < wireBytes(raw) ? referenced : raw
})
}
/**
* Reconstruct canonical session events from one request-relative representation.
* @param events - encoded event list from the extension field.
* @param messages - serialized messages from the same request.
* @returns reconstructed event values in order.
*/
export function unpackSessionEvents(
events: readonly EncodedSessionEvent[],
messages: readonly DeepSeekLlmApiJson[],
): SessionEvent[] {
return events.map(encoded => encoded.encoding === 'raw'
? structuredClone(encoded.event)
: unpackJsonValue(encoded.event, messages) as unknown as SessionEvent)
}
@@ -0,0 +1,109 @@
/**
* Incremental session-log contribution for official DeepSeek LLM API requests.
* Accepted sequence watermarks live in the canonical log, so restart recovery
* can conservatively resend uncertain tails without maintaining another store.
* @module @deepseek-ai/dsh-session-log-deepseek
*/
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import { SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
import { packSessionEvents } from './codec.ts'
import type { DeepSeekSessionLogExtension } from './types.ts'
import type {} from './types.ts'
export { packSessionEvents, unpackJsonValue, unpackSessionEvents } from './codec.ts'
export type * from './types.ts'
/** Cordis plugin name. */
export const name = 'session-log-deepseek'
/** Services required to resolve sessions and contribute the provider request field. */
export const inject = ['deepseekLlmApiExtensions', 'sessions']
/** Session-log request contribution configuration. */
export interface Config {
/** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `false`. */
enabled?: boolean
}
/** Validated Session-log request contribution configuration. */
export const Config: z<Config> = z.object({
enabled: z.boolean().default(false),
})
interface AcceptanceFold {
readonly scannedEvents: number
readonly throughSeq: number
}
const acceptanceFolds = new WeakMap<Session, AcceptanceFold>()
/**
* Highest confirmed sequence for this exact session identity.
* @param session - canonical log whose matching acceptance events are folded.
* @returns greatest accepted sequence, or `-1` before any accepted request.
*/
export function acceptedThrough(session: Session): number {
const previous = acceptanceFolds.get(session)
let throughSeq = previous?.throughSeq ?? -1
const events = session.events
const start = previous?.scannedEvents ?? 0
for (let index = start; index < events.length; index++) {
const event = events[index] as SessionEvent
if (event.type !== 'session-log-deepseek/accepted') continue
if (typeof event.data.sessionId !== 'string' || event.data.sessionId.length === 0
|| !Number.isSafeInteger(event.data.throughSeq) || event.data.throughSeq < 0
|| event.data.throughSeq >= event.seq) {
throw new Error(`session-log-deepseek: malformed acceptance watermark at seq ${event.seq}`)
}
if (event.data.sessionId !== session.id) continue
throughSeq = Math.max(throughSeq, event.data.throughSeq)
}
acceptanceFolds.set(session, { scannedEvents: events.length, throughSeq })
return throughSeq
}
/** Resolve the serialized message list or fail before transport. */
function requestMessages(body: Readonly<Record<string, DeepSeekLlmApiJson>>): readonly DeepSeekLlmApiJson[] {
const messages = body.messages
if (!Array.isArray(messages)) throw new Error('session-log-deepseek: DeepSeek request body has no messages array')
return messages
}
/**
* Register the incremental `dsh_session_log` request contribution when enabled.
* @param ctx - plugin context carrying Sessions and the DeepSeek request-extension registry.
* @param config - validated opt-in configuration.
*/
export function apply(ctx: Context, config: Config): void {
if (config.enabled !== true) return
ctx.deepseekLlmApiExtensions.register('dsh_session_log', {
prepare: (request) => {
// TODO: Define an explicit wire result for direct or stale-session calls if they become a supported product path.
if (request.sessionId === undefined) return undefined
const session = ctx.sessions.get(SessionId(request.sessionId))
if (session === undefined) return undefined
const afterSeq = acceptedThrough(session)
const snapshot = session.events
const throughSeq = snapshot.length - 1
if (throughSeq < 0) return undefined
const suffix = snapshot.slice(afterSeq + 1)
const value: DeepSeekSessionLogExtension = {
version: 1,
session: session.header,
afterSeq,
throughSeq,
events: packSessionEvents(suffix, requestMessages(request.body)),
}
return {
value,
accept: () => {
session.append('session-log-deepseek/accepted', { sessionId: session.id, throughSeq })
// TODO: Add an immediate lightweight checkpoint if duplicate replay after a 2xx crash window becomes unacceptable.
},
}
},
})
}
@@ -0,0 +1,58 @@
/** Package-owned invariants for DeepSeek session-log acceptance watermarks. */
import type { Context } from '@deepseek-ai/cordis'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type {} from './types.ts'
const PACKAGE_NAME = '@deepseek-ai/dsh-session-log-deepseek'
/** Cordis companion plugin name. */
export const name = 'session-log-deepseek-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** Validate one acceptance watermark against its containing event and session. */
function validateAccepted(session: Session, event: SessionEvent<'session-log-deepseek/accepted'>, fail: InvariantFailure): void {
const { sessionId, throughSeq } = event.data
const inherited = session.header.parentSession !== undefined
&& session.header.seedLength !== undefined
&& event.seq < session.header.seedLength
if (sessionId !== session.id && !inherited) {
fail('a non-inherited session-log-deepseek/accepted event must name its containing session')
}
if (!Number.isSafeInteger(throughSeq) || throughSeq < 0 || throughSeq >= event.seq) {
fail(`session-log-deepseek/accepted throughSeq must identify an earlier event, got ${throughSeq} at seq ${event.seq}`)
}
}
/** Validate acceptance watermarks already present in one Session. */
function validateSession(session: Session, fail: InvariantFailure): void {
for (const event of session.events) {
if (event.type === 'session-log-deepseek/accepted') validateAccepted(session, event, fail)
}
}
/** Validate one live session-event dispatch. */
function validateDispatched(args: unknown[], fail: InvariantFailure): void {
const [session, event] = args as [Session, SessionEvent]
if (event.type === 'session-log-deepseek/accepted') validateAccepted(session, event, fail)
}
/** Install validation for restored, newly created, and newly appended watermarks. */
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
const validateExisting = (session: Session): void => { validateSession(session, fail) }
ctx.sessions.list().forEach(validateExisting)
ctx.on('session/created', validateExisting, { global: true })
ctx.on('internal/dispatch', (_mode, eventName, args) => {
if (eventName === 'session/event') validateDispatched(args, fail)
}, { global: true })
}, { inject: ['sessions'] })
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
@@ -0,0 +1,61 @@
/** Wire types for lossless incremental DeepSeek session-log upload. */
import type { JsonValue, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
/** Path from one DeepSeek wire message root to a string value. */
export type DeepSeekMessageStringPath = readonly (string | number)[]
/** Exact half-open UTF-8 slice of one string in the containing request's messages. */
export interface DeepSeekMessageStringSlice {
readonly messageIndex: number
readonly path: DeepSeekMessageStringPath
readonly utf8Start: number
readonly utf8End: number
}
/** One literal or request-relative fragment of a packed JSON string. */
export type PackedJsonStringPart =
| { readonly kind: 'literal'; readonly value: string }
| { readonly kind: 'message-slice'; readonly value: DeepSeekMessageStringSlice }
/** Tagged JSON representation whose string leaves may cite the containing request. */
export type PackedJsonValue =
| { readonly kind: 'literal'; readonly value: JsonValue }
| { readonly kind: 'string'; readonly parts: readonly PackedJsonStringPart[] }
| { readonly kind: 'array'; readonly items: readonly PackedJsonValue[] }
| { readonly kind: 'object'; readonly entries: readonly (readonly [string, PackedJsonValue])[] }
/** One canonical session event, sent raw unless request-relative references reduce its encoded bytes. */
export type EncodedSessionEvent =
| { readonly encoding: 'raw'; readonly event: SessionEvent }
| { readonly encoding: 'message-references'; readonly event: PackedJsonValue }
/** Versioned incremental session-log field carried by an official DeepSeek request. */
export interface DeepSeekSessionLogExtension {
readonly version: 1
readonly session: SessionHeader
/** Highest sequence durably recorded as accepted before this request, or `-1`. */
readonly afterSeq: number
/** Highest sequence represented by {@link events}. */
readonly throughSeq: number
/** Contiguous canonical events from `afterSeq + 1` through `throughSeq`. */
readonly events: readonly EncodedSessionEvent[]
}
declare module '@deepseek-ai/dsh-deepseek-llm-api-extensions/types' {
interface DeepSeekLlmApiExtensionMap {
dsh_session_log: DeepSeekSessionLogExtension
}
}
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */
'session-log-deepseek/accepted': {
/** Session identity the accepted request carried; inherited fork markers retain the parent's id. */
sessionId: import('@deepseek-ai/dsh-session/types').SessionId
/** Last canonical event included in the accepted request. */
throughSeq: number
}
}
}
@@ -0,0 +1,139 @@
import { describe, expect, it } from 'vitest'
import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import {
packSessionEvents,
unpackJsonValue,
unpackSessionEvents,
} from '../src/codec.ts'
import type { PackedJsonValue } from '../src/types.ts'
function event(data: unknown, seq = 0): SessionEvent {
return {
type: 'plugin/test',
seq,
time: 1_700_000_000_000 + seq,
data,
} as unknown as SessionEvent
}
describe('DeepSeek session-log codec', () => {
it('replaces a raw assistant chunk with an exact slice of its assembled wire message', () => {
const delta = 'streamed assistant content '.repeat(30)
const source = {
type: 'assistant/chunk',
seq: 0,
time: 1,
data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: delta } },
} as SessionEvent<'assistant/chunk'>
const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: `prefix:${delta}:suffix` }]
const packed = packSessionEvents([source], messages)
expect(packed[0]?.encoding).toBe('message-references')
expect(unpackSessionEvents(packed, messages)).toEqual([source])
})
it('references exact whole strings and reconstructs Unicode event values', () => {
const text = `前缀-${'shared text '.repeat(40)}-结尾`
const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: text }]
const source = event({ nested: [{ text }], untouched: 3 })
const packed = packSessionEvents([source], messages)
expect(packed[0]?.encoding).toBe('message-references')
expect(JSON.stringify(packed)).toContain('message-slice')
expect(unpackSessionEvents(packed, messages)).toEqual([source])
})
it('keeps surrogate-splitting and ill-formed UTF-16 strings raw', () => {
const high = String.fromCharCode(0xD83D)
const low = String.fromCharCode(0xDE00)
const tail = 'shared-tail-'.repeat(80)
const cases = [
{ message: `😀${tail}`, logged: `${low}${tail}` },
{ message: `${tail}😀`, logged: `${tail}${high}` },
{ message: `${high}${tail}`, logged: `${high}${tail}` },
]
for (const item of cases) {
const source = event({ text: item.logged })
const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: item.message }]
const packed = packSessionEvents([source], messages)
expect(packed).toEqual([{ encoding: 'raw', event: source }])
expect(unpackSessionEvents(packed, messages)).toEqual([source])
}
})
it('references one large inner message string and preserves literal prefix and suffix', () => {
const shared = 'payload '.repeat(80)
const messages: DeepSeekLlmApiJson[] = [{ role: 'tool', content: shared }]
const source = event({ output: `before:${shared}:after` })
const packed = packSessionEvents([source], messages)
expect(packed[0]?.encoding).toBe('message-references')
expect(unpackSessionEvents(packed, messages)).toEqual([source])
})
it('keeps short or unrelated events raw when references would expand them', () => {
const messages: DeepSeekLlmApiJson[] = [{ role: 'user', content: 'tiny', empty: '', parts: [null, true, 5, 'nested'] }]
const source = event({ text: 'tiny', empty: '', other: ['unrelated', true, null] })
const packed = packSessionEvents([source], messages)
expect(packed).toEqual([{ encoding: 'raw', event: source }])
expect(unpackSessionEvents(packed, messages)).toEqual([source])
})
it('falls back to a raw event when referenced children do not reduce the complete envelope', () => {
let found = false
for (let length = 40; length <= 240; length += 1) {
const text = 'x'.repeat(length)
const source = event({ text })
const packed = packSessionEvents([source], [{ content: text }])
if (packed[0]?.encoding === 'raw' && length > 80) found = true
}
expect(found).toBe(true)
})
it('rejects missing paths, non-string targets, invalid ranges, and split UTF-8 code points', () => {
const messages: DeepSeekLlmApiJson[] = [{ content: '😀abc', nested: [5] }]
const packed = (value: object): PackedJsonValue => ({
kind: 'string',
parts: [{ kind: 'message-slice', value: value as never }],
})
expect(() => unpackJsonValue(packed({ messageIndex: 2, path: ['content'], utf8Start: 0, utf8End: 1 }), messages))
.toThrow(/index 2 is absent/)
expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['missing'], utf8Start: 0, utf8End: 1 }), messages))
.toThrow(/invalid object segment/)
expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['nested', 0], utf8Start: 0, utf8End: 1 }), messages))
.toThrow(/does not resolve to a string/)
expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['nested', 2], utf8Start: 0, utf8End: 1 }), messages))
.toThrow(/invalid array segment/)
expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['content'], utf8Start: -1, utf8End: 1 }), messages))
.toThrow(/byte range is invalid/)
expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['content'], utf8Start: 0, utf8End: 1 }), messages))
.toThrow(/splits a UTF-8 code point/)
})
it('decodes every packed JSON variant and rejects unknown tags', () => {
const messages: DeepSeekLlmApiJson[] = [{ content: 'abcdef' }]
const value: PackedJsonValue = {
kind: 'object',
entries: [[
'items',
{
kind: 'array',
items: [
{ kind: 'literal', value: 1 },
{
kind: 'string',
parts: [
{ kind: 'literal', value: 'x' },
{ kind: 'message-slice', value: { messageIndex: 0, path: ['content'], utf8Start: 1, utf8End: 4 } },
],
},
],
},
]],
}
expect(unpackJsonValue(value, messages)).toEqual({ items: [1, 'xbcd'] })
expect(() => unpackJsonValue({ kind: 'future' } as never, messages)).toThrow(/unknown packed JSON value/)
})
})
@@ -0,0 +1,95 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import * as SessionLogInvariant from '../src/invariant.ts'
import type {} from '../src/types.ts'
const contexts: Context[] = []
afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
})
async function setup(): Promise<Context> {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantRegistry, { enabled: true })
await ctx.plugin(SessionLogInvariant)
return ctx
}
describe('DeepSeek session-log acceptance invariant', () => {
it('accepts a watermark naming an earlier event in its containing Session', async () => {
const ctx = await setup()
const session = ctx.sessions.create(SessionId('valid'))
session.append('turn/start', { turn: 1 })
expect(() => session.append('session-log-deepseek/accepted', { sessionId: session.id, throughSeq: 0 }))
.not.toThrow()
})
it('rejects a live watermark for another Session or a non-earlier sequence', async () => {
const ctx = await setup()
const wrongId = ctx.sessions.create(SessionId('wrong-id'))
wrongId.append('turn/start', { turn: 1 })
expect(() => wrongId.append('session-log-deepseek/accepted', {
sessionId: SessionId('other'),
throughSeq: 0,
})).toThrow(expect.objectContaining<Partial<InvariantError>>({
code: 'INVARIANT',
packageName: '@deepseek-ai/dsh-session-log-deepseek',
}))
const wrongSeq = ctx.sessions.create(SessionId('wrong-seq'))
wrongSeq.append('turn/start', { turn: 1 })
expect(() => wrongSeq.append('session-log-deepseek/accepted', {
sessionId: wrongSeq.id,
throughSeq: 1,
})).toThrow(expect.objectContaining<Partial<InvariantError>>({
code: 'INVARIANT',
packageName: '@deepseek-ai/dsh-session-log-deepseek',
}))
})
it('validates existing history when the invariant loads after the Session', async () => {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantRegistry, { enabled: true })
const id = SessionId('late-invalid')
ctx.sessions.create(id, { seed: [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 1 } },
] })
let failure: unknown
try {
await ctx.plugin(SessionLogInvariant)
} catch (error) {
failure = error
}
expect(failure).toMatchObject<Partial<InvariantError>>({
code: 'INVARIANT',
packageName: '@deepseek-ai/dsh-session-log-deepseek',
})
})
it('allows an inherited parent watermark inside a fork seed', async () => {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantRegistry, { enabled: true })
const parentId = SessionId('fork-parent')
const childId = SessionId('fork-child')
ctx.sessions.create(childId, {
seed: [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: parentId, throughSeq: 0 } },
],
meta: { parentSession: parentId, seedLength: 2 },
})
await expect(ctx.plugin(SessionLogInvariant)).resolves.toBeDefined()
})
})
@@ -0,0 +1,181 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { Session, SessionId, type CreateSessionOptions, type SessionEvent } from '@deepseek-ai/dsh-session'
import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions'
import * as SessionLogDeepSeek from '../src/index.ts'
const contexts: Context[] = []
const SIGNAL = new AbortController().signal
afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
})
async function harness(id: string, seed?: readonly SessionEvent[], meta?: CreateSessionOptions['meta']): Promise<{
ctx: Context
session: Session
disposeUpload: () => Promise<void>
}> {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
const upload = ctx.plugin(SessionLogDeepSeek, { enabled: true })
await upload
const options = seed === undefined
? undefined
: { seed, ...meta === undefined ? {} : { meta } }
const session = ctx.sessions.create(SessionId(id), options)
return { ctx, session, disposeUpload: () => upload.dispose() }
}
function body(text = 'x'.repeat(300)) {
return { messages: [{ role: 'user', content: text }] }
}
describe('incremental DeepSeek session-log upload', () => {
it('does not contribute the session log under its default configuration', async () => {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SessionStore)
await ctx.plugin(DeepSeekLlmApiExtensionRegistry)
await ctx.plugin(SessionLogDeepSeek)
const session = ctx.sessions.create(SessionId('default-off'))
session.append('turn/start', { turn: 1 })
const prepared = await ctx.deepseekLlmApiExtensions.prepare({
body: body(), signal: SIGNAL, sessionId: session.id,
})
expect(prepared.fields).not.toHaveProperty('dsh_session_log')
})
it('uploads the full first prefix, records acceptance, then sends only the appended suffix', async () => {
const { ctx, session } = await harness('incremental')
session.append('turn/start', { turn: 1 })
session.append('step/start', { turn: 1, step: 1 })
const first = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
const firstPayload = first.fields.dsh_session_log
expect(firstPayload).toMatchObject({ afterSeq: -1, throughSeq: 1 })
expect(firstPayload?.events).toHaveLength(2)
await first.accept()
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(1)
session.append('step/end', { turn: 1, step: 1 })
const second = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
expect(second.fields.dsh_session_log).toMatchObject({ afterSeq: 1, throughSeq: 3 })
expect(second.fields.dsh_session_log?.events).toHaveLength(2)
expect(second.fields.dsh_session_log?.events[0]).toMatchObject({
encoding: 'raw',
event: { type: 'session-log-deepseek/accepted', seq: 2 },
})
})
it('reconstructs a persisted cursor and ignores an inherited parent watermark in a fork', async () => {
const first = await harness('parent')
first.session.append('turn/start', { turn: 1 })
const prepared = await first.ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: first.session.id })
await prepared.accept()
const seed = first.session.events
const resumed = await harness('parent', seed)
expect(SessionLogDeepSeek.acceptedThrough(resumed.session)).toBe(0)
const resumedPayload = await resumed.ctx.deepseekLlmApiExtensions.prepare({
body: body(), signal: SIGNAL, sessionId: resumed.session.id,
})
expect(resumedPayload.fields.dsh_session_log?.afterSeq).toBe(0)
const fork = await harness('child', seed, { parentSession: first.session.id, seedLength: seed.length })
expect(SessionLogDeepSeek.acceptedThrough(fork.session)).toBe(-1)
const forkPayload = await fork.ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: fork.session.id })
expect(forkPayload.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: fork.session.seq - 1 })
})
it('takes the maximum watermark when concurrent acceptances settle out of order', async () => {
const { ctx, session } = await harness('concurrent')
session.append('turn/start', { turn: 1 })
const earlier = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
session.append('step/start', { turn: 1, step: 1 })
const later = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
await later.accept()
await earlier.accept()
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(1)
})
it('folds only events appended after the cached acceptance scan', () => {
const id = SessionId('incremental-fold')
const events: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
{ type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 0 } },
]
let reads = 0
const observed = new Proxy(events, {
get(target, property, receiver) {
if (typeof property === 'string' && /^\d+$/.test(property)) reads++
return Reflect.get(target, property, receiver) as unknown
},
})
const session = { id, get events() { return observed } } as unknown as Session
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0)
expect(reads).toBe(2)
reads = 0
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0)
expect(reads).toBe(0)
events.push(
{ type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } },
{ type: 'session-log-deepseek/accepted', seq: 3, time: 4, data: { sessionId: id, throughSeq: 2 } },
)
expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(2)
expect(reads).toBe(2)
})
it('omits the field for direct or stale requests and uploads the prior acceptance marker next', async () => {
const { ctx, session } = await harness('edges')
await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL }))
.resolves.toMatchObject({ fields: {} })
await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: 'missing' }))
.resolves.toMatchObject({ fields: {} })
await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }))
.resolves.toMatchObject({ fields: {} })
session.append('turn/start', { turn: 1 })
const first = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
await first.accept()
const current = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })
expect(current.fields.dsh_session_log).toMatchObject({
afterSeq: 0,
throughSeq: 1,
events: [{ event: { type: 'session-log-deepseek/accepted' } }],
})
})
it('fails preparation when the DeepSeek body has no messages array', async () => {
const { ctx, session } = await harness('bad-body')
session.append('turn/start', { turn: 1 })
await expect(ctx.deepseekLlmApiExtensions.prepare({ body: {}, signal: SIGNAL, sessionId: session.id }))
.rejects.toThrow(/no messages array/)
})
it('fails closed on a malformed persisted acceptance watermark', async () => {
const malformed = [{
type: 'session-log-deepseek/accepted',
seq: 0,
time: 1,
data: { sessionId: 'malformed', throughSeq: 0 },
}] as unknown as SessionEvent[]
const session = Session.create(SessionId('malformed'), malformed)
expect(() => SessionLogDeepSeek.acceptedThrough(session)).toThrow(/malformed acceptance watermark/)
})
it('withdraws its request field when the contributing plugin reloads', async () => {
const { ctx, session, disposeUpload } = await harness('hmr')
session.append('turn/start', { turn: 1 })
expect((await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })).fields)
.toHaveProperty('dsh_session_log')
await disposeUpload()
expect((await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })).fields)
.not.toHaveProperty('dsh_session_log')
})
})
@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../core/session"
},
{
"path": "../../llm/deepseek-llm-api-extensions"
},
{
"path": "../../runtime-diagnostics/invariants"
}
]
}
+31
View File
@@ -589,6 +589,9 @@ importers:
'@deepseek-ai/dsh-session-checkpoint-policy':
specifier: workspace:*
version: link:../packages/session/session-checkpoint-policy
'@deepseek-ai/dsh-session-log-deepseek':
specifier: workspace:*
version: link:../packages/session/session-log-deepseek
'@deepseek-ai/dsh-session-persistence-jsonl':
specifier: workspace:*
version: link:../packages/session/session-persistence-jsonl
@@ -1132,6 +1135,9 @@ importers:
'@deepseek-ai/dsh-session-checkpoint-policy':
specifier: workspace:^
version: link:../../session/session-checkpoint-policy
'@deepseek-ai/dsh-session-log-deepseek':
specifier: workspace:^
version: link:../../session/session-log-deepseek
'@deepseek-ai/dsh-session-persistence-jsonl':
specifier: workspace:^
version: link:../../session/session-persistence-jsonl
@@ -5686,6 +5692,9 @@ importers:
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-session-log-deepseek':
specifier: workspace:^
version: link:../../session/session-log-deepseek
'@deepseek-ai/dsh-settings':
specifier: workspace:^
version: link:../../settings/settings
@@ -6529,6 +6538,25 @@ importers:
specifier: workspace:^
version: link:../../core/tools
packages/session/session-log-deepseek:
dependencies:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-deepseek-llm-api-extensions':
specifier: workspace:^
version: link:../../llm/deepseek-llm-api-extensions
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../runtime-diagnostics/invariants
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
packages/session/session-persistence:
devDependencies:
'@deepseek-ai/cordis':
@@ -9108,6 +9136,9 @@ importers:
'@deepseek-ai/dsh-session-checkpoint-policy':
specifier: workspace:^
version: link:../../packages/session/session-checkpoint-policy
'@deepseek-ai/dsh-session-log-deepseek':
specifier: workspace:^
version: link:../../packages/session/session-log-deepseek
'@deepseek-ai/dsh-session-persistence':
specifier: workspace:^
version: link:../../packages/session/session-persistence
+1
View File
@@ -50,6 +50,7 @@
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^",
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^",
"@deepseek-ai/dsh-session-log-deepseek": "workspace:^",
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
"@deepseek-ai/dsh-llm-retry": "workspace:^",
"@deepseek-ai/dsh-mcp-client": "workspace:^",
@@ -20,6 +20,9 @@
- id: deepseek-llm-api-extensions
name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'
- id: session-log-deepseek
name: '@deepseek-ai/dsh-session-log-deepseek'
- id: plugin-package-inventory-deepseek
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
+1 -1
View File
@@ -120,7 +120,7 @@ const SERVICE_ROLES: ServiceRole[] = [
pkg: 'deepseek-llm-api-extensions',
title: 'Official DeepSeek request extensions',
mode: 'seam',
implementations: ['plugin-package-inventory-deepseek'],
implementations: ['session-log-deepseek', 'plugin-package-inventory-deepseek'],
consumers: ['llm-deepseek'],
note: 'Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance.',
},
+1
View File
@@ -146,6 +146,7 @@
{ "path": "./packages/typert/loader" },
{ "path": "./packages/session/session-persistence" },
{ "path": "./packages/session/session-checkpoint-policy" },
{ "path": "./packages/session/session-log-deepseek" },
{ "path": "./packages/session/session-persistence-jsonl" },
{ "path": "./packages/session/session-persistence-sqlite" },
{ "path": "./packages/session/session-projection" },