@deepseek-ai/dsh-api-session-controller 0.1.7-alpha.2 → 0.1.7-rc.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
@@ -1,6 +1,40 @@
1
- # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
- # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
- # after editing either side, bring the other along and re-record with:
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
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: fa2a251e94cfa2855790d7d71457ac4bdf2ff752
6
- README.zh.md: d1a73ed5843fddadfc2f401fee1a86eece4e5272
5
+ /:
6
+ en: 82789442228fc62a
7
+ zh: f1d511ad838187de
8
+ /session-controller:
9
+ en: fb78499ee63f41b9
10
+ zh: 6fff916b1fa26314
11
+ /session-controller/summary:
12
+ en: 4914824c347fdab5
13
+ zh: 684a7b7fa970c39f
14
+ /session-controller/table-of-contents:
15
+ en: 1fdf707322fb6996
16
+ zh: 0204f7e9d4aa3cce
17
+ /session-controller/use-this-package:
18
+ en: b1a66e01de414924
19
+ zh: 7c01f66b94b01ff9
20
+ /session-controller/client-references:
21
+ en: 41bdda1ac32df558
22
+ zh: aa5d0e325924066b
23
+ /session-controller/session-media-references:
24
+ en: 266ab2b1e2fec542
25
+ zh: b3450ae28a1e0baf
26
+ /session-controller/configuration:
27
+ en: 5c4c4224074e38a8
28
+ zh: e0b0edac65c88ff4
29
+ /session-controller/model-experience:
30
+ en: 30b917bf822bb4df
31
+ zh: b0b023b5b526c317
32
+ /session-controller/model-experience/kv-cache-effect:
33
+ en: 22fc0c9dbedd3bb6
34
+ zh: fad040ca55fd0e0a
35
+ /session-controller/known-limitations-and-deferred-work:
36
+ en: e896bfcaf38b4f6c
37
+ zh: 55a4eba838f166de
38
+ /session-controller/known-limitations-and-deferred-work/dev-note:
39
+ en: 68e0dfe4588c5b88
40
+ zh: db51b435611bded1
package/README.md CHANGED
@@ -43,7 +43,7 @@ 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 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.
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 releases the durable entry. A successful message retains its transient rows until the owning `step/end`, so pending tool identities remain available for dispatch. Failed or interrupted settlements retire their rows immediately; 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
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
 
@@ -78,7 +78,9 @@ References keep local Session data, scoped Contexts, and history streams alive,
78
78
 
79
79
  `SessionMediaReferences` mounts `GET|HEAD /api/file?path=<absolute path>` on the authenticated `connection.fetch` channel when `connection`, `fs`, and `attachments` are composed. It reads ordinary files through `ctx.fs`, including temporary paths outside registered workspaces and files in remote providers. Neither directory containment nor MIME categories restrict access; `mime-types` supplies the response type, with `application/octet-stream` for unknown extensions. GET reuses `readBytes` for preflight and ongoing byte limits; HEAD reads metadata only. All files use `ctx.attachments.imageLimits.maxImageBytes` (normally 20 MiB); exceeding this limit returns 413. Responses contain the complete file, ignore Range, and carry `private, no-store`, `nosniff`, and a sandbox CSP so directly opened HTML/SVG cannot execute with the API origin. The Client rewrite lives in `ui-chat` (`AssistantMarkdown`); audio/video responses are available, while Markdown audio/video player nodes remain separate work.
80
80
 
81
- `workspacePathApplications({ path })` returns the serving desktop’s file handlers after the same filesystem mapping check used by `openWorkspacePath`. An optional `application` in the open request selects a currently registered handler without changing the system default. Application names, default selection, icons, and platform coverage come from [native-command](../../util/native-command/README.md). These operations do not activate an Agent or append Session events. Native failures return bounded messages; original command errors remain Host-side causes.
81
+ GUI model selection requires the exact provider/model pair in the available catalog; unavailable choices reject with `session/model-unavailable`. Prompt admission retains the saved route without a catalog availability gate, so request execution reports missing credentials or unavailable models. `initializeDefaultModel()` saves the first available account model after login when no other provider has a configured API key; credential checks use configured references independently of model availability. A provider with no available models rejects initialization with `session/provider-models-unavailable`. Availability never substitutes another model or rewrites a Session selection.
82
+
83
+ A successful `selectModel` response acknowledges the Session-local selection without waiting for the default profile setting to save. Default saves run in the background in submission order; a failure logs a warning and leaves the Session selection intact. New Sessions read the last successfully saved default.
82
84
 
83
85
  -----
84
86
 
package/README.zh.md CHANGED
@@ -43,7 +43,7 @@ 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()` 拉一页按轮次开头对齐的历史,而 `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 与历史操作,不提供文件传输。
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 会发布持久条目。成功消息的瞬态 row 保留到所属 `step/end`,让待派发工具的身份持续可用;失败或中断的 settlement 立即移除对应 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
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
 
@@ -78,7 +78,9 @@ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以
78
78
 
79
79
  当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 20 MiB);超过此上限返回 413。响应包含完整文件,忽略 Range,并携带 `private, no-store`、`nosniff` 与沙箱 CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。
80
80
 
81
- `workspacePathApplications({ path })` 使用与 `openWorkspacePath` 相同的文件系统映射校验,返回服务端桌面上该文件的关联应用。打开请求中的可选 `application` 指定当前关联的应用,不修改系统默认应用。应用名称、默认项、图标和平台支持范围由 [native-command](../../util/native-command/README.zh.md) 提供。这些操作不激活 Agent,也不追加会话事件。原生操作失败时返回简短消息,原始命令异常作为 Host 端原因保留。
81
+ GUI 模型选择要求确切提供方/模型对出现在可用目录中;不可用的选择以 `session/model-unavailable` 拒绝。提示词准入保留已保存的路由,不按目录可用性阻断发送,由请求执行报告凭据缺失或模型不可用。`initializeDefaultModel()` 在账号登录后、其他提供方均未配置 API key 时,将第一个可用账号模型保存为默认模型;凭据检查使用已配置的引用,不依赖模型是否可用。提供方没有可用模型时,初始化以 `session/provider-models-unavailable` 拒绝。可用性变化不会替换模型或改写会话选择。
82
+
83
+ `selectModel` 成功返回表示会话级模型选择已生效,不等待默认 profile 设置保存。默认设置在后台按提交顺序保存;保存失败会记录警告,并保留会话选择。新会话读取最近一次成功保存的默认值。
82
84
 
83
85
  -----
84
86
 
package/lib/client.js CHANGED
@@ -1378,6 +1378,7 @@ window.__ModuleLoader__.load({
1378
1378
  /** Keeps transient Assistant presentation behind one settlement-aware interface. */
1379
1379
  var ClientAssistantStream = class {
1380
1380
  activeAttempt;
1381
+ retainedAttempt;
1381
1382
  pending = /* @__PURE__ */ new Map();
1382
1383
  publishedSeqs = /* @__PURE__ */ new Set();
1383
1384
  durableCursor = -1;
@@ -1392,6 +1393,7 @@ window.__ModuleLoader__.load({
1392
1393
  this.pending.clear();
1393
1394
  this.transientInGap = 0;
1394
1395
  this.activeAttempt = void 0;
1396
+ this.retainedAttempt = void 0;
1395
1397
  const opening = baseline?.activeAttempt;
1396
1398
  if (opening !== void 0) this.activeAttempt = {
1397
1399
  attemptId: opening.attemptId,
@@ -1442,13 +1444,15 @@ window.__ModuleLoader__.load({
1442
1444
  }
1443
1445
  /**
1444
1446
  * Fold one dense transient frame and release its named durable settlement.
1447
+ * Successful messages retain their transient rows until the owning Step ends;
1448
+ * interrupted messages, failed attempts, and abandonment retire them immediately.
1445
1449
  * @param frame - next Assistant stream frame received by the follow connection.
1446
1450
  * @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
1447
1451
  */
1448
1452
  acceptFrame(frame) {
1449
1453
  switch (frame.type) {
1450
1454
  case "start":
1451
- if (this.activeAttempt !== void 0 || this.pending.size > 0) return { type: "rebaseline" };
1455
+ if (this.activeAttempt !== void 0 || this.retainedAttempt !== void 0 || this.pending.size > 0) return { type: "rebaseline" };
1452
1456
  this.pending.clear();
1453
1457
  this.activeAttempt = {
1454
1458
  attemptId: frame.attemptId,
@@ -1495,6 +1499,14 @@ window.__ModuleLoader__.load({
1495
1499
  const entry = this.pending.get(frame.outcome.seq);
1496
1500
  if (entry === void 0 || entry.event.type !== frame.outcome.eventType) return { type: "rebaseline" };
1497
1501
  this.pending.delete(frame.outcome.seq);
1502
+ if (entry.event.type === "assistant/message" && entry.event.data.interrupted !== true) {
1503
+ this.retainedAttempt = {
1504
+ attemptId: attempt.attemptId,
1505
+ turn: attempt.turn,
1506
+ step: attempt.step
1507
+ };
1508
+ return this.publish(entry);
1509
+ }
1498
1510
  this.publishedSeqs.add(entry.event.seq);
1499
1511
  return {
1500
1512
  type: "settlement",
@@ -1511,6 +1523,15 @@ window.__ModuleLoader__.load({
1511
1523
  }
1512
1524
  publish(entry) {
1513
1525
  this.publishedSeqs.add(entry.event.seq);
1526
+ const retained = this.retainedAttempt;
1527
+ if (retained !== void 0 && entry.event.type === "step/end" && entry.event.data.turn === retained.turn && entry.event.data.step === retained.step) {
1528
+ this.retainedAttempt = void 0;
1529
+ return {
1530
+ type: "publish",
1531
+ entry,
1532
+ retireAttemptId: retained.attemptId
1533
+ };
1534
+ }
1514
1535
  return {
1515
1536
  type: "publish",
1516
1537
  entry
@@ -2100,8 +2121,11 @@ window.__ModuleLoader__.load({
2100
2121
  this.notifier.markDirty();
2101
2122
  return;
2102
2123
  }
2103
- if (result?.type === "publish" && this.appendLive(result.entry)) this.notifier.markDirty();
2104
- else if (result?.type === "transient") {
2124
+ if (result?.type === "publish") {
2125
+ const changed = this.appendLive(result.entry);
2126
+ if (result.retireAttemptId !== void 0) this.eventSource.settleAssistant(result.retireAttemptId);
2127
+ if (changed || result.retireAttemptId !== void 0) this.notifier.markDirty();
2128
+ } else if (result?.type === "transient") {
2105
2129
  this.eventSource.append(result.entry);
2106
2130
  this.notifier.markDirty();
2107
2131
  }
package/lib/index.js CHANGED
@@ -7,6 +7,7 @@ import { SessionQueryError } from "@deepseek-ai/dsh-session-query";
7
7
  import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
8
8
  import { mkdir } from "node:fs/promises";
9
9
  import { installModelSelection } from "@deepseek-ai/dsh-agent";
10
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
10
11
  import { randomUUID } from "node:crypto";
11
12
  import { brandString } from "@deepseek-ai/dsh-brand";
12
13
  import { AttachmentError } from "@deepseek-ai/dsh-attachment";
@@ -488,6 +489,103 @@ function agentModelSelection(selection) {
488
489
  };
489
490
  }
490
491
  //#endregion
492
+ //#region lib/types/catalog.js
493
+ /** Shared projection of the live LLM registry into the browser model catalog. */
494
+ /**
495
+ * Build the browser model catalog without requiring a Session.
496
+ * @param ctx - Host context carrying the live LLM registry.
497
+ * @param defaultSelection - deployment default used before a Session selects a model.
498
+ * @returns successful non-empty provider groups and isolated provider failures.
499
+ */
500
+ async function buildModelCatalog(ctx, defaultSelection = ctx.agentDefaultModel.currentSelection()) {
501
+ const providers = ctx.llm.listProviders();
502
+ const catalog = await Promise.all(providers.map(async (provider) => {
503
+ try {
504
+ const models = await ctx.llm.listModels(provider.id);
505
+ const entries = await Promise.all(models.map(async (model) => {
506
+ const resolved = await ctx.llm.resolveModelInfo(provider.id, model.id);
507
+ const reasoning = resolved.reasoning === void 0 ? void 0 : {
508
+ efforts: resolved.reasoning.efforts.map((effort) => ({
509
+ id: effort.id,
510
+ name: effort.name,
511
+ ...effort.description === void 0 ? {} : { description: effort.description }
512
+ })),
513
+ ...resolved.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: resolved.reasoning.defaultEffort }
514
+ };
515
+ return {
516
+ id: model.id,
517
+ name: model.name,
518
+ ...model.description === void 0 ? {} : { description: model.description },
519
+ ...reasoning === void 0 ? {} : { reasoning }
520
+ };
521
+ }));
522
+ return {
523
+ kind: "group",
524
+ group: {
525
+ id: provider.id,
526
+ name: provider.name,
527
+ models: entries
528
+ }
529
+ };
530
+ } catch (error) {
531
+ return {
532
+ kind: "failure",
533
+ failure: {
534
+ id: provider.id,
535
+ name: provider.name,
536
+ message: error instanceof Error ? error.message : String(error)
537
+ }
538
+ };
539
+ }
540
+ }));
541
+ const groups = catalog.flatMap((item) => item.kind === "group" ? [item.group] : []).filter((group) => group.models.length > 0);
542
+ return {
543
+ default: { ...defaultSelection },
544
+ routableProviders: groups.map((group) => group.id),
545
+ groups,
546
+ failures: catalog.flatMap((item) => item.kind === "failure" ? [item.failure] : [])
547
+ };
548
+ }
549
+ /**
550
+ * Check a GUI selection against the current available provider catalog.
551
+ * @param ctx - Host LLM registry.
552
+ * @param selection - stored or explicitly requested selection.
553
+ * @returns whether the exact model is currently advertised as available.
554
+ */
555
+ async function modelAvailable(ctx, selection) {
556
+ if (!ctx.llm.listProviders().some((provider) => provider.id === selection.provider)) return false;
557
+ let models;
558
+ try {
559
+ models = await ctx.llm.listModels(selection.provider);
560
+ } catch (error) {
561
+ throw new RemoteError("session/model-unavailable", error instanceof Error ? error.message : String(error), {
562
+ provider: selection.provider,
563
+ model: selection.model
564
+ });
565
+ }
566
+ return models.some((model) => model.id === selection.model);
567
+ }
568
+ /**
569
+ * Check configured provider API-key references independently of model availability.
570
+ * @param ctx - Host registry, settings, and credential services.
571
+ * @returns whether any API-key provider has a configured credential.
572
+ */
573
+ async function hasProviderApiKey(ctx) {
574
+ const settings = ctx.get("settings");
575
+ const credentials = ctx.get("credentials");
576
+ if (settings === void 0 || credentials === void 0) throw new RemoteError("session/provider-credentials-unavailable", "provider credentials are unavailable", {});
577
+ const namespaces = settings.describe({ redactSecrets: true });
578
+ for (const provider of ctx.llm.listConfigurableProviders()) {
579
+ if (provider.provider === "deepseek-account") continue;
580
+ let profile = namespaces.find((namespace) => namespace.ns === provider.settingsNs)?.value;
581
+ for (const key of provider.settingsPath) profile = typeof profile === "object" && profile !== null ? Reflect.get(profile, key) : void 0;
582
+ if (typeof profile !== "object" || profile === null) continue;
583
+ const ref = Reflect.get(profile, "apiKeyEnv");
584
+ if (typeof ref === "string" && ref.length > 0 && (await credentials.describe(credentialRef(ref))).configured) return true;
585
+ }
586
+ return false;
587
+ }
588
+ //#endregion
491
589
  //#region lib/types/commands.js
492
590
  /** Session commands whose activation policy is explicit at each Remote method. */
493
591
  var __addDisposableResource$3 = function(env, value, async) {
@@ -615,14 +713,15 @@ var SessionCommandController = class {
615
713
  };
616
714
  }
617
715
  /**
618
- * Validate and install one Session-local model selection.
716
+ * Validate and install one Session-local model selection; save the default in the background.
619
717
  * @param request - Session identity and requested model selection.
620
- * @returns the normalized selection installed for the Session.
718
+ * @returns the normalized selection installed for the Session, without waiting for default persistence.
621
719
  */
622
720
  async selectModel(request) {
623
721
  const agent = await this.resolveAgent(request.sessionId);
624
722
  return this.agents.serializeImageAdmission(agent, async () => {
625
723
  try {
724
+ await this.requireModel(request);
626
725
  const resolved = await this.ctx.llm.resolveCallConfig({
627
726
  provider: request.provider,
628
727
  model: request.model,
@@ -634,11 +733,9 @@ var SessionCommandController = class {
634
733
  ...resolved.reasoningEffort === void 0 ? {} : { reasoningEffort: resolved.reasoningEffort }
635
734
  };
636
735
  this.agents.selectForNextRequest(agent, selected);
637
- try {
638
- await this.ctx.agentDefaultModel.saveSelection(selected);
639
- } catch (error) {
736
+ this.ctx.agentDefaultModel.saveSelection(selected).catch((error) => {
640
737
  this.ctx.logger.warn(`session-controller: model selection changed for the Session but the default was not saved: ${String(error)}`);
641
- }
738
+ });
642
739
  return { selected: { ...selected } };
643
740
  } catch (error) {
644
741
  if (remoteErrorOf(error) !== void 0) throw error;
@@ -756,11 +853,6 @@ var SessionCommandController = class {
756
853
  if (request.clientTimeZone !== void 0 && clientTimeZone === void 0) throw new RemoteError("session/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: request.clientTimeZone });
757
854
  const agent = await this.resolveAgent(request.sessionId);
758
855
  if (hasPromptRequest(agent, request.requestId)) return { accepted: true };
759
- const selection = this.agents.selectionFor(agent).current;
760
- if (!routeServed(this.ctx, selection.provider)) throw new RemoteError("session/model-unavailable", `no adapter serves provider "${selection.provider}"; select a model for this session`, {
761
- provider: selection.provider,
762
- model: selection.model
763
- });
764
856
  const source = {
765
857
  kind: "user",
766
858
  rpcId: request.requestId,
@@ -805,6 +897,12 @@ var SessionCommandController = class {
805
897
  };
806
898
  return hasImage ? this.agents.serializeImageAdmission(agent, admit) : admit();
807
899
  }
900
+ async requireModel(selection) {
901
+ if (!await modelAvailable(this.ctx, selection)) throw new RemoteError("session/model-unavailable", "Select an available model before sending a message.", {
902
+ provider: selection.provider,
903
+ model: selection.model
904
+ });
905
+ }
808
906
  /**
809
907
  * Read one durable image after proving the Session log references it.
810
908
  * @param request - Session and attachment identities used for authorization.
@@ -1026,9 +1124,6 @@ function referencedImage(events, attachmentId) {
1026
1124
  if (found !== void 0) return found;
1027
1125
  }
1028
1126
  }
1029
- function routeServed(ctx, provider) {
1030
- return ctx.llm.listProviders().some((entry) => entry.id === provider);
1031
- }
1032
1127
  //#endregion
1033
1128
  //#region lib/types/control.js
1034
1129
  /** Live Session projection state with reconnect baselines. */
@@ -1953,63 +2048,6 @@ function listFields(header) {
1953
2048
  };
1954
2049
  }
1955
2050
  //#endregion
1956
- //#region lib/types/catalog.js
1957
- /** Shared projection of the live LLM registry into the browser model catalog. */
1958
- /**
1959
- * Build the browser model catalog without requiring a Session.
1960
- * @param ctx - Host context carrying the live LLM registry.
1961
- * @param defaultSelection - deployment default used before a Session selects a model.
1962
- * @returns successful non-empty provider groups and isolated provider failures.
1963
- */
1964
- async function buildModelCatalog(ctx, defaultSelection = ctx.agentDefaultModel.currentSelection()) {
1965
- const providers = ctx.llm.listProviders();
1966
- const catalog = await Promise.all(providers.map(async (provider) => {
1967
- try {
1968
- const models = await ctx.llm.listModels(provider.id);
1969
- const entries = await Promise.all(models.map(async (model) => {
1970
- const resolved = await ctx.llm.resolveModelInfo(provider.id, model.id);
1971
- const reasoning = resolved.reasoning === void 0 ? void 0 : {
1972
- efforts: resolved.reasoning.efforts.map((effort) => ({
1973
- id: effort.id,
1974
- name: effort.name,
1975
- ...effort.description === void 0 ? {} : { description: effort.description }
1976
- })),
1977
- ...resolved.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: resolved.reasoning.defaultEffort }
1978
- };
1979
- return {
1980
- id: model.id,
1981
- name: model.name,
1982
- ...model.description === void 0 ? {} : { description: model.description },
1983
- ...reasoning === void 0 ? {} : { reasoning }
1984
- };
1985
- }));
1986
- return {
1987
- kind: "group",
1988
- group: {
1989
- id: provider.id,
1990
- name: provider.name,
1991
- models: entries
1992
- }
1993
- };
1994
- } catch (error) {
1995
- return {
1996
- kind: "failure",
1997
- failure: {
1998
- id: provider.id,
1999
- name: provider.name,
2000
- message: error instanceof Error ? error.message : String(error)
2001
- }
2002
- };
2003
- }
2004
- }));
2005
- return {
2006
- default: { ...defaultSelection },
2007
- routableProviders: providers.map((provider) => provider.id),
2008
- groups: catalog.flatMap((item) => item.kind === "group" ? [item.group] : []).filter((group) => group.models.length > 0),
2009
- failures: catalog.flatMap((item) => item.kind === "failure" ? [item.failure] : [])
2010
- };
2011
- }
2012
- //#endregion
2013
2051
  //#region lib/types/model-selection-projection.js
2014
2052
  /** Durable model-selection intent and request-use projection. */
2015
2053
  const modelSelectionSchema = z$1.object({
@@ -2522,6 +2560,7 @@ let SessionController = (() => {
2522
2560
  let _search_decorators;
2523
2561
  let _create_decorators;
2524
2562
  let _selectModel_decorators;
2563
+ let _initializeDefaultModel_decorators;
2525
2564
  let _modelCatalog_decorators;
2526
2565
  let _canOpenWorkspacePath_decorators;
2527
2566
  let _openWorkspacePath_decorators;
@@ -2543,6 +2582,7 @@ let SessionController = (() => {
2543
2582
  _search_decorators = [Remote("search")];
2544
2583
  _create_decorators = [Remote("create")];
2545
2584
  _selectModel_decorators = [Remote("selectModel")];
2585
+ _initializeDefaultModel_decorators = [Remote];
2546
2586
  _modelCatalog_decorators = [Remote("modelCatalog")];
2547
2587
  _canOpenWorkspacePath_decorators = [Remote];
2548
2588
  _openWorkspacePath_decorators = [Remote("openWorkspacePath")];
@@ -2601,6 +2641,17 @@ let SessionController = (() => {
2601
2641
  },
2602
2642
  metadata: _metadata
2603
2643
  }, null, _instanceExtraInitializers);
2644
+ __esDecorate(this, null, _initializeDefaultModel_decorators, {
2645
+ kind: "method",
2646
+ name: "initializeDefaultModel",
2647
+ static: false,
2648
+ private: false,
2649
+ access: {
2650
+ has: (obj) => "initializeDefaultModel" in obj,
2651
+ get: (obj) => obj.initializeDefaultModel
2652
+ },
2653
+ metadata: _metadata
2654
+ }, null, _instanceExtraInitializers);
2604
2655
  __esDecorate(this, null, _modelCatalog_decorators, {
2605
2656
  kind: "method",
2606
2657
  name: "modelCatalog",
@@ -2921,14 +2972,30 @@ let SessionController = (() => {
2921
2972
  return this.commands.create(request);
2922
2973
  }
2923
2974
  /**
2924
- * Select one Session-local model after explicitly resuming the Session.
2975
+ * Select one Session-local model after explicitly resuming the Session; save the default in the background.
2925
2976
  * @param request - Session identity and requested model selection.
2926
- * @returns the normalized selection installed for the Session.
2977
+ * @returns the normalized selection installed for the Session, without waiting for default persistence.
2927
2978
  */
2928
2979
  selectModel(request) {
2929
2980
  return this.commands.selectModel(request);
2930
2981
  }
2931
2982
  /**
2983
+ * Select the first available account model after login when no provider API key is configured.
2984
+ * @returns after saving the first available model or retaining the existing default.
2985
+ */
2986
+ async initializeDefaultModel() {
2987
+ const provider = "deepseek-account";
2988
+ if (await hasProviderApiKey(this.ctx)) return;
2989
+ const model = (await buildModelCatalog(this.ctx)).groups.find((group) => group.id === provider)?.models[0];
2990
+ if (model === void 0) throw new RemoteError("session/provider-models-unavailable", `provider "${provider}" has no available models`, { provider });
2991
+ const selection = {
2992
+ provider,
2993
+ model: model.id,
2994
+ ...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(model.reasoning.defaultEffort) }
2995
+ };
2996
+ await this.ctx.agentDefaultModel.saveSelection(selection);
2997
+ }
2998
+ /**
2932
2999
  * Describe every currently routable model for Host-generation selectors.
2933
3000
  * @returns provider-grouped models, the deployment default, and isolated provider failures.
2934
3001
  */