@deepseek-ai/dsh-api-session-controller 0.1.7-alpha.1 → 0.1.7-rc.1
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 +2 -2
- package/README.md +6 -4
- package/README.zh.md +6 -4
- package/lib/client.js +101 -32
- package/lib/index.js +19 -6
- package/lib/typert.host.js +15 -79
- package/lib/typert.remote-client.js +9 -1
- package/lib/types/client/contract/session.d.ts +9 -4
- package/lib/types/client/contract/snapshot.d.ts +1 -1
- package/lib/types/client/sessions/assistant-stream.d.ts +5 -0
- package/lib/types/client/sessions/assistant-stream.js +16 -1
- package/lib/types/client/sessions/projection-store.d.ts +6 -0
- package/lib/types/client/sessions/projection-store.js +9 -0
- package/lib/types/client/sessions/session.d.ts +7 -5
- package/lib/types/client/sessions/session.js +81 -40
- package/lib/types/client/transport.js +5 -2
- package/lib/types/history.js +24 -9
- package/lib/types/types.d.ts +8 -2
- package/package.json +71 -71
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 728d98b90aa96829a878b071c03af3ec961c839e
|
|
6
|
+
README.zh.md: 9a2c54b6865302e69daa5f67ec9baa17255f8100
|
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
|
|
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
|
-
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
|
|
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
|
|
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()`
|
|
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()`
|
|
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
|
-
Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与提示词之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。回显按顺序存放图片预览与持久文件引用。Session 根据当前运行状态与请求的投递模式推导其 `transcript`、`queued` 或 `steering`
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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
|
*/
|
|
@@ -1366,6 +1378,7 @@ window.__ModuleLoader__.load({
|
|
|
1366
1378
|
/** Keeps transient Assistant presentation behind one settlement-aware interface. */
|
|
1367
1379
|
var ClientAssistantStream = class {
|
|
1368
1380
|
activeAttempt;
|
|
1381
|
+
retainedAttempt;
|
|
1369
1382
|
pending = /* @__PURE__ */ new Map();
|
|
1370
1383
|
publishedSeqs = /* @__PURE__ */ new Set();
|
|
1371
1384
|
durableCursor = -1;
|
|
@@ -1380,6 +1393,7 @@ window.__ModuleLoader__.load({
|
|
|
1380
1393
|
this.pending.clear();
|
|
1381
1394
|
this.transientInGap = 0;
|
|
1382
1395
|
this.activeAttempt = void 0;
|
|
1396
|
+
this.retainedAttempt = void 0;
|
|
1383
1397
|
const opening = baseline?.activeAttempt;
|
|
1384
1398
|
if (opening !== void 0) this.activeAttempt = {
|
|
1385
1399
|
attemptId: opening.attemptId,
|
|
@@ -1430,13 +1444,15 @@ window.__ModuleLoader__.load({
|
|
|
1430
1444
|
}
|
|
1431
1445
|
/**
|
|
1432
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.
|
|
1433
1449
|
* @param frame - next Assistant stream frame received by the follow connection.
|
|
1434
1450
|
* @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
|
|
1435
1451
|
*/
|
|
1436
1452
|
acceptFrame(frame) {
|
|
1437
1453
|
switch (frame.type) {
|
|
1438
1454
|
case "start":
|
|
1439
|
-
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" };
|
|
1440
1456
|
this.pending.clear();
|
|
1441
1457
|
this.activeAttempt = {
|
|
1442
1458
|
attemptId: frame.attemptId,
|
|
@@ -1483,6 +1499,14 @@ window.__ModuleLoader__.load({
|
|
|
1483
1499
|
const entry = this.pending.get(frame.outcome.seq);
|
|
1484
1500
|
if (entry === void 0 || entry.event.type !== frame.outcome.eventType) return { type: "rebaseline" };
|
|
1485
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
|
+
}
|
|
1486
1510
|
this.publishedSeqs.add(entry.event.seq);
|
|
1487
1511
|
return {
|
|
1488
1512
|
type: "settlement",
|
|
@@ -1499,6 +1523,15 @@ window.__ModuleLoader__.load({
|
|
|
1499
1523
|
}
|
|
1500
1524
|
publish(entry) {
|
|
1501
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
|
+
}
|
|
1502
1535
|
return {
|
|
1503
1536
|
type: "publish",
|
|
1504
1537
|
entry
|
|
@@ -1516,6 +1549,20 @@ window.__ModuleLoader__.load({
|
|
|
1516
1549
|
asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq)
|
|
1517
1550
|
};
|
|
1518
1551
|
}
|
|
1552
|
+
const HISTORY_PAGE_OPTIONS = {
|
|
1553
|
+
maxMessages: 500,
|
|
1554
|
+
turnWindow: {
|
|
1555
|
+
minMessages: 50,
|
|
1556
|
+
minTurns: 2
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
const JUMP_PAGE_OPTIONS = {
|
|
1560
|
+
...HISTORY_PAGE_OPTIONS,
|
|
1561
|
+
turnWindow: {
|
|
1562
|
+
...HISTORY_PAGE_OPTIONS.turnWindow,
|
|
1563
|
+
minMessages: 200
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1519
1566
|
/**
|
|
1520
1567
|
* Owns a session's event window, lifecycle state, and observable
|
|
1521
1568
|
* snapshot. React bindings remain outside this data layer. Features see only
|
|
@@ -1816,7 +1863,7 @@ window.__ModuleLoader__.load({
|
|
|
1816
1863
|
this.openPromise = promise;
|
|
1817
1864
|
return promise;
|
|
1818
1865
|
}
|
|
1819
|
-
/**
|
|
1866
|
+
/** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
|
|
1820
1867
|
async loadOlder() {
|
|
1821
1868
|
if (this.openState !== "open" || !this.hasMore || this.loadingOlder) return;
|
|
1822
1869
|
const events = this.events;
|
|
@@ -1826,7 +1873,7 @@ window.__ModuleLoader__.load({
|
|
|
1826
1873
|
try {
|
|
1827
1874
|
await events.prepend({
|
|
1828
1875
|
beforeSeq: this.baseSeq,
|
|
1829
|
-
|
|
1876
|
+
...HISTORY_PAGE_OPTIONS
|
|
1830
1877
|
});
|
|
1831
1878
|
} catch (error) {
|
|
1832
1879
|
if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadOlder failed:", error);
|
|
@@ -1862,7 +1909,7 @@ window.__ModuleLoader__.load({
|
|
|
1862
1909
|
const before = pending.beforeSeq;
|
|
1863
1910
|
await events.prepend({
|
|
1864
1911
|
beforeSeq: before,
|
|
1865
|
-
|
|
1912
|
+
...JUMP_PAGE_OPTIONS
|
|
1866
1913
|
});
|
|
1867
1914
|
if (pending.beforeSeq >= before) return;
|
|
1868
1915
|
}
|
|
@@ -1981,7 +2028,8 @@ window.__ModuleLoader__.load({
|
|
|
1981
2028
|
*/
|
|
1982
2029
|
async dispose() {
|
|
1983
2030
|
this.stopObservingInbox();
|
|
1984
|
-
for (const requestId of [...this.submissionSettlements
|
|
2031
|
+
for (const [requestId, settlement] of [...this.submissionSettlements]) if (settlement.admitted !== void 0) this.scheduleObservedRetirement(requestId, settlement.admitted);
|
|
2032
|
+
else this.retireFailedSubmission(requestId);
|
|
1985
2033
|
this.openGeneration++;
|
|
1986
2034
|
const events = this.events;
|
|
1987
2035
|
this.events = void 0;
|
|
@@ -2003,7 +2051,7 @@ window.__ModuleLoader__.load({
|
|
|
2003
2051
|
});
|
|
2004
2052
|
this.events = events;
|
|
2005
2053
|
try {
|
|
2006
|
-
await events.open(
|
|
2054
|
+
await events.open(HISTORY_PAGE_OPTIONS);
|
|
2007
2055
|
if (generation !== this.openGeneration || this.events !== events) return;
|
|
2008
2056
|
this.openState = "open";
|
|
2009
2057
|
} catch (error) {
|
|
@@ -2050,7 +2098,7 @@ window.__ModuleLoader__.load({
|
|
|
2050
2098
|
for (const entry of visible) this.observeSubmissionEvent(entry.event);
|
|
2051
2099
|
if (projections !== void 0) {
|
|
2052
2100
|
const inbox = projections.values.inbox;
|
|
2053
|
-
|
|
2101
|
+
for (const target of ["next-turn", "next-step"]) this.observeSubmissionInsertions(target, inbox?.[target] ?? [], 0, projections.asOfSeq);
|
|
2054
2102
|
}
|
|
2055
2103
|
this.notifier.markDirty();
|
|
2056
2104
|
}
|
|
@@ -2073,8 +2121,11 @@ window.__ModuleLoader__.load({
|
|
|
2073
2121
|
this.notifier.markDirty();
|
|
2074
2122
|
return;
|
|
2075
2123
|
}
|
|
2076
|
-
if (result?.type === "publish"
|
|
2077
|
-
|
|
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") {
|
|
2078
2129
|
this.eventSource.append(result.entry);
|
|
2079
2130
|
this.notifier.markDirty();
|
|
2080
2131
|
}
|
|
@@ -2106,36 +2157,35 @@ window.__ModuleLoader__.load({
|
|
|
2106
2157
|
if (this.submissionSettlements.size === 0) return;
|
|
2107
2158
|
if (event.type === "agent/inbox/spliced") {
|
|
2108
2159
|
const { target, start, removedCount = 0, inserted, outcome } = event.data;
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
this.observeSteeringInsertions(inserted, start, event.seq);
|
|
2160
|
+
for (const [requestId, settlement] of this.submissionSettlements) {
|
|
2161
|
+
const receipt = settlement.receipt;
|
|
2162
|
+
if (receipt?.target !== target || receipt.index === null || receipt.seq >= event.seq) continue;
|
|
2163
|
+
const removed = receipt.index >= start && receipt.index < start + removedCount;
|
|
2164
|
+
if (removed && outcome === "canceled") this.retireFailedSubmission(requestId);
|
|
2165
|
+
else settlement.receipt = {
|
|
2166
|
+
...receipt,
|
|
2167
|
+
seq: event.seq,
|
|
2168
|
+
index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount
|
|
2169
|
+
};
|
|
2122
2170
|
}
|
|
2171
|
+
this.observeSubmissionInsertions(target, inserted, start, event.seq);
|
|
2123
2172
|
for (const message of inserted) this.observeSubmissionMessage(message, false);
|
|
2124
2173
|
return;
|
|
2125
2174
|
}
|
|
2126
2175
|
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);
|
|
2176
|
+
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
2177
|
return;
|
|
2129
2178
|
}
|
|
2130
2179
|
if (event.type === "user/message") this.observeSubmissionMessage(event.data, true);
|
|
2131
2180
|
}
|
|
2132
|
-
|
|
2181
|
+
observeSubmissionInsertions(target, messages, start, seq) {
|
|
2133
2182
|
for (const [index, message] of messages.entries()) {
|
|
2134
2183
|
const source = message.source;
|
|
2135
2184
|
if (source.kind !== "user" || !("rpcId" in source)) continue;
|
|
2136
2185
|
const settlement = this.submissionSettlements.get(source.rpcId);
|
|
2137
|
-
if (settlement
|
|
2186
|
+
if (settlement === void 0 || settlement.placement === "queued" || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue;
|
|
2138
2187
|
settlement.receipt = {
|
|
2188
|
+
target,
|
|
2139
2189
|
seq,
|
|
2140
2190
|
index: start + index,
|
|
2141
2191
|
attachments: attachmentRefsIn(message.content)
|
|
@@ -2145,15 +2195,34 @@ window.__ModuleLoader__.load({
|
|
|
2145
2195
|
observeSubmissionMessage(message, admitted) {
|
|
2146
2196
|
const source = message.source;
|
|
2147
2197
|
if (source.kind !== "user" || !("rpcId" in source)) return;
|
|
2148
|
-
|
|
2149
|
-
|
|
2198
|
+
const settlement = this.submissionSettlements.get(source.rpcId);
|
|
2199
|
+
if (settlement === void 0 || settlement.retiring) return;
|
|
2200
|
+
if (!admitted) {
|
|
2201
|
+
if (settlement.placement === "queued") this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
settlement.admitted = attachmentRefsIn(message.content);
|
|
2205
|
+
this.retireAdmittedSubmission(source.rpcId);
|
|
2206
|
+
}
|
|
2207
|
+
/** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
|
|
2208
|
+
retireAdmittedSubmission(requestId) {
|
|
2209
|
+
const settlement = this.submissionSettlements.get(requestId);
|
|
2210
|
+
if (settlement?.admitted === void 0) return;
|
|
2211
|
+
const receipt = settlement.receipt;
|
|
2212
|
+
if (receipt?.index === null && (this.projections.seqOf("inbox") ?? -1) < receipt.seq) return;
|
|
2213
|
+
this.scheduleObservedRetirement(requestId, settlement.admitted);
|
|
2150
2214
|
}
|
|
2151
|
-
/**
|
|
2215
|
+
/** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
|
|
2152
2216
|
observeSubmissionInbox() {
|
|
2153
2217
|
if (this.submissionSettlements.size === 0) return;
|
|
2154
2218
|
const inbox = this.projections.get("inbox");
|
|
2155
2219
|
if (inbox === void 0) return;
|
|
2156
|
-
|
|
2220
|
+
const seq = this.projections.seqOf("inbox");
|
|
2221
|
+
for (const target of ["next-turn", "next-step"]) {
|
|
2222
|
+
if (seq !== void 0) this.observeSubmissionInsertions(target, inbox[target], 0, seq);
|
|
2223
|
+
for (const message of inbox[target]) this.observeSubmissionMessage(message, false);
|
|
2224
|
+
}
|
|
2225
|
+
for (const requestId of this.submissionSettlements.keys()) this.retireAdmittedSubmission(requestId);
|
|
2157
2226
|
}
|
|
2158
2227
|
/**
|
|
2159
2228
|
* Latch one observed settlement and remove the echo an animation frame
|
|
@@ -2175,7 +2244,7 @@ window.__ModuleLoader__.load({
|
|
|
2175
2244
|
/** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
|
|
2176
2245
|
retireFailedSubmission(requestId) {
|
|
2177
2246
|
const settlement = this.submissionSettlements.get(requestId);
|
|
2178
|
-
if (settlement === void 0 || settlement.retiring) return;
|
|
2247
|
+
if (settlement === void 0 || settlement.retiring || settlement.admitted !== void 0) return;
|
|
2179
2248
|
settlement.retiring = true;
|
|
2180
2249
|
this.finishSubmission(requestId, { reason: "failed" });
|
|
2181
2250
|
}
|
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
|
-
|
|
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
|
-
|
|
1518
|
+
validateHistoryWindow(request);
|
|
1519
1519
|
}
|
|
1520
|
-
function
|
|
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
|
|
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;
|