@deepseek-ai/dsh-api-session-controller 0.1.7-alpha.1 → 0.1.7-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
5
- README.md: e12ad7623eefc73c94e99cc3078e0ae78d6a969a
6
- README.zh.md: 5f4ceec9a8e8303cad6651dbbe7af27fc07bccec
5
+ README.md: fa2a251e94cfa2855790d7d71457ac4bdf2ff752
6
+ README.zh.md: d1a73ed5843fddadfc2f401fee1a86eece4e5272
package/README.md CHANGED
@@ -43,12 +43,12 @@ Projection reads retain loading and failure state independently of their values.
43
43
 
44
44
  Accepted prompts and observed running keep their Client display conversion across list refresh, reconnect, and Session-object replacement. The Manager retains these observations by Session id; they do not establish durable history. Drafts, projection stores, and sidebar display rules retain their existing behavior. The [blank rollback decision](../../../.agents/notes/implemented/bug-fix/2026-09-15-client-session-blank-reconciliation.md) defines retention and late-response handling.
45
45
 
46
- The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, `append`, and `settle-assistant` changes, and repairs reconnect or sequence gaps through a tail page. Backwards paging has two verbs: `loadOlder()` pulls one 50-message page, and `loadThrough(seq)` — the turn-jump loader — loops 200-message pages until the window covers the target seq, lowering a shared target on repeated calls, stopping on a page that makes no progress, and reporting busy through the same `loadingOlder` snapshot bit. The Web adapter explicitly opts into cursorless Assistant frames: each opening carries the active attempt's `startedAfterSeq`, `nextIndex`, and compact stream, and every stream member becomes a Client-only `assistant/live-chunk` entry ordered between durable cursors. The Host captures a follower-local arrival ordinal with that baseline and suppresses buffered frames at or before the cut; a replacement Agent may restart frame revision at one. A durable `assistant/message` or `assistant/attempt` arriving after an active opening stays staged only when its seq follows `startedAfterSeq` and its Turn and Step match; the matching end type, seq, and index publishes one named settlement delta that retires the attempt's transient rows and adds the durable entry while earlier same-step retries remain visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow, while a controller that missed the start ignores unknown-attempt frames and publishes their durable settlement normally. An abandoned end publishes a settlement delta without a durable entry so its transient rows retire immediately. A durable gap-repair page has no Assistant baseline, so its held notification reopens follow once for a paired page and baseline. Every history record covers exactly its event seq. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces projection state instead of treating transient values as durable events. At each ready Host generation, a synchronous Client subscription clears retained projection values and watermarks before refreshing queries and restarting the control stream, including Sessions absent from the control baseline. The first control stream waits for generation readiness, so its opening values cannot precede invalidation. Outstanding list responses from the previous generation cannot republish those values. Within a generation, a delayed control baseline cannot overwrite or clear newer sequenced values, whether they arrived through a live Session's list block, a history page, or a frame; a cached list block, viewed from the projection cache for a cold Session, yields to the connected Session's baseline whatever watermark it carries. The durable `inbox` projection carries both pending lists through the same cold-read and reconnect path as other projections. Client Agent contexts provide the identity used by the independent [`fileUpload`](../../client/file-upload/README.md) service; Session objects expose lifecycle, prompt, queue, and history operations rather than file transfer.
46
+ The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, `append`, and `settle-assistant` changes, and repairs reconnect or sequence gaps through a tail page. Backwards paging has two verbs: `loadOlder()` pulls one Turn-aligned page, and `loadThrough(seq)` — the turn-jump loader — loops Turn-aligned pages until the window covers the target seq, lowering a shared target on repeated calls, stopping on a page that makes no progress, and reporting busy through the same `loadingOlder` snapshot bit. The Web adapter explicitly opts into cursorless Assistant frames: each opening carries the active attempt's `startedAfterSeq`, `nextIndex`, and compact stream, and every stream member becomes a Client-only `assistant/live-chunk` entry ordered between durable cursors. The Host captures a follower-local arrival ordinal with that baseline and suppresses buffered frames at or before the cut; a replacement Agent may restart frame revision at one. A durable `assistant/message` or `assistant/attempt` arriving after an active opening stays staged only when its seq follows `startedAfterSeq` and its Turn and Step match; the matching end type, seq, and index publishes one named settlement delta that retires the attempt's transient rows and adds the durable entry while earlier same-step retries remain visible. Revision, dense-index, or settlement gaps for a known attempt reopen follow, while a controller that missed the start ignores unknown-attempt frames and publishes their durable settlement normally. An abandoned end publishes a settlement delta without a durable entry so its transient rows retire immediately. A durable gap-repair page has no Assistant baseline, so its held notification reopens follow once for a paired page and baseline. Every history record covers exactly its event seq. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces projection state instead of treating transient values as durable events. At each ready Host generation, a synchronous Client subscription clears retained projection values and watermarks before refreshing queries and restarting the control stream, including Sessions absent from the control baseline. The first control stream waits for generation readiness, so its opening values cannot precede invalidation. Outstanding list responses from the previous generation cannot republish those values. Within a generation, a delayed control baseline cannot overwrite or clear newer sequenced values, whether they arrived through a live Session's list block, a history page, or a frame; a cached list block, viewed from the projection cache for a cold Session, yields to the connected Session's baseline whatever watermark it carries. The durable `inbox` projection carries both pending lists through the same cold-read and reconnect path as other projections. Client Agent contexts provide the identity used by the independent [`fileUpload`](../../client/file-upload/README.md) service; Session objects expose lifecycle, prompt, queue, and history operations rather than file transfer.
47
47
 
48
- The Session object also carries local submission echoes: `session.beginSubmission` inserts one into `SessionSnapshot.pendingSubmissions` synchronously, before the caller serializes and prompts, so a conversation UI can show the message on the submit click's own frame. The echo stores ordered image previews and durable file references. Session derives its `transcript`, `queued`, or `steering` placement from the current running state and requested delivery mode, then retains that placement while serialization is in flight. The prompt's `requestId` is the correlation identity: the Host echoes it as the durable user source's `rpcId`, including pending messages in the `inbox` projection. An echo retires one animation frame after its durable event or queue occurrence is observed, immediately when its identified prompt fails or is abandoned, and as failed on disposal. Each retirement fires `onRetire` exactly once; an observed retirement includes the ordered durable attachment references so the composer can release successful cards while preserving failed drafts. Echoes are Client memory only; reload and reconnect rebuild the conversation from durable events alone.
48
+ The Session object also carries local submission echoes: `session.beginSubmission` inserts one into `SessionSnapshot.pendingSubmissions` synchronously, before the caller serializes and prompts, so a conversation UI can show the message on the submit click's own frame. The echo stores ordered image previews and durable file references. Session derives its `transcript`, `queued`, or `steering` placement from the current running state and requested delivery mode, then retains that placement until the display handoff. The prompt's `requestId` is the correlation identity: the Host echoes it as the durable user source's `rpcId`, including pending messages in the `inbox` projection. Queued echoes retire one animation frame after queue acceptance; Chat echoes follow the admission rule below. Identified failures and abandonment retire unadmitted echoes immediately. Disposal preserves observed admission and fails other unsettled submissions. Each retirement fires `onRetire` exactly once; an observed retirement includes the ordered durable attachment references so the composer can release successful cards while preserving failed drafts. Echoes are Client memory only; reload and reconnect rebuild the conversation from durable events alone.
49
49
 
50
50
 
51
- During uninterrupted following, steering echoes stay mounted through Inbox acceptance and claim until their durable messages arrive. A replacement follow baseline withdraws earlier receipt-confirmed steering echoes as observed, using their accepted attachment references; unconfirmed submissions remain pending. Host data then supplies the pending or admitted rows. Reconnecting between claim and admission can briefly omit that bubble; withdrawal confirms acceptance, not execution or failure.
51
+ During uninterrupted following, transcript and steering echoes stay in Chat through Inbox acceptance and claim until their durable messages arrive. After a local Chat message is admitted, its identity remains until the Inbox watermark reaches its next-turn or next-step claim seq: Chat already renders the durable Node, while Chat and QueueDock exclude matching stale Inbox rows. Other queued messages remain visible. A replacement follow baseline withdraws receipt-confirmed echoes as observed, using their accepted attachment references; unconfirmed submissions remain pending. Host data then supplies the pending or admitted rows. Reconnecting between claim and admission can briefly omit that bubble; withdrawal confirms acceptance, not execution or failure.
52
52
 
53
53
  The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent.
54
54
 
@@ -56,7 +56,9 @@ Fork copies the exact inclusive event prefix selected by `atSeq`, including a cu
56
56
 
57
57
  A resume blocked by an existing write handle returns `session/writer-held` with the Session id; other resume failures retain `gateway/internal`.
58
58
 
59
- `loadThrough(seq)` retains older pages privately until its shared target is covered or loading ends, then publishes the successful pages as one ordered prepend. Live events remain visible while history loads. A later page failure retains the successful prefix; replacing the history window discards buffered pages from the replaced window. Ordinary `loadOlder()` still publishes its single page directly.
59
+ `loadThrough(seq)` retains older pages privately until its shared target is covered or loading ends, then publishes the successful pages as one ordered prepend. Live events remain visible while history loads. A later page failure retains the successful prefix; replacing the history window discards buffered pages from the replaced window. Ordinary `loadOlder()` publishes one Host-selected page directly.
60
+
61
+ The Client's initial `follow`, reconnect openings, and `loadOlder()` request at least 50 append-origin `user/message` and `assistant/message` events and cross at least two `turn/start` events, including the partial Turn at the loaded window's beginning. The Host stops at the first Turn start satisfying both minima, or at 500 counted messages or history exhaustion, whichever comes first. Steering does not add a Turn boundary. Intervening events accompany the page without counting toward the message budget. The optional `turnWindow` on page and follow requests supplies these minima under `maxMessages`. `loadThrough()` uses the same rules with a 200-message minimum per page; the 500-message cap does not limit the whole jump. Requests without `turnWindow` retain message-aligned paging.
60
62
 
61
63
  Queue edits replace pending content with non-empty text only.
62
64
 
package/README.zh.md CHANGED
@@ -43,12 +43,12 @@ Client 列表刷新保留未变化的行对象,并在顺序和值均相同时
43
43
 
44
44
  已受理的提示词与已观察到的运行,会使客户端展示转换跨列表刷新、重连及 Session 对象替换保留。Manager 按 Session id 保留这些观察;它们不证明持久历史已经产生。草稿、投影存储和侧边栏展示规则保留原有行为。[blank 回退修复决策](../../../.agents/notes/implemented/bug-fix/2026-09-15-client-session-blank-reconciliation.zh.md) 定义保留期限及迟到响应处理。
45
45
 
46
- Client 适配器提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页 50 条消息,而 `loadThrough(seq)`——轮次跳转加载器——按每页 200 条消息循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web 适配器显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且轮次与步骤匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的事件 seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 projection 状态,而不会把瞬态值当作 durable event。每次 Host generation 就绪时,同步的 Client 订阅会先清除保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括 control baseline 中没有列出的 Session。首次 control stream 会等待 generation 就绪,确保其 opening 值不会先于旧状态清理到达。上一代尚未完成的 list 响应无法重新发布这些值。同一 generation 内,延迟到达的 control baseline 不能覆盖或清除较新的 sequenced 值,无论它们来自活 Session 的 list block、history page 还是 frame;冷 Session 从 projection cache 看出来的 cached list block 则不论水位都让位于建连 Session 的 baseline。持久 `inbox` 投影通过与其他投影相同的冷读取和重连路径传输两份待处理列表。Client Agent 上下文提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。
46
+ Client 适配器提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend`、`append` 与 `settle-assistant` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页按轮次开头对齐的历史,而 `loadThrough(seq)`——轮次跳转加载器——按轮次开头对齐页面循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。Web 适配器显式选择接收无 cursor 的 Assistant frame:每个 opening 携带活跃 attempt 的 `startedAfterSeq`、`nextIndex` 与紧凑 stream,每个 stream member 都成为排在持久 cursor 之间的 Client-only `assistant/live-chunk` 条目。Host 会随该 baseline 捕获 follower 本地到达序号,并抑制该 cut 及之前的 buffered frame;replacement Agent 可以从 revision 一重新开始。活跃 opening 之后到达的持久 `assistant/message` 或 `assistant/attempt` 只有在其 seq 晚于 `startedAfterSeq` 且轮次与步骤匹配时才会保持暂存;匹配的 end type、seq 与 index 会发布一个具名 settlement delta,删除该 attempt 的瞬态 row、加入持久条目,并保留同一步骤中更早的 retry。已知 attempt 的 revision、密集 index 或 settlement 缺口会重新打开 follow;若 controller 错过 start,则忽略 unknown-attempt frame,并正常发布其持久 settlement。Abandoned end 会发布不含持久条目的 settlement delta,使瞬态 row 立即退出。持久缺口修复 page 不携带 Assistant baseline,因此 held notification 会重新打开 follow 一次,以取得配对的 page 与 baseline。每条历史 record 只覆盖自身的事件 seq。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 projection 状态,而不会把瞬态值当作 durable event。每次 Host generation 就绪时,同步的 Client 订阅会先清除保留的投影值及其水位,再刷新查询并重新打开 control stream,其中也包括 control baseline 中没有列出的 Session。首次 control stream 会等待 generation 就绪,确保其 opening 值不会先于旧状态清理到达。上一代尚未完成的 list 响应无法重新发布这些值。同一 generation 内,延迟到达的 control baseline 不能覆盖或清除较新的 sequenced 值,无论它们来自活 Session 的 list block、history page 还是 frame;冷 Session 从 projection cache 看出来的 cached list block 则不论水位都让位于建连 Session 的 baseline。持久 `inbox` 投影通过与其他投影相同的冷读取和重连路径传输两份待处理列表。Client Agent 上下文提供独立 [`fileUpload`](../../client/file-upload/README.zh.md) 服务使用的身份;Session 对象提供生命周期、prompt、queue 与历史操作,不提供文件传输。
47
47
 
48
- Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与提示词之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering` 位置,并在序列化期间保留该位置。提示词的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,`inbox` 投影中的待处理消息也保留同一 source。回显在观察到其 durable event 或 queue occurrence 后延迟一个动画帧退休,带标识的提示词失败或被放弃时立即退休,销毁时按 failed 退休。每次退休恰好触发一次 `onRetire`;observed 退休还会携带有序的持久附件引用,让 composer 释放成功卡片并保留失败草稿。回显只存在于 Client 内存;刷新与重连只从持久事件重建会话。
48
+ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与提示词之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering` 位置,并保留该位置直到展示接管。提示词的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,`inbox` 投影中的待处理消息也保留同一 source。排队回显在队列接受后延迟一个动画帧退休;Chat 回显遵循下述入档规则。尚未入档的回显在带标识的提示词失败或被放弃时立即退休。销毁时保留已观察到的入档结果,其余未结算提交按 failed 退休。每次退休恰好触发一次 `onRetire`;observed 退休还会携带有序的持久附件引用,让 composer 释放成功卡片并保留失败草稿。回显只存在于 Client 内存;刷新与重连只从持久事件重建会话。
49
49
 
50
50
 
51
- 正常在线时,steering 回显在 Inbox 接受与领取期间保持挂载,直到持久消息到达。新的 follow 基线会将此前已有接收回执的 steering 回显按 observed 撤去,使用已接受的附件引用完成结算;尚未确认接收的提交继续保留。待处理或已入档的消息随后由 Host 数据呈现。在领取与入档之间重连时,气泡可能短暂消失;撤去回显只确认接收,不代表执行或失败。
51
+ 正常在线时,transcript 与 steering 回显在 Inbox 接受与领取期间留在 Chat,直到持久消息到达。本地 Chat 消息入档后,其身份保留到 Inbox 水位覆盖 next-turn 或 next-step 领取序号:Chat 已显示真实 Node,而 Chat 与 QueueDock 排除匹配的旧 Inbox 行。其他排队消息照常显示。新的 follow 基线会将已有接收回执的回显按 observed 撤去,使用已接受的附件引用完成结算;尚未确认接收的提交继续保留。待处理或已入档的消息随后由 Host 数据呈现。在领取与入档之间重连时,气泡可能短暂消失;撤去回显只确认接收,不代表执行或失败。
52
52
 
53
53
  面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。
54
54
 
@@ -56,7 +56,9 @@ Fork 复制 `atSeq` 所选的精确事件前缀,包含切点事件,允许在
56
56
 
57
57
  恢复会话时若已有写句柄占用,返回 `session/writer-held`,并携带会话 id;其他恢复失败仍返回 `gateway/internal`。
58
58
 
59
- `loadThrough(seq)` 在共享目标被覆盖或加载结束前私下保留较早页面,随后把成功取得的页面按顺序作为一次前插发布。历史加载期间实时事件仍然可见。后续页面失败时保留已成功取得的部分;历史窗口被替换时丢弃被替换窗口的暂存页面。普通 `loadOlder()` 仍直接发布其单页结果。
59
+ `loadThrough(seq)` 在共享目标被覆盖或加载结束前私下保留较早页面,随后把成功取得的页面按顺序作为一次前插发布。历史加载期间实时事件仍然可见。后续页面失败时保留已成功取得的部分;历史窗口被替换时丢弃被替换窗口的暂存页面。普通 `loadOlder()` 直接发布 Host 选取的一页结果。
60
+
61
+ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以 append 方式追加的 `user/message` 和 `assistant/message` 事件,并向前跨过至少两个 `turn/start`,其中包含已加载窗口开头尚未补齐的轮次。Host 在首次同时满足两个下限的轮次开头停止;若先达到 500 条计数消息或历史已耗尽,则立即返回。Steering 不增加轮次分界。区间内的其他事件随页返回,但不计入消息预算。分页和 follow 请求可选的 `turnWindow` 在 `maxMessages` 上限内指定这两个下限。`loadThrough()` 复用相同规则,仅将每页消息数下限设为 200;500 条上限不限制整次跳转。不带 `turnWindow` 的请求保留按消息对齐的分页方式。
60
62
 
61
63
  队列编辑仅允许用非空文本替换待处理内容。
62
64
 
package/lib/client.js CHANGED
@@ -428,7 +428,7 @@ window.__ModuleLoader__.load({
428
428
  for await (const frame of this.remote.session.follow({
429
429
  address: this.address,
430
430
  assistantStream: true,
431
- ...request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages }
431
+ ...this.repairRequest(request)
432
432
  }, signal)) {
433
433
  if (frame.type === "snapshot") {
434
434
  for (const record of frame.records) assertSessionWireEvent(record.event);
@@ -476,7 +476,10 @@ window.__ModuleLoader__.load({
476
476
  }
477
477
  /** @inheritdoc */
478
478
  repairRequest(request) {
479
- return request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages };
479
+ return {
480
+ ...request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages },
481
+ ...request.turnWindow === void 0 ? {} : { turnWindow: request.turnWindow }
482
+ };
480
483
  }
481
484
  };
482
485
  //#endregion
@@ -949,6 +952,15 @@ window.__ModuleLoader__.load({
949
952
  return this.rows.get(key)?.value;
950
953
  }
951
954
  /**
955
+ * Read the accepted Host watermark without subscribing or copying a value.
956
+ * @param key - projection key.
957
+ * @returns the current sequence, or undefined for absent and cached values.
958
+ */
959
+ seqOf(key) {
960
+ const row = this.rows.get(key);
961
+ return row?.kind === "sequenced" ? row.seq : void 0;
962
+ }
963
+ /**
952
964
  * Read every current projection value as one reference-stable snapshot.
953
965
  * @returns The same frozen value map until a row changes.
954
966
  */
@@ -1516,6 +1528,20 @@ window.__ModuleLoader__.load({
1516
1528
  asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq)
1517
1529
  };
1518
1530
  }
1531
+ const HISTORY_PAGE_OPTIONS = {
1532
+ maxMessages: 500,
1533
+ turnWindow: {
1534
+ minMessages: 50,
1535
+ minTurns: 2
1536
+ }
1537
+ };
1538
+ const JUMP_PAGE_OPTIONS = {
1539
+ ...HISTORY_PAGE_OPTIONS,
1540
+ turnWindow: {
1541
+ ...HISTORY_PAGE_OPTIONS.turnWindow,
1542
+ minMessages: 200
1543
+ }
1544
+ };
1519
1545
  /**
1520
1546
  * Owns a session's event window, lifecycle state, and observable
1521
1547
  * snapshot. React bindings remain outside this data layer. Features see only
@@ -1816,7 +1842,7 @@ window.__ModuleLoader__.load({
1816
1842
  this.openPromise = promise;
1817
1843
  return promise;
1818
1844
  }
1819
- /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
1845
+ /** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
1820
1846
  async loadOlder() {
1821
1847
  if (this.openState !== "open" || !this.hasMore || this.loadingOlder) return;
1822
1848
  const events = this.events;
@@ -1826,7 +1852,7 @@ window.__ModuleLoader__.load({
1826
1852
  try {
1827
1853
  await events.prepend({
1828
1854
  beforeSeq: this.baseSeq,
1829
- maxMessages: 50
1855
+ ...HISTORY_PAGE_OPTIONS
1830
1856
  });
1831
1857
  } catch (error) {
1832
1858
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadOlder failed:", error);
@@ -1862,7 +1888,7 @@ window.__ModuleLoader__.load({
1862
1888
  const before = pending.beforeSeq;
1863
1889
  await events.prepend({
1864
1890
  beforeSeq: before,
1865
- maxMessages: 200
1891
+ ...JUMP_PAGE_OPTIONS
1866
1892
  });
1867
1893
  if (pending.beforeSeq >= before) return;
1868
1894
  }
@@ -1981,7 +2007,8 @@ window.__ModuleLoader__.load({
1981
2007
  */
1982
2008
  async dispose() {
1983
2009
  this.stopObservingInbox();
1984
- for (const requestId of [...this.submissionSettlements.keys()]) this.retireFailedSubmission(requestId);
2010
+ for (const [requestId, settlement] of [...this.submissionSettlements]) if (settlement.admitted !== void 0) this.scheduleObservedRetirement(requestId, settlement.admitted);
2011
+ else this.retireFailedSubmission(requestId);
1985
2012
  this.openGeneration++;
1986
2013
  const events = this.events;
1987
2014
  this.events = void 0;
@@ -2003,7 +2030,7 @@ window.__ModuleLoader__.load({
2003
2030
  });
2004
2031
  this.events = events;
2005
2032
  try {
2006
- await events.open({ maxMessages: 50 });
2033
+ await events.open(HISTORY_PAGE_OPTIONS);
2007
2034
  if (generation !== this.openGeneration || this.events !== events) return;
2008
2035
  this.openState = "open";
2009
2036
  } catch (error) {
@@ -2050,7 +2077,7 @@ window.__ModuleLoader__.load({
2050
2077
  for (const entry of visible) this.observeSubmissionEvent(entry.event);
2051
2078
  if (projections !== void 0) {
2052
2079
  const inbox = projections.values.inbox;
2053
- this.observeSteeringInsertions(inbox?.["next-step"] ?? [], 0, projections.asOfSeq);
2080
+ for (const target of ["next-turn", "next-step"]) this.observeSubmissionInsertions(target, inbox?.[target] ?? [], 0, projections.asOfSeq);
2054
2081
  }
2055
2082
  this.notifier.markDirty();
2056
2083
  }
@@ -2106,36 +2133,35 @@ window.__ModuleLoader__.load({
2106
2133
  if (this.submissionSettlements.size === 0) return;
2107
2134
  if (event.type === "agent/inbox/spliced") {
2108
2135
  const { target, start, removedCount = 0, inserted, outcome } = event.data;
2109
- if (target === "next-step") {
2110
- for (const [requestId, settlement] of this.submissionSettlements) {
2111
- const receipt = settlement.receipt;
2112
- if (receipt === void 0 || receipt.index === null || receipt.seq >= event.seq) continue;
2113
- const removed = receipt.index >= start && receipt.index < start + removedCount;
2114
- if (removed && outcome === "canceled") this.retireFailedSubmission(requestId);
2115
- else settlement.receipt = {
2116
- ...receipt,
2117
- seq: event.seq,
2118
- index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount
2119
- };
2120
- }
2121
- this.observeSteeringInsertions(inserted, start, event.seq);
2136
+ for (const [requestId, settlement] of this.submissionSettlements) {
2137
+ const receipt = settlement.receipt;
2138
+ if (receipt?.target !== target || receipt.index === null || receipt.seq >= event.seq) continue;
2139
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
2140
+ if (removed && outcome === "canceled") this.retireFailedSubmission(requestId);
2141
+ else settlement.receipt = {
2142
+ ...receipt,
2143
+ seq: event.seq,
2144
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount
2145
+ };
2122
2146
  }
2147
+ this.observeSubmissionInsertions(target, inserted, start, event.seq);
2123
2148
  for (const message of inserted) this.observeSubmissionMessage(message, false);
2124
2149
  return;
2125
2150
  }
2126
2151
  if (event.type === "request/context" || event.type === "turn/end") {
2127
- for (const [requestId, settlement] of this.submissionSettlements) if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq) this.retireFailedSubmission(requestId);
2152
+ for (const [requestId, settlement] of this.submissionSettlements) if (settlement.admitted === void 0 && settlement.receipt?.index === null && settlement.receipt.seq < event.seq) this.retireFailedSubmission(requestId);
2128
2153
  return;
2129
2154
  }
2130
2155
  if (event.type === "user/message") this.observeSubmissionMessage(event.data, true);
2131
2156
  }
2132
- observeSteeringInsertions(messages, start, seq) {
2157
+ observeSubmissionInsertions(target, messages, start, seq) {
2133
2158
  for (const [index, message] of messages.entries()) {
2134
2159
  const source = message.source;
2135
2160
  if (source.kind !== "user" || !("rpcId" in source)) continue;
2136
2161
  const settlement = this.submissionSettlements.get(source.rpcId);
2137
- if (settlement?.placement !== "steering" || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue;
2162
+ if (settlement === void 0 || settlement.placement === "queued" || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue;
2138
2163
  settlement.receipt = {
2164
+ target,
2139
2165
  seq,
2140
2166
  index: start + index,
2141
2167
  attachments: attachmentRefsIn(message.content)
@@ -2145,15 +2171,34 @@ window.__ModuleLoader__.load({
2145
2171
  observeSubmissionMessage(message, admitted) {
2146
2172
  const source = message.source;
2147
2173
  if (source.kind !== "user" || !("rpcId" in source)) return;
2148
- if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === "steering") return;
2149
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
2174
+ const settlement = this.submissionSettlements.get(source.rpcId);
2175
+ if (settlement === void 0 || settlement.retiring) return;
2176
+ if (!admitted) {
2177
+ if (settlement.placement === "queued") this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
2178
+ return;
2179
+ }
2180
+ settlement.admitted = attachmentRefsIn(message.content);
2181
+ this.retireAdmittedSubmission(source.rpcId);
2182
+ }
2183
+ /** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
2184
+ retireAdmittedSubmission(requestId) {
2185
+ const settlement = this.submissionSettlements.get(requestId);
2186
+ if (settlement?.admitted === void 0) return;
2187
+ const receipt = settlement.receipt;
2188
+ if (receipt?.index === null && (this.projections.seqOf("inbox") ?? -1) < receipt.seq) return;
2189
+ this.scheduleObservedRetirement(requestId, settlement.admitted);
2150
2190
  }
2151
- /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
2191
+ /** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
2152
2192
  observeSubmissionInbox() {
2153
2193
  if (this.submissionSettlements.size === 0) return;
2154
2194
  const inbox = this.projections.get("inbox");
2155
2195
  if (inbox === void 0) return;
2156
- for (const message of [...inbox["next-turn"], ...inbox["next-step"]]) this.observeSubmissionMessage(message, false);
2196
+ const seq = this.projections.seqOf("inbox");
2197
+ for (const target of ["next-turn", "next-step"]) {
2198
+ if (seq !== void 0) this.observeSubmissionInsertions(target, inbox[target], 0, seq);
2199
+ for (const message of inbox[target]) this.observeSubmissionMessage(message, false);
2200
+ }
2201
+ for (const requestId of this.submissionSettlements.keys()) this.retireAdmittedSubmission(requestId);
2157
2202
  }
2158
2203
  /**
2159
2204
  * Latch one observed settlement and remove the echo an animation frame
@@ -2175,7 +2220,7 @@ window.__ModuleLoader__.load({
2175
2220
  /** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
2176
2221
  retireFailedSubmission(requestId) {
2177
2222
  const settlement = this.submissionSettlements.get(requestId);
2178
- if (settlement === void 0 || settlement.retiring) return;
2223
+ if (settlement === void 0 || settlement.retiring || settlement.admitted !== void 0) return;
2179
2224
  settlement.retiring = true;
2180
2225
  this.finishSubmission(requestId, { reason: "failed" });
2181
2226
  }
package/lib/index.js CHANGED
@@ -1329,7 +1329,7 @@ var SessionHistoryController = class {
1329
1329
  if (throughSeq > sourceCursor) throw new RemoteError("gateway/bad-request", `session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`, {});
1330
1330
  /* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
1331
1331
  if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) throw new RemoteError("gateway/internal", `session log does not contain through seq ${String(throughSeq)}`, {});
1332
- const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
1332
+ const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq, request.turnWindow);
1333
1333
  return {
1334
1334
  records: pageRecords(page.events),
1335
1335
  hasMore: page.hasMore
@@ -1348,7 +1348,7 @@ var SessionHistoryController = class {
1348
1348
  * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
1349
1349
  */
1350
1350
  async *follow(request, signal) {
1351
- validateFollowRequest(request);
1351
+ validateHistoryWindow(request);
1352
1352
  const { address } = request;
1353
1353
  const target = addressId(address);
1354
1354
  const buffered = new Deque();
@@ -1408,7 +1408,7 @@ var SessionHistoryController = class {
1408
1408
  signal.throwIfAborted();
1409
1409
  const cursor = source.cursor;
1410
1410
  snapshotCursor = cursor;
1411
- const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
1411
+ const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES, cursor, request.turnWindow);
1412
1412
  const assistantStream = request.assistantStream === true ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 } : void 0;
1413
1413
  const assistantStreamOrdinalCut = assistantStreamOrdinal;
1414
1414
  yield {
@@ -1515,10 +1515,15 @@ function projectionBlock(snapshot) {
1515
1515
  function validatePageRequest(request) {
1516
1516
  if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1 || Object.is(request.throughSeq, -0)) throw new RemoteError("gateway/bad-request", "throughSeq must be an integer greater than or equal to -1", {});
1517
1517
  if (request.beforeSeq !== void 0 && (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0 || Object.is(request.beforeSeq, -0))) throw new RemoteError("gateway/bad-request", "beforeSeq must be a non-negative safe integer", {});
1518
- if (request.maxMessages !== void 0 && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) throw new RemoteError("gateway/bad-request", "maxMessages must be a positive safe integer", {});
1518
+ validateHistoryWindow(request);
1519
1519
  }
1520
- function validateFollowRequest(request) {
1520
+ function validateHistoryWindow(request) {
1521
1521
  if (request.maxMessages !== void 0 && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) throw new RemoteError("gateway/bad-request", "maxMessages must be a positive safe integer", {});
1522
+ const window = request.turnWindow;
1523
+ if (window !== void 0) {
1524
+ if (!Number.isSafeInteger(window.minMessages) || window.minMessages <= 0 || window.minMessages > (request.maxMessages ?? DEFAULT_MAX_MESSAGES)) throw new RemoteError("gateway/bad-request", "turnWindow.minMessages must be a positive safe integer no greater than maxMessages", {});
1525
+ if (!Number.isSafeInteger(window.minTurns) || window.minTurns <= 0) throw new RemoteError("gateway/bad-request", "turnWindow.minTurns must be a positive safe integer", {});
1526
+ }
1522
1527
  }
1523
1528
  function addressId(address) {
1524
1529
  return address.kind === "session" ? address.sessionId : address.childSessionId;
@@ -1549,12 +1554,20 @@ function rejectNotFound(address) {
1549
1554
  childSessionId: address.childSessionId
1550
1555
  });
1551
1556
  }
1552
- function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
1557
+ function paginate(events, beforeSeq, maxMessages, throughSeq, turnWindow) {
1553
1558
  const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
1554
1559
  let count = 0;
1560
+ let turns = 0;
1555
1561
  let cut = SessionLogOffset(0);
1556
1562
  for (let index = end - 1; index >= 0; index--) {
1557
1563
  const event = events[index];
1564
+ if (turnWindow !== void 0 && event.type === "turn/start") {
1565
+ turns++;
1566
+ if (count >= turnWindow.minMessages && turns >= turnWindow.minTurns) {
1567
+ cut = SessionLogOffset(index);
1568
+ break;
1569
+ }
1570
+ }
1558
1571
  if (!MESSAGE_TYPES$1.has(event.type) || !isAppendSurfaceEvent(event)) continue;
1559
1572
  count++;
1560
1573
  const sources = event.sourceEventSeqs;
@@ -181,8 +181,12 @@ const _deepseek_ai_dsh_api_session_controller_session_follow_parameter_0$schema
181
181
  'childSessionId': z.intersection(z.string(), z.unknown()).readonly(),
182
182
  'mode': z.union([z.literal("one-shot"), z.literal("continuable"), z.literal("unknown")]).readonly(),
183
183
  })]).readonly(),
184
- 'maxMessages': z.number().readonly().optional(),
185
184
  'assistantStream': z.literal(true).readonly().optional(),
185
+ 'maxMessages': z.number().readonly().optional(),
186
+ 'turnWindow': z.object({
187
+ 'minMessages': z.number().readonly(),
188
+ 'minTurns': z.number().readonly(),
189
+ }).readonly().optional(),
186
190
  }))
187
191
  let _deepseek_ai_dsh_api_session_controller_session_follow_result$schema$value
188
192
  const _deepseek_ai_dsh_api_session_controller_session_follow_result$schema = () => (_deepseek_ai_dsh_api_session_controller_session_follow_result$schema$value ??= z.union([z.object({
@@ -534,6 +538,10 @@ const _deepseek_ai_dsh_api_session_controller_session_page_parameter_0$schema =
534
538
  'throughSeq': z.number().readonly(),
535
539
  'beforeSeq': z.number().readonly().optional(),
536
540
  'maxMessages': z.number().readonly().optional(),
541
+ 'turnWindow': z.object({
542
+ 'minMessages': z.number().readonly(),
543
+ 'minTurns': z.number().readonly(),
544
+ }).readonly().optional(),
537
545
  }))
538
546
  let _deepseek_ai_dsh_api_session_controller_session_page_result$schema$value
539
547
  const _deepseek_ai_dsh_api_session_controller_session_page_result$schema = () => (_deepseek_ai_dsh_api_session_controller_session_page_result$schema$value ??= z.object({
@@ -1914,7 +1922,7 @@ export const TYPERT = {
1914
1922
  },
1915
1923
  {
1916
1924
  "name": "SessionFollowRequest",
1917
- "declaration": "export interface SessionFollowRequest {\n readonly address: SessionAddress;\n readonly maxMessages?: number;\n readonly assistantStream?: true;\n}"
1925
+ "declaration": "export interface SessionFollowRequest extends Pick<SessionPageRequest, 'maxMessages' | 'turnWindow'> {\n readonly address: SessionAddress;\n readonly assistantStream?: true;\n}"
1918
1926
  },
1919
1927
  {
1920
1928
  "name": "SessionForkRequest",
@@ -1970,7 +1978,7 @@ export const TYPERT = {
1970
1978
  },
1971
1979
  {
1972
1980
  "name": "SessionPageRequest",
1973
- "declaration": "export interface SessionPageRequest {\n readonly address: SessionAddress;\n readonly throughSeq: number;\n readonly beforeSeq?: number;\n readonly maxMessages?: number;\n}"
1981
+ "declaration": "export interface SessionPageRequest {\n readonly address: SessionAddress;\n readonly throughSeq: number;\n readonly beforeSeq?: number;\n readonly maxMessages?: number;\n readonly turnWindow?: { readonly minMessages: number; readonly minTurns: number; };\n}"
1974
1982
  },
1975
1983
  {
1976
1984
  "name": "SessionProjectionBaseline",
@@ -181,8 +181,12 @@ const _deepseek_ai_dsh_api_session_controller_session_follow_parameter_0$schema
181
181
  'childSessionId': z.intersection(z.string(), z.unknown()).readonly(),
182
182
  'mode': z.union([z.literal("one-shot"), z.literal("continuable"), z.literal("unknown")]).readonly(),
183
183
  })]).readonly(),
184
- 'maxMessages': z.number().readonly().optional(),
185
184
  'assistantStream': z.literal(true).readonly().optional(),
185
+ 'maxMessages': z.number().readonly().optional(),
186
+ 'turnWindow': z.object({
187
+ 'minMessages': z.number().readonly(),
188
+ 'minTurns': z.number().readonly(),
189
+ }).readonly().optional(),
186
190
  }))
187
191
  let _deepseek_ai_dsh_api_session_controller_session_follow_result$schema$value
188
192
  const _deepseek_ai_dsh_api_session_controller_session_follow_result$schema = () => (_deepseek_ai_dsh_api_session_controller_session_follow_result$schema$value ??= z.union([z.object({
@@ -534,6 +538,10 @@ const _deepseek_ai_dsh_api_session_controller_session_page_parameter_0$schema =
534
538
  'throughSeq': z.number().readonly(),
535
539
  'beforeSeq': z.number().readonly().optional(),
536
540
  'maxMessages': z.number().readonly().optional(),
541
+ 'turnWindow': z.object({
542
+ 'minMessages': z.number().readonly(),
543
+ 'minTurns': z.number().readonly(),
544
+ }).readonly().optional(),
537
545
  }))
538
546
  let _deepseek_ai_dsh_api_session_controller_session_page_result$schema$value
539
547
  const _deepseek_ai_dsh_api_session_controller_session_page_result$schema = () => (_deepseek_ai_dsh_api_session_controller_session_page_result$schema$value ??= z.object({
@@ -63,9 +63,10 @@ export interface ISession {
63
63
  /**
64
64
  * Register one local submission echo in `snapshot.pendingSubmissions`,
65
65
  * synchronously, before the caller serializes and sends the prompt. The
66
- * echo retires when a durable `user/message` event or queue occurrence
67
- * carrying the returned identity arrives, or when the identified prompt
68
- * call fails.
66
+ * Chat echoes persist until durable admission; transcript identities also
67
+ * wait for the Inbox claim watermark so stale queue rows stay suppressed.
68
+ * Queued echoes retire on queue acceptance. Identified failures retire
69
+ * submissions that have not already reached durable admission.
69
70
  * @param input - echo content and the optional settlement callback.
70
71
  * @returns the minted identity for {@link prompt} plus the pre-prompt abandon path.
71
72
  */
@@ -118,7 +119,9 @@ export interface ISession {
118
119
  seq: SessionSeq;
119
120
  }>>;
120
121
  /**
121
- * Extend the history window backwards (older messages pagination).
122
+ * Extend history by at least 50 messages and two Turn starts, including a
123
+ * partial Turn at the window's beginning. Stop at 500 messages or history
124
+ * exhaustion even when those minima cannot be met. Publish one prepend.
122
125
  * @returns completion; failures land in snapshot.openState/loadingOlder.
123
126
  */
124
127
  loadOlder(): Promise<void>;
@@ -127,6 +130,8 @@ export interface ISession {
127
130
  * turn-jump loader. Repeated calls while a jump is paging lower its shared
128
131
  * target and return the in-flight completion; `snapshot.loadingOlder` is
129
132
  * the busy signal for the whole jump.
133
+ * Each page uses the same Turn alignment as loadOlder, with a 200-message
134
+ * minimum. The 500-message limit applies per page, not to the whole jump.
130
135
  * Older pages publish together at completion, including successful pages
131
136
  * before a later failure; live events remain independently visible.
132
137
  * @param seq - durable event seq the window must reach (a turn's `turn/start` seq).
@@ -38,7 +38,7 @@ export type PendingSubmissionPlacement = 'transcript' | 'queued' | 'steering';
38
38
  export interface PendingSubmission {
39
39
  /** The prompt RPC identity; the durable `user/message` source echoes it as `rpcId`. */
40
40
  readonly requestId: SessionRequestId;
41
- /** Expected surface until the Host reports the admitted queue or durable occurrence. */
41
+ /** Surface fixed at submission time; Inbox acceptance does not move a Chat echo into the dock. */
42
42
  readonly placement: PendingSubmissionPlacement;
43
43
  /** Client wall-clock ms when the submission began. */
44
44
  readonly time: number;
@@ -75,6 +75,12 @@ export declare class ProjectionValueStore {
75
75
  * @returns the value, or undefined while the key is absent.
76
76
  */
77
77
  get(key: string): unknown;
78
+ /**
79
+ * Read the accepted Host watermark without subscribing or copying a value.
80
+ * @param key - projection key.
81
+ * @returns the current sequence, or undefined for absent and cached values.
82
+ */
83
+ seqOf(key: string): SessionSeqCursor | undefined;
78
84
  /**
79
85
  * Read every current projection value as one reference-stable snapshot.
80
86
  * @returns The same frozen value map until a row changes.
@@ -38,6 +38,15 @@ export class ProjectionValueStore {
38
38
  get(key) {
39
39
  return this.rows.get(key)?.value;
40
40
  }
41
+ /**
42
+ * Read the accepted Host watermark without subscribing or copying a value.
43
+ * @param key - projection key.
44
+ * @returns the current sequence, or undefined for absent and cached values.
45
+ */
46
+ seqOf(key) {
47
+ const row = this.rows.get(key);
48
+ return row?.kind === 'sequenced' ? row.seq : undefined;
49
+ }
41
50
  /**
42
51
  * Read every current projection value as one reference-stable snapshot.
43
52
  * @returns The same frozen value map until a row changes.
@@ -10,9 +10,9 @@ import { MutableSessionEventSource } from '../contract/events.ts';
10
10
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
11
11
  import type { SessionRemotes } from './remotes.ts';
12
12
  import { ProjectionValueStore } from './projection-store.ts';
13
- /** Messages requested per history page. */
13
+ /** Minimum message count for ordinary history windows. */
14
14
  export declare const PAGE_MESSAGES = 50;
15
- /** Messages requested per page while a turn jump loops backwards (fewer, larger round trips). */
15
+ /** Minimum messages per page while a turn jump loops backwards. */
16
16
  export declare const JUMP_PAGE_MESSAGES = 200;
17
17
  /** Manager-owned observers of a Session object's local state edges. */
18
18
  export interface SessionOptions {
@@ -192,7 +192,7 @@ export declare class Session implements SessionFace {
192
192
  }>>;
193
193
  /** First open: pull the tail page (idempotent — in-flight/already-open returns the existing promise). */
194
194
  open(): Promise<void>;
195
- /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
195
+ /** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
196
196
  loadOlder(): Promise<void>;
197
197
  /** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
198
198
  loadThrough(seq: SessionSeq): Promise<void>;
@@ -262,9 +262,11 @@ export declare class Session implements SessionFace {
262
262
  private appendLive;
263
263
  /** Observe durable acceptance even when insertion and claim share one projection notification. */
264
264
  private observeSubmissionEvent;
265
- private observeSteeringInsertions;
265
+ private observeSubmissionInsertions;
266
266
  private observeSubmissionMessage;
267
- /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
267
+ /** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
268
+ private retireAdmittedSubmission;
269
+ /** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
268
270
  private observeSubmissionInbox;
269
271
  /**
270
272
  * Latch one observed settlement and remove the echo an animation frame
@@ -15,10 +15,15 @@ function projectionsBaseline(value) {
15
15
  asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
16
16
  };
17
17
  }
18
- /** Messages requested per history page. */
18
+ /** Minimum message count for ordinary history windows. */
19
19
  export const PAGE_MESSAGES = 50;
20
- /** Messages requested per page while a turn jump loops backwards (fewer, larger round trips). */
20
+ const HISTORY_PAGE_OPTIONS = { maxMessages: 500, turnWindow: { minMessages: PAGE_MESSAGES, minTurns: 2 } };
21
+ /** Minimum messages per page while a turn jump loops backwards. */
21
22
  export const JUMP_PAGE_MESSAGES = 200;
23
+ const JUMP_PAGE_OPTIONS = {
24
+ ...HISTORY_PAGE_OPTIONS,
25
+ turnWindow: { ...HISTORY_PAGE_OPTIONS.turnWindow, minMessages: JUMP_PAGE_MESSAGES },
26
+ };
22
27
  /**
23
28
  * Owns a session's event window, lifecycle state, and observable
24
29
  * snapshot. React bindings remain outside this data layer. Features see only
@@ -304,7 +309,7 @@ export class Session {
304
309
  this.openPromise = promise;
305
310
  return promise;
306
311
  }
307
- /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
312
+ /** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
308
313
  async loadOlder() {
309
314
  if (this.openState !== 'open' || !this.hasMore || this.loadingOlder)
310
315
  return;
@@ -314,7 +319,10 @@ export class Session {
314
319
  this.loadingOlder = true;
315
320
  this.notifier.markDirty();
316
321
  try {
317
- await events.prepend({ beforeSeq: this.baseSeq, maxMessages: PAGE_MESSAGES });
322
+ await events.prepend({
323
+ beforeSeq: this.baseSeq,
324
+ ...HISTORY_PAGE_OPTIONS,
325
+ });
318
326
  }
319
327
  catch (error) {
320
328
  if (!isRemoteFailure(error)) {
@@ -363,7 +371,7 @@ export class Session {
363
371
  if (generation !== this.openGeneration)
364
372
  return;
365
373
  const before = pending.beforeSeq;
366
- await events.prepend({ beforeSeq: before, maxMessages: JUMP_PAGE_MESSAGES });
374
+ await events.prepend({ beforeSeq: before, ...JUMP_PAGE_OPTIONS });
367
375
  // No-progress guard: an empty or dropped page that still claims more
368
376
  // history must end the loop, not spin it.
369
377
  if (pending.beforeSeq >= before)
@@ -504,10 +512,12 @@ export class Session {
504
512
  async dispose() {
505
513
  this.stopObservingInbox();
506
514
  // Unsettled echoes retire as failed so their owners can restore or
507
- // release browser resources; echoes already scheduled as observed keep
508
- // that settlement.
509
- for (const requestId of [...this.submissionSettlements.keys()]) {
510
- this.retireFailedSubmission(requestId);
515
+ // release browser resources; admitted echoes keep their observed outcome.
516
+ for (const [requestId, settlement] of [...this.submissionSettlements]) {
517
+ if (settlement.admitted !== undefined)
518
+ this.scheduleObservedRetirement(requestId, settlement.admitted);
519
+ else
520
+ this.retireFailedSubmission(requestId);
511
521
  }
512
522
  this.openGeneration++;
513
523
  const events = this.events;
@@ -532,7 +542,7 @@ export class Session {
532
542
  });
533
543
  this.events = events;
534
544
  try {
535
- await events.open({ maxMessages: PAGE_MESSAGES });
545
+ await events.open(HISTORY_PAGE_OPTIONS);
536
546
  if (generation !== this.openGeneration || this.events !== events)
537
547
  return;
538
548
  this.openState = 'open';
@@ -585,7 +595,7 @@ export class Session {
585
595
  if (projections !== undefined)
586
596
  this.projections.seed(projections);
587
597
  this.eventSource.replace(visible, hasMore);
588
- // A new follow baseline replaces optimistic steering with Host-owned rows.
598
+ // A new follow baseline replaces confirmed optimistic inputs with Host-owned rows.
589
599
  // Receipt-backed inputs are accepted, not failed, even if their history is outside this window.
590
600
  if (projections !== undefined) {
591
601
  for (const [requestId, { receipt }] of this.submissionSettlements) {
@@ -598,7 +608,9 @@ export class Session {
598
608
  this.observeSubmissionEvent(entry.event);
599
609
  if (projections !== undefined) {
600
610
  const inbox = projections.values.inbox;
601
- this.observeSteeringInsertions(inbox?.['next-step'] ?? [], 0, projections.asOfSeq);
611
+ for (const target of ['next-turn', 'next-step']) {
612
+ this.observeSubmissionInsertions(target, inbox?.[target] ?? [], 0, projections.asOfSeq);
613
+ }
602
614
  }
603
615
  this.notifier.markDirty();
604
616
  }
@@ -662,30 +674,29 @@ export class Session {
662
674
  return;
663
675
  if (event.type === 'agent/inbox/spliced') {
664
676
  const { target, start, removedCount = 0, inserted, outcome } = event.data;
665
- if (target === 'next-step') {
666
- for (const [requestId, settlement] of this.submissionSettlements) {
667
- const receipt = settlement.receipt;
668
- if (receipt === undefined || receipt.index === null || receipt.seq >= event.seq)
669
- continue;
670
- const removed = receipt.index >= start && receipt.index < start + removedCount;
671
- if (removed && outcome === 'canceled')
672
- this.retireFailedSubmission(requestId);
673
- else
674
- settlement.receipt = {
675
- ...receipt,
676
- seq: event.seq,
677
- index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount,
678
- };
679
- }
680
- this.observeSteeringInsertions(inserted, start, event.seq);
677
+ for (const [requestId, settlement] of this.submissionSettlements) {
678
+ const receipt = settlement.receipt;
679
+ if (receipt?.target !== target || receipt.index === null || receipt.seq >= event.seq)
680
+ continue;
681
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
682
+ if (removed && outcome === 'canceled')
683
+ this.retireFailedSubmission(requestId);
684
+ else
685
+ settlement.receipt = {
686
+ ...receipt,
687
+ seq: event.seq,
688
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount,
689
+ };
681
690
  }
691
+ this.observeSubmissionInsertions(target, inserted, start, event.seq);
682
692
  for (const message of inserted)
683
693
  this.observeSubmissionMessage(message, false);
684
694
  return;
685
695
  }
686
696
  if (event.type === 'request/context' || event.type === 'turn/end') {
687
697
  for (const [requestId, settlement] of this.submissionSettlements) {
688
- if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq)
698
+ if (settlement.admitted === undefined && settlement.receipt?.index === null
699
+ && settlement.receipt.seq < event.seq)
689
700
  this.retireFailedSubmission(requestId);
690
701
  }
691
702
  return;
@@ -693,34 +704,60 @@ export class Session {
693
704
  if (event.type === 'user/message')
694
705
  this.observeSubmissionMessage(event.data, true);
695
706
  }
696
- observeSteeringInsertions(messages, start, seq) {
707
+ observeSubmissionInsertions(target, messages, start, seq) {
697
708
  for (const [index, message] of messages.entries()) {
698
709
  const source = message.source;
699
710
  if (source.kind !== 'user' || !('rpcId' in source))
700
711
  continue;
701
712
  const settlement = this.submissionSettlements.get(source.rpcId);
702
- if (settlement?.placement !== 'steering' || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq)
713
+ if (settlement === undefined || settlement.placement === 'queued'
714
+ || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq)
703
715
  continue;
704
- settlement.receipt = { seq, index: start + index, attachments: attachmentRefsIn(message.content) };
716
+ settlement.receipt = { target, seq, index: start + index, attachments: attachmentRefsIn(message.content) };
705
717
  }
706
718
  }
707
719
  observeSubmissionMessage(message, admitted) {
708
720
  const source = message.source;
709
721
  if (source.kind !== 'user' || !('rpcId' in source))
710
722
  return;
711
- if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === 'steering')
723
+ const settlement = this.submissionSettlements.get(source.rpcId);
724
+ if (settlement === undefined || settlement.retiring)
725
+ return;
726
+ if (!admitted) {
727
+ if (settlement.placement === 'queued')
728
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
712
729
  return;
713
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
730
+ }
731
+ settlement.admitted = attachmentRefsIn(message.content);
732
+ this.retireAdmittedSubmission(source.rpcId);
733
+ }
734
+ /** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
735
+ retireAdmittedSubmission(requestId) {
736
+ const settlement = this.submissionSettlements.get(requestId);
737
+ if (settlement?.admitted === undefined)
738
+ return;
739
+ const receipt = settlement.receipt;
740
+ if (receipt?.index === null
741
+ && (this.projections.seqOf('inbox') ?? -1) < receipt.seq)
742
+ return;
743
+ this.scheduleObservedRetirement(requestId, settlement.admitted);
714
744
  }
715
- /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
745
+ /** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
716
746
  observeSubmissionInbox() {
717
747
  if (this.submissionSettlements.size === 0)
718
748
  return;
719
749
  const inbox = this.projections.get('inbox');
720
750
  if (inbox === undefined)
721
751
  return;
722
- for (const message of [...inbox['next-turn'], ...inbox['next-step']])
723
- this.observeSubmissionMessage(message, false);
752
+ const seq = this.projections.seqOf('inbox');
753
+ for (const target of ['next-turn', 'next-step']) {
754
+ if (seq !== undefined)
755
+ this.observeSubmissionInsertions(target, inbox[target], 0, seq);
756
+ for (const message of inbox[target])
757
+ this.observeSubmissionMessage(message, false);
758
+ }
759
+ for (const requestId of this.submissionSettlements.keys())
760
+ this.retireAdmittedSubmission(requestId);
724
761
  }
725
762
  /**
726
763
  * Latch one observed settlement and remove the echo an animation frame
@@ -738,7 +775,7 @@ export class Session {
738
775
  /** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
739
776
  retireFailedSubmission(requestId) {
740
777
  const settlement = this.submissionSettlements.get(requestId);
741
- if (settlement === undefined || settlement.retiring)
778
+ if (settlement === undefined || settlement.retiring || settlement.admitted !== undefined)
742
779
  return;
743
780
  settlement.retiring = true;
744
781
  this.finishSubmission(requestId, { reason: 'failed' });
@@ -76,7 +76,7 @@ export class SessionEventStream extends RemoteJournalStream {
76
76
  for await (const frame of this.remote.session.follow({
77
77
  address: this.address,
78
78
  assistantStream: true,
79
- ...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
79
+ ...this.repairRequest(request),
80
80
  }, signal)) {
81
81
  if (frame.type === 'snapshot') {
82
82
  for (const record of frame.records)
@@ -121,7 +121,10 @@ export class SessionEventStream extends RemoteJournalStream {
121
121
  }
122
122
  /** @inheritdoc */
123
123
  repairRequest(request) {
124
- return request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages };
124
+ return {
125
+ ...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
126
+ ...(request.turnWindow === undefined ? {} : { turnWindow: request.turnWindow }),
127
+ };
125
128
  }
126
129
  }
127
130
  //# sourceMappingURL=transport.js.map
@@ -115,7 +115,7 @@ export class SessionHistoryController {
115
115
  if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) {
116
116
  throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(throughSeq)}`, {});
117
117
  }
118
- const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
118
+ const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq, request.turnWindow);
119
119
  const records = pageRecords(page.events);
120
120
  return {
121
121
  records,
@@ -137,7 +137,7 @@ export class SessionHistoryController {
137
137
  * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
138
138
  */
139
139
  async *follow(request, signal) {
140
- validateFollowRequest(request);
140
+ validateHistoryWindow(request);
141
141
  const { address } = request;
142
142
  const target = addressId(address);
143
143
  const buffered = new Deque();
@@ -198,7 +198,7 @@ export class SessionHistoryController {
198
198
  signal.throwIfAborted();
199
199
  const cursor = source.cursor;
200
200
  snapshotCursor = cursor;
201
- const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
201
+ const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES, cursor, request.turnWindow);
202
202
  const assistantStream = request.assistantStream === true
203
203
  ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 }
204
204
  : undefined;
@@ -327,16 +327,23 @@ function validatePageRequest(request) {
327
327
  || Object.is(request.beforeSeq, -0))) {
328
328
  throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {});
329
329
  }
330
- if (request.maxMessages !== undefined
331
- && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) {
332
- throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {});
333
- }
330
+ validateHistoryWindow(request);
334
331
  }
335
- function validateFollowRequest(request) {
332
+ function validateHistoryWindow(request) {
336
333
  if (request.maxMessages !== undefined
337
334
  && (!Number.isSafeInteger(request.maxMessages) || request.maxMessages <= 0)) {
338
335
  throw new RemoteError('gateway/bad-request', 'maxMessages must be a positive safe integer', {});
339
336
  }
337
+ const window = request.turnWindow;
338
+ if (window !== undefined) {
339
+ if (!Number.isSafeInteger(window.minMessages) || window.minMessages <= 0
340
+ || window.minMessages > (request.maxMessages ?? DEFAULT_MAX_MESSAGES)) {
341
+ throw new RemoteError('gateway/bad-request', 'turnWindow.minMessages must be a positive safe integer no greater than maxMessages', {});
342
+ }
343
+ if (!Number.isSafeInteger(window.minTurns) || window.minTurns <= 0) {
344
+ throw new RemoteError('gateway/bad-request', 'turnWindow.minTurns must be a positive safe integer', {});
345
+ }
346
+ }
340
347
  }
341
348
  function addressId(address) {
342
349
  return address.kind === 'session' ? address.sessionId : address.childSessionId;
@@ -385,12 +392,20 @@ function rejectNotFound(address) {
385
392
  childSessionId: address.childSessionId,
386
393
  });
387
394
  }
388
- function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
395
+ function paginate(events, beforeSeq, maxMessages, throughSeq, turnWindow) {
389
396
  const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
390
397
  let count = 0;
398
+ let turns = 0;
391
399
  let cut = SessionLogOffset(0);
392
400
  for (let index = end - 1; index >= 0; index--) {
393
401
  const event = events[index];
402
+ if (turnWindow !== undefined && event.type === 'turn/start') {
403
+ turns++;
404
+ if (count >= turnWindow.minMessages && turns >= turnWindow.minTurns) {
405
+ cut = SessionLogOffset(index);
406
+ break;
407
+ }
408
+ }
394
409
  if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event))
395
410
  continue;
396
411
  count++;
@@ -443,11 +443,17 @@ export interface SessionPageRequest {
443
443
  readonly throughSeq: number;
444
444
  readonly beforeSeq?: number;
445
445
  readonly maxMessages?: number;
446
+ /** Stop at a Turn start after both minima, unless maxMessages or history exhaustion wins. */
447
+ readonly turnWindow?: {
448
+ /** Minimum append-origin user/assistant messages; must not exceed maxMessages. */
449
+ readonly minMessages: number;
450
+ /** Minimum Turn starts crossed, including the partial Turn at beforeSeq. */
451
+ readonly minTurns: number;
452
+ };
446
453
  }
447
454
  /** One live event request for a durable Session address. */
448
- export interface SessionFollowRequest {
455
+ export interface SessionFollowRequest extends Pick<SessionPageRequest, 'maxMessages' | 'turnWindow'> {
449
456
  readonly address: SessionAddress;
450
- readonly maxMessages?: number;
451
457
  /** Include process-local assistant presentation frames for the Web client. */
452
458
  readonly assistantStream?: true;
453
459
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-api-session-controller",
3
3
  "description": "Session Remote commands, cold reads, and live control transport",
4
- "version": "0.1.7-alpha.1",
4
+ "version": "0.1.7-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -67,39 +67,39 @@
67
67
  "dependencies": {
68
68
  "mime-types": "^3.0.2",
69
69
  "zod": "^4.4.3",
70
- "@deepseek-ai/dsh-deque": "^0.1.7-alpha.1",
71
- "@deepseek-ai/schemastery": "^3.18.3",
72
- "@deepseek-ai/dsh-brand": "^0.1.7-alpha.1"
70
+ "@deepseek-ai/schemastery": "~3.18.4",
71
+ "@deepseek-ai/dsh-brand": "0.1.7-alpha.2",
72
+ "@deepseek-ai/dsh-deque": "0.1.7-alpha.2"
73
73
  },
74
74
  "peerDependencies": {
75
- "@deepseek-ai/cordis": "^4.0.3",
76
- "@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
77
- "@deepseek-ai/dsh-agent-default-model": "^0.1.7-alpha.1",
78
- "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-alpha.1",
79
- "@deepseek-ai/dsh-api-gateway": "^0.1.7-alpha.1",
80
- "@deepseek-ai/dsh-client-connection": "^0.1.7-alpha.1",
81
- "@deepseek-ai/dsh-client-file-upload": "^0.1.7-alpha.1",
82
- "@deepseek-ai/dsh-commands": "^0.1.7-alpha.1",
83
- "@deepseek-ai/dsh-attachment": "^0.1.7-alpha.1",
84
- "@deepseek-ai/dsh-fs": "^0.1.7-alpha.1",
85
- "@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
86
- "@deepseek-ai/dsh-file-reference": "^0.1.7-alpha.1",
87
- "@deepseek-ai/dsh-native-command": "^0.1.7-alpha.1",
88
- "@deepseek-ai/dsh-scope": "^0.1.7-alpha.1",
89
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.7-alpha.1",
90
- "@deepseek-ai/dsh-session-title": "^0.1.7-alpha.1",
91
- "@deepseek-ai/dsh-session-query": "^0.1.7-alpha.1",
92
- "@deepseek-ai/dsh-skill": "^0.1.7-alpha.1",
93
- "@deepseek-ai/dsh-typert-registry": "^0.1.7-alpha.1",
94
- "@deepseek-ai/dsh-typert-protocol": "^0.1.7-alpha.1",
95
- "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
96
- "@deepseek-ai/dsh-util-time": "^0.1.7-alpha.1",
97
- "@deepseek-ai/dsh-subagent": "^0.1.7-alpha.1",
98
- "@deepseek-ai/dsh-util-values": "^0.1.7-alpha.1",
99
- "@deepseek-ai/dsh-workspace": "^0.1.7-alpha.1",
100
- "@deepseek-ai/dsh-util-workspace-path": "^0.1.7-alpha.1",
101
- "@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
102
- "@deepseek-ai/dsh-session-persistence": "^0.1.7-alpha.1"
75
+ "@deepseek-ai/dsh-agent": "0.1.7-alpha.2",
76
+ "@deepseek-ai/cordis": "~4.0.4",
77
+ "@deepseek-ai/dsh-agent-default-model": "0.1.7-alpha.2",
78
+ "@deepseek-ai/dsh-agent-preset-registry": "0.1.7-alpha.2",
79
+ "@deepseek-ai/dsh-api-gateway": "0.1.7-alpha.2",
80
+ "@deepseek-ai/dsh-attachment": "0.1.7-alpha.2",
81
+ "@deepseek-ai/dsh-client-connection": "0.1.7-alpha.2",
82
+ "@deepseek-ai/dsh-client-file-upload": "0.1.7-alpha.2",
83
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
84
+ "@deepseek-ai/dsh-native-command": "0.1.7-alpha.2",
85
+ "@deepseek-ai/dsh-scope": "0.1.7-alpha.2",
86
+ "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
87
+ "@deepseek-ai/dsh-file-reference": "0.1.7-alpha.2",
88
+ "@deepseek-ai/dsh-session-persistence": "0.1.7-alpha.2",
89
+ "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
90
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.7-alpha.2",
91
+ "@deepseek-ai/dsh-session-query": "0.1.7-alpha.2",
92
+ "@deepseek-ai/dsh-session-title": "0.1.7-alpha.2",
93
+ "@deepseek-ai/dsh-fs": "0.1.7-alpha.2",
94
+ "@deepseek-ai/dsh-commands": "0.1.7-alpha.2",
95
+ "@deepseek-ai/dsh-skill": "0.1.7-alpha.2",
96
+ "@deepseek-ai/dsh-subagent": "0.1.7-alpha.2",
97
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
98
+ "@deepseek-ai/dsh-typert-registry": "0.1.7-alpha.2",
99
+ "@deepseek-ai/dsh-util-time": "0.1.7-alpha.2",
100
+ "@deepseek-ai/dsh-util-workspace-path": "0.1.7-alpha.2",
101
+ "@deepseek-ai/dsh-workspace": "0.1.7-alpha.2",
102
+ "@deepseek-ai/dsh-util-values": "0.1.7-alpha.2"
103
103
  },
104
104
  "peerDependenciesMeta": {
105
105
  "@deepseek-ai/dsh-session-persistence": {
@@ -111,45 +111,45 @@
111
111
  },
112
112
  "devDependencies": {
113
113
  "@types/mime-types": "^3.0.1",
114
- "@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
115
- "@deepseek-ai/dsh-agent-default-model": "^0.1.7-alpha.1",
116
- "@deepseek-ai/dsh-agent-loop": "^0.1.7-alpha.1",
117
- "@deepseek-ai/dsh-api-gateway": "^0.1.7-alpha.1",
118
- "@deepseek-ai/dsh-client-connection": "^0.1.7-alpha.1",
119
- "@deepseek-ai/dsh-attachment": "^0.1.7-alpha.1",
120
- "@deepseek-ai/dsh-client-file-upload": "^0.1.7-alpha.1",
121
- "@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1",
122
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.7-alpha.1",
123
- "@deepseek-ai/dsh-commands": "^0.1.7-alpha.1",
124
- "@deepseek-ai/dsh-file-reference": "^0.1.7-alpha.1",
125
- "@deepseek-ai/dsh-fs": "^0.1.7-alpha.1",
126
- "@deepseek-ai/dsh-fs-local": "^0.1.7-alpha.1",
127
- "@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
128
- "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-alpha.1",
129
- "@deepseek-ai/cordis": "^4.0.3",
130
- "@deepseek-ai/dsh-native-command": "^0.1.7-alpha.1",
131
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.7-alpha.1",
132
- "@deepseek-ai/dsh-permission-presets": "^0.1.7-alpha.1",
133
- "@deepseek-ai/dsh-scope": "^0.1.7-alpha.1",
134
- "@deepseek-ai/dsh-session-persistence": "^0.1.7-alpha.1",
135
- "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
136
- "@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
137
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.7-alpha.1",
138
- "@deepseek-ai/dsh-session-title": "^0.1.7-alpha.1",
139
- "@deepseek-ai/dsh-skill": "^0.1.7-alpha.1",
140
- "@deepseek-ai/dsh-storage": "^0.1.7-alpha.1",
141
- "@deepseek-ai/dsh-remote-mock": "^0.1.7-alpha.1",
142
- "@deepseek-ai/dsh-typert-protocol": "^0.1.7-alpha.1",
143
- "@deepseek-ai/dsh-typert-registry": "^0.1.7-alpha.1",
144
- "@deepseek-ai/dsh-util-crypto": "^0.1.7-alpha.1",
145
- "@deepseek-ai/dsh-storage-domain": "^0.1.7-alpha.1",
146
- "@deepseek-ai/dsh-util-time": "^0.1.7-alpha.1",
147
- "@deepseek-ai/dsh-storage-json": "^0.1.7-alpha.1",
148
- "@deepseek-ai/dsh-session-query": "^0.1.7-alpha.1",
149
- "@deepseek-ai/dsh-util-workspace-path": "^0.1.7-alpha.1",
150
- "@deepseek-ai/dsh-workspace": "^0.1.7-alpha.1",
151
- "@deepseek-ai/dsh-subagent": "^0.1.7-alpha.1",
152
- "@deepseek-ai/dsh-util-values": "^0.1.7-alpha.1"
114
+ "@deepseek-ai/cordis": "~4.0.4",
115
+ "@deepseek-ai/dsh-agent": "0.1.7-alpha.2",
116
+ "@deepseek-ai/dsh-agent-default-model": "0.1.7-alpha.2",
117
+ "@deepseek-ai/dsh-agent-loop": "0.1.7-alpha.2",
118
+ "@deepseek-ai/dsh-agent-loop-testkit": "0.1.7-alpha.2",
119
+ "@deepseek-ai/dsh-agent-preset-registry": "0.1.7-alpha.2",
120
+ "@deepseek-ai/dsh-api-gateway": "0.1.7-alpha.2",
121
+ "@deepseek-ai/dsh-client-connection": "0.1.7-alpha.2",
122
+ "@deepseek-ai/dsh-attachment": "0.1.7-alpha.2",
123
+ "@deepseek-ai/dsh-client-test-runtime": "0.1.7-alpha.2",
124
+ "@deepseek-ai/dsh-file-reference": "0.1.7-alpha.2",
125
+ "@deepseek-ai/dsh-commands": "0.1.7-alpha.2",
126
+ "@deepseek-ai/dsh-fs": "0.1.7-alpha.2",
127
+ "@deepseek-ai/dsh-fs-local": "0.1.7-alpha.2",
128
+ "@deepseek-ai/dsh-permission-presets": "0.1.7-alpha.2",
129
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
130
+ "@deepseek-ai/dsh-remote-mock": "0.1.7-alpha.2",
131
+ "@deepseek-ai/dsh-native-command": "0.1.7-alpha.2",
132
+ "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
133
+ "@deepseek-ai/dsh-session-persistence": "0.1.7-alpha.2",
134
+ "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
135
+ "@deepseek-ai/dsh-scope": "0.1.7-alpha.2",
136
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.7-alpha.2",
137
+ "@deepseek-ai/dsh-session-query": "0.1.7-alpha.2",
138
+ "@deepseek-ai/dsh-session-title": "0.1.7-alpha.2",
139
+ "@deepseek-ai/dsh-client-store": "0.1.7-alpha.2",
140
+ "@deepseek-ai/dsh-client-file-upload": "0.1.7-alpha.2",
141
+ "@deepseek-ai/dsh-skill": "0.1.7-alpha.2",
142
+ "@deepseek-ai/dsh-storage-domain": "0.1.7-alpha.2",
143
+ "@deepseek-ai/dsh-storage-json": "0.1.7-alpha.2",
144
+ "@deepseek-ai/dsh-storage": "0.1.7-alpha.2",
145
+ "@deepseek-ai/dsh-subagent": "0.1.7-alpha.2",
146
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.2",
147
+ "@deepseek-ai/dsh-typert-registry": "0.1.7-alpha.2",
148
+ "@deepseek-ai/dsh-util-crypto": "0.1.7-alpha.2",
149
+ "@deepseek-ai/dsh-util-time": "0.1.7-alpha.2",
150
+ "@deepseek-ai/dsh-util-values": "0.1.7-alpha.2",
151
+ "@deepseek-ai/dsh-workspace": "0.1.7-alpha.2",
152
+ "@deepseek-ai/dsh-util-workspace-path": "0.1.7-alpha.2"
153
153
  },
154
154
  "scripts": {
155
155
  "bundle": "tsdown",