@deepseek-ai/dsh-api-session-controller 0.1.2-alpha.2 → 0.1.2-alpha.4
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 +4 -2
- package/README.zh.md +4 -2
- package/lib/client.js +108 -14
- package/lib/index.js +49 -44
- package/lib/typert.host.js +113 -73
- package/lib/typert.remote-client.js +31 -31
- package/lib/types/agent.d.ts +3 -5
- package/lib/types/agent.js +5 -1
- package/lib/types/client/contract/session.d.ts +13 -2
- package/lib/types/client/contract/snapshot.d.ts +4 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/sessions/manager.d.ts +2 -2
- package/lib/types/client/sessions/manager.js +10 -5
- package/lib/types/client/sessions/projection-store.d.ts +4 -3
- package/lib/types/client/sessions/queue-mirror.js +3 -0
- package/lib/types/client/sessions/service.d.ts +1 -1
- package/lib/types/client/sessions/service.js +2 -1
- package/lib/types/client/sessions/session.d.ts +10 -2
- package/lib/types/client/sessions/session.js +78 -8
- package/lib/types/commands.js +16 -22
- package/lib/types/history.js +45 -24
- package/lib/types/index.d.ts +3 -5
- package/lib/types/index.js +5 -1
- package/lib/types/list.js +4 -1
- package/lib/types/types.d.ts +22 -3
- package/package.json +57 -64
- package/lib/invariant.js +0 -13
- package/lib/types/invariant.d.ts +0 -9
- package/lib/types/invariant.js +0 -12
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: b3d22a340fff6a49270de520a043a1c8dfdbf096
|
|
6
|
+
README.zh.md: 45590a53db17b32cc59f6107957c284b53ee8302
|
package/README.md
CHANGED
|
@@ -27,9 +27,9 @@ History pages and follow opening snapshots carry a discriminated `SessionHistory
|
|
|
27
27
|
|
|
28
28
|
Each endpoint states its activation policy. List, search, attachment, history pages, log following, skill discovery, and workspace-path opening can inspect persistence without activating an Agent; `canOpenWorkspacePath()` reports native-opening availability without addressing a Session. Queue mutation and cancellation require live state; model, rename, prompt, and file-reference operations may resolve or resume an ordinary Session. Create and fork are the only operations that create a new Agent directly. The skill catalog instead uses a live Agent when present or the recorded preset's standing scope when cold, so listing never starts an Agent.
|
|
29
29
|
|
|
30
|
-
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`, and `append` changes, and repairs reconnect or sequence gaps through a tail page. Ordinary records cover `[event.seq, event.seq]`; packed rows cover `[event.seq, event.seq + memberCount - 1]`. 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 queue, jobs, and projection state instead of treating transient values as durable events.
|
|
30
|
+
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`, and `append` 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. Ordinary records cover `[event.seq, event.seq]`; packed rows cover `[event.seq, event.seq + memberCount - 1]`. 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 queue, jobs, and projection state instead of treating transient values as durable events.
|
|
31
31
|
|
|
32
|
-
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 prompt's `requestId` is the correlation identity
|
|
32
|
+
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. Session derives each echo's `transcript`, `queued`, or `steering` placement from its current running state and the 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`, and queue occurrences project it as `SessionQueuedItem.rpcId`. An echo retires one animation frame after its durable event or queue occurrence is observed (the delay keeps it renderable until the replacement is ready), immediately when its identified prompt fails or is abandoned, and as failed on disposal; each retirement fires the registered `onRetire` callback exactly once. Echoes are Client memory only; reload and reconnect rebuild the conversation from durable events alone.
|
|
33
33
|
|
|
34
34
|
-----
|
|
35
35
|
|
|
@@ -72,3 +72,5 @@ No direct effect; model requests remain owned by the Agent and LLM packages.
|
|
|
72
72
|
None.
|
|
73
73
|
|
|
74
74
|
</details>
|
|
75
|
+
|
|
76
|
+
**Runtime invariant:** No companion is published. Every page and frame is checked against the addressed durable Session.
|
package/README.zh.md
CHANGED
|
@@ -27,9 +27,9 @@ kind: "package-reference"
|
|
|
27
27
|
|
|
28
28
|
每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页、日志跟随、skill 发现和工作区路径打开可以在不激活 Agent 的情况下检查 persistence;`canOpenWorkspacePath()` 无需指定 Session 即可报告原生打开能力。queue 变更与取消要求 live 状态;模型、重命名、prompt 和文件引用操作可以解析或恢复普通 Session。只有 create 与 fork 会直接创建新 Agent。skill 目录则优先使用已有 live Agent,否则使用所记录 preset 的常驻 scope,因此列表查询绝不会启动 Agent。
|
|
29
29
|
|
|
30
|
-
Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend` 和 `append` 变更,并通过 tail page 修复重连或 seq
|
|
30
|
+
Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend` 和 `append` 变更,并通过 tail page 修复重连或 seq 缺口。向后分页有两个动词:`loadOlder()` 拉一页 50 条 message,而 `loadThrough(seq)`——轮次跳转加载器——按 200 条 message 一页循环拉取直到窗口覆盖目标 seq,重复调用会下调共享目标,遇到无进展的页即停止,忙碌状态复用同一个 `loadingOlder` 快照位。普通 record 覆盖 `[event.seq, event.seq]`,packed row 覆盖 `[event.seq, event.seq + memberCount - 1]`。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作 durable event。
|
|
31
31
|
|
|
32
|
-
Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与 prompt 之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。prompt 的 `requestId`
|
|
32
|
+
Session 对象还承载本地提交回显:`session.beginSubmission` 在调用方序列化与 prompt 之前,同步把一条回显写入 `SessionSnapshot.pendingSubmissions`,会话 UI 因此能在点击提交的当帧显示消息。Session 根据当前运行状态与请求的投递模式推导每条回显的 `transcript`、`queued` 或 `steering` 位置,并在序列化期间保留该位置。prompt 的 `requestId` 是关联标识:Host 把它回显为 durable user source 的 `rpcId`,queue occurrence 也把它投影为 `SessionQueuedItem.rpcId`。回显在观察到其 durable event 或 queue occurrence 后延迟一个动画帧退休,该延迟保证替代内容就绪前回显仍可渲染;带标识的 prompt 失败或被放弃时立即退休,销毁时按 failed 退休;每次退休恰好触发一次注册的 `onRetire` 回调。回显只存在于 Client 内存;刷新与重连只从 durable event 重建会话。
|
|
33
33
|
|
|
34
34
|
-----
|
|
35
35
|
|
|
@@ -72,3 +72,5 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用
|
|
|
72
72
|
无。
|
|
73
73
|
|
|
74
74
|
</details>
|
|
75
|
+
|
|
76
|
+
**运行时不变式:** 不发布伴生入口。每个分页与帧都会对照其指向的持久 Session 校验。
|
package/lib/client.js
CHANGED
|
@@ -176,6 +176,36 @@ window.__ModuleLoader__.load({
|
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
178
|
//#endregion
|
|
179
|
+
//#region ../../util/brand/lib/index.js
|
|
180
|
+
/**
|
|
181
|
+
* Apply a compile-time number brand without changing the value.
|
|
182
|
+
* @param value - number admitted by the domain that owns the target brand.
|
|
183
|
+
* @returns the same number with the requested compile-time brand.
|
|
184
|
+
*/
|
|
185
|
+
function brandNumber(value) {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region ../../core/session/lib/types/types.js
|
|
190
|
+
/**
|
|
191
|
+
* Admit a numeric value as an existing Session event position.
|
|
192
|
+
* @param value - non-negative safe integer admitted by the owning log operation.
|
|
193
|
+
* @returns the same number with the Session-sequence brand.
|
|
194
|
+
*/
|
|
195
|
+
function SessionSeq(value) {
|
|
196
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`SessionSeq must be a non-negative safe integer, got ${String(value)}`);
|
|
197
|
+
return brandNumber(value);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Admit a numeric value as a Session log offset.
|
|
201
|
+
* @param value - non-negative safe integer used as a gap or prefix length.
|
|
202
|
+
* @returns the same number with the Session-log-offset brand.
|
|
203
|
+
*/
|
|
204
|
+
function SessionLogOffset(value) {
|
|
205
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`SessionLogOffset must be a non-negative safe integer, got ${String(value)}`);
|
|
206
|
+
return brandNumber(value);
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
179
209
|
//#region ../../util/workspace-path/lib/index.js
|
|
180
210
|
/**
|
|
181
211
|
* Read the final non-empty segment of a Workspace path for display.
|
|
@@ -671,7 +701,7 @@ window.__ModuleLoader__.load({
|
|
|
671
701
|
//#region lib/types/client/sessions/queue-mirror.js
|
|
672
702
|
const QUEUE_PREVIEW_CHARS = 200;
|
|
673
703
|
function previewOf(content) {
|
|
674
|
-
const flat = content.map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
|
|
704
|
+
const flat = content.filter((block) => block.type !== "image").map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
|
|
675
705
|
const chars = Array.from(flat);
|
|
676
706
|
return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}…` : flat;
|
|
677
707
|
}
|
|
@@ -721,6 +751,14 @@ window.__ModuleLoader__.load({
|
|
|
721
751
|
return true;
|
|
722
752
|
}
|
|
723
753
|
};
|
|
754
|
+
//#endregion
|
|
755
|
+
//#region lib/types/client/sessions/session.js
|
|
756
|
+
function projectionsBaseline(value) {
|
|
757
|
+
return {
|
|
758
|
+
...value,
|
|
759
|
+
asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
724
762
|
/**
|
|
725
763
|
* Owns a session's event window, lifecycle state, and observable
|
|
726
764
|
* snapshot. React bindings remain outside this data layer. Features see only
|
|
@@ -731,7 +769,7 @@ window.__ModuleLoader__.load({
|
|
|
731
769
|
sessionId;
|
|
732
770
|
remote;
|
|
733
771
|
options;
|
|
734
|
-
baseSeq = 0;
|
|
772
|
+
baseSeq = SessionLogOffset(0);
|
|
735
773
|
hasMore = false;
|
|
736
774
|
openState = "cold";
|
|
737
775
|
openError = null;
|
|
@@ -740,6 +778,10 @@ window.__ModuleLoader__.load({
|
|
|
740
778
|
* passes drop all writes once the generation moves on. */
|
|
741
779
|
openGeneration = 0;
|
|
742
780
|
loadingOlder = false;
|
|
781
|
+
/** Shared low-water target of the running jump loop; null when no jump is paging. */
|
|
782
|
+
jumpTargetSeq = null;
|
|
783
|
+
/** The running jump loop's completion, shared by retargeting callers. */
|
|
784
|
+
jumpPromise = null;
|
|
743
785
|
/** Authoritative stream-only inbox snapshot; pending work never hits history. */
|
|
744
786
|
queueMirror = new SessionQueueMirror();
|
|
745
787
|
running = false;
|
|
@@ -834,6 +876,7 @@ window.__ModuleLoader__.load({
|
|
|
834
876
|
const requestId = randomUUID();
|
|
835
877
|
this.pendingSubmissions = [...this.pendingSubmissions, {
|
|
836
878
|
requestId,
|
|
879
|
+
placement: this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript",
|
|
837
880
|
time: Date.now(),
|
|
838
881
|
text: input.text,
|
|
839
882
|
images: input.images
|
|
@@ -967,8 +1010,16 @@ window.__ModuleLoader__.load({
|
|
|
967
1010
|
sessionId: this.sessionId,
|
|
968
1011
|
title
|
|
969
1012
|
});
|
|
970
|
-
if (result.ok)
|
|
971
|
-
|
|
1013
|
+
if (!result.ok) return result;
|
|
1014
|
+
const seq = SessionSeq(result.value.seq);
|
|
1015
|
+
this.projections.apply("title", result.value.title, seq);
|
|
1016
|
+
return {
|
|
1017
|
+
ok: true,
|
|
1018
|
+
value: {
|
|
1019
|
+
title: result.value.title,
|
|
1020
|
+
seq
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
972
1023
|
}
|
|
973
1024
|
/**
|
|
974
1025
|
* Execute one slash-command line against this session's agent — pure
|
|
@@ -1014,6 +1065,42 @@ window.__ModuleLoader__.load({
|
|
|
1014
1065
|
this.notifier.markDirty();
|
|
1015
1066
|
}
|
|
1016
1067
|
}
|
|
1068
|
+
/** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
|
|
1069
|
+
loadThrough(seq) {
|
|
1070
|
+
if (this.openState !== "open" || !this.hasMore || this.baseSeq <= seq) return Promise.resolve();
|
|
1071
|
+
if (this.jumpPromise !== null) {
|
|
1072
|
+
this.jumpTargetSeq = SessionSeq(Math.min(this.jumpTargetSeq ?? seq, seq));
|
|
1073
|
+
return this.jumpPromise;
|
|
1074
|
+
}
|
|
1075
|
+
if (this.loadingOlder) return Promise.resolve();
|
|
1076
|
+
this.jumpTargetSeq = seq;
|
|
1077
|
+
this.loadingOlder = true;
|
|
1078
|
+
this.notifier.markDirty();
|
|
1079
|
+
const generation = this.openGeneration;
|
|
1080
|
+
this.jumpPromise = (async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
|
|
1083
|
+
if (generation !== this.openGeneration) return;
|
|
1084
|
+
const events = this.events;
|
|
1085
|
+
if (events === void 0) return;
|
|
1086
|
+
const before = this.baseSeq;
|
|
1087
|
+
await events.prepend({
|
|
1088
|
+
beforeSeq: this.baseSeq,
|
|
1089
|
+
maxMessages: 200
|
|
1090
|
+
});
|
|
1091
|
+
if (this.baseSeq >= before) return;
|
|
1092
|
+
}
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadThrough failed:", error);
|
|
1095
|
+
} finally {
|
|
1096
|
+
this.jumpTargetSeq = null;
|
|
1097
|
+
this.jumpPromise = null;
|
|
1098
|
+
this.loadingOlder = false;
|
|
1099
|
+
this.notifier.markDirty();
|
|
1100
|
+
}
|
|
1101
|
+
})();
|
|
1102
|
+
return this.jumpPromise;
|
|
1103
|
+
}
|
|
1017
1104
|
/** Rebuild an opened history source after address replacement.
|
|
1018
1105
|
* Invalidates any in-flight open first; queue state belongs to the independently
|
|
1019
1106
|
* reconnecting control stream and remains untouched. */
|
|
@@ -1026,7 +1113,7 @@ window.__ModuleLoader__.load({
|
|
|
1026
1113
|
this.openPromise = null;
|
|
1027
1114
|
this.openState = "cold";
|
|
1028
1115
|
this.openError = null;
|
|
1029
|
-
this.baseSeq = 0;
|
|
1116
|
+
this.baseSeq = SessionLogOffset(0);
|
|
1030
1117
|
this.notifier.markDirty();
|
|
1031
1118
|
await this.open();
|
|
1032
1119
|
}
|
|
@@ -1170,7 +1257,7 @@ window.__ModuleLoader__.load({
|
|
|
1170
1257
|
acceptEventChange(change) {
|
|
1171
1258
|
switch (change.type) {
|
|
1172
1259
|
case "replace":
|
|
1173
|
-
this.installWindow(change.entries, change.hasMore, change.page.projections);
|
|
1260
|
+
this.installWindow(change.entries, change.hasMore, change.page.projections === void 0 ? void 0 : projectionsBaseline(change.page.projections));
|
|
1174
1261
|
return;
|
|
1175
1262
|
case "prepend":
|
|
1176
1263
|
this.prependWindow(change.entries, change.hasMore);
|
|
@@ -1180,7 +1267,7 @@ window.__ModuleLoader__.load({
|
|
|
1180
1267
|
}
|
|
1181
1268
|
/** Replace the complete contiguous window and apply page-owned projection metadata. */
|
|
1182
1269
|
installWindow(entries, hasMore, projections) {
|
|
1183
|
-
this.baseSeq = entries[0]?.event.seq ?? 0;
|
|
1270
|
+
this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
|
|
1184
1271
|
this.hasMore = hasMore;
|
|
1185
1272
|
if (entries.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
|
|
1186
1273
|
if (projections !== void 0) this.projections.seed(projections);
|
|
@@ -1190,7 +1277,7 @@ window.__ModuleLoader__.load({
|
|
|
1190
1277
|
}
|
|
1191
1278
|
/** Prepend one stream-validated history page. */
|
|
1192
1279
|
prependWindow(entries, hasMore) {
|
|
1193
|
-
this.baseSeq = entries[0]
|
|
1280
|
+
this.baseSeq = entries[0] === void 0 ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
|
|
1194
1281
|
this.hasMore = hasMore;
|
|
1195
1282
|
this.eventSource.prepend(entries, hasMore);
|
|
1196
1283
|
}
|
|
@@ -1315,6 +1402,9 @@ window.__ModuleLoader__.load({
|
|
|
1315
1402
|
}
|
|
1316
1403
|
//#endregion
|
|
1317
1404
|
//#region lib/types/client/sessions/manager.js
|
|
1405
|
+
function sessionSeqCursor(value) {
|
|
1406
|
+
return value === -1 ? -1 : SessionSeq(value);
|
|
1407
|
+
}
|
|
1318
1408
|
function catalogAvailability(parentAvailable) {
|
|
1319
1409
|
return parentAvailable === void 0 ? {} : { parentAvailable };
|
|
1320
1410
|
}
|
|
@@ -1648,7 +1738,7 @@ window.__ModuleLoader__.load({
|
|
|
1648
1738
|
if (block === void 0) continue;
|
|
1649
1739
|
const store = this.projectionStore(s.sessionId);
|
|
1650
1740
|
const values = block.values;
|
|
1651
|
-
for (const key of Object.keys(values)) store.apply(key, values[key], block.asOfSeq);
|
|
1741
|
+
for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq));
|
|
1652
1742
|
}
|
|
1653
1743
|
} else {
|
|
1654
1744
|
this.listState = "error";
|
|
@@ -1799,7 +1889,7 @@ window.__ModuleLoader__.load({
|
|
|
1799
1889
|
return;
|
|
1800
1890
|
}
|
|
1801
1891
|
if (frame.type === "projection") {
|
|
1802
|
-
this.projectionStore(frame.sessionId).apply(frame.key, frame.value, frame.seq);
|
|
1892
|
+
this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq));
|
|
1803
1893
|
this.notifier.markDirty();
|
|
1804
1894
|
return;
|
|
1805
1895
|
}
|
|
@@ -1819,8 +1909,12 @@ window.__ModuleLoader__.load({
|
|
|
1819
1909
|
for (const [sessionId, jobs] of Object.entries(baseline.jobs)) if (jobs.length > 0) this.jobsBySession.set(sessionId, jobs);
|
|
1820
1910
|
for (const [sessionId, block] of Object.entries(baseline.projections)) {
|
|
1821
1911
|
const store = this.projectionStore(sessionId);
|
|
1822
|
-
|
|
1823
|
-
store.
|
|
1912
|
+
const asOfSeq = sessionSeqCursor(block.asOfSeq);
|
|
1913
|
+
store.truncate(asOfSeq);
|
|
1914
|
+
store.seed({
|
|
1915
|
+
...block,
|
|
1916
|
+
asOfSeq
|
|
1917
|
+
});
|
|
1824
1918
|
}
|
|
1825
1919
|
for (const [sessionId, session] of this.sessions) session.replaceControl(this.queues.get(sessionId) ?? []);
|
|
1826
1920
|
this.notifier.markDirty();
|
|
@@ -1835,7 +1929,7 @@ window.__ModuleLoader__.load({
|
|
|
1835
1929
|
const projections = summary.projections;
|
|
1836
1930
|
if (projections !== void 0) {
|
|
1837
1931
|
const store = this.projectionStore(summary.sessionId);
|
|
1838
|
-
for (const [key, value] of Object.entries(projections.values)) store.apply(key, value, projections.asOfSeq);
|
|
1932
|
+
for (const [key, value] of Object.entries(projections.values)) store.apply(key, value, sessionSeqCursor(projections.asOfSeq));
|
|
1839
1933
|
}
|
|
1840
1934
|
if (summary.origin === "subagent" && summary.parentSessionId !== void 0) this.markCatalogParentExpandable(summary.parentSessionId);
|
|
1841
1935
|
if (summary.parentSessionId !== void 0 && (this.selected === summary.parentSessionId || this.openCatalogs.has(summary.parentSessionId))) this.scheduleCatalogRefresh(summary.parentSessionId);
|
|
@@ -2359,7 +2453,7 @@ window.__ModuleLoader__.load({
|
|
|
2359
2453
|
const sourceTitle = opts.increaseTitle ? this.list.getSnapshot().byId[opts.sessionId]?.title : void 0;
|
|
2360
2454
|
const result = await this.manager.fork({
|
|
2361
2455
|
sessionId: opts.sessionId,
|
|
2362
|
-
...opts.atSeq === void 0 ? {} : { atSeq: Math.floor(opts.atSeq) }
|
|
2456
|
+
...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }
|
|
2363
2457
|
});
|
|
2364
2458
|
if (!result.ok) throw new SessionForkError(result.error, opts.sessionId);
|
|
2365
2459
|
this.projectList();
|
package/lib/index.js
CHANGED
|
@@ -7,11 +7,11 @@ import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
|
7
7
|
import { SessionQueryError } from "@deepseek-ai/dsh-session-query";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
10
|
-
import { AttachmentError,
|
|
10
|
+
import { AttachmentError, admitPromptContent } from "@deepseek-ai/dsh-attachment";
|
|
11
|
+
import { SessionLogOffset, SessionSeq, isAppendSurfaceEvent } from "@deepseek-ai/dsh-session";
|
|
11
12
|
import { SessionTitleInvalidError } from "@deepseek-ai/dsh-session-title";
|
|
12
13
|
import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
|
|
13
14
|
import { Deque } from "@deepseek-ai/dsh-deque";
|
|
14
|
-
import { isAppendSurfaceEvent } from "@deepseek-ai/dsh-session";
|
|
15
15
|
import { isChunkRow, packChunkRuns } from "@deepseek-ai/dsh-session/chunk-rows";
|
|
16
16
|
import { z as z$1 } from "zod";
|
|
17
17
|
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
@@ -154,6 +154,7 @@ async function inspectApiSession(ctx, sessionId, signal) {
|
|
|
154
154
|
if (observation.header.cwd === void 0) throw new ApiSessionNotFound(`session "${sessionId}" not found`);
|
|
155
155
|
return {
|
|
156
156
|
meta: observation.header,
|
|
157
|
+
inheritedEventCount: observation.inheritedEventCount,
|
|
157
158
|
events: [...observation.events]
|
|
158
159
|
};
|
|
159
160
|
} catch (e_1) {
|
|
@@ -658,7 +659,12 @@ var SessionCommandController = class {
|
|
|
658
659
|
hasError: false
|
|
659
660
|
};
|
|
660
661
|
try {
|
|
661
|
-
|
|
662
|
+
let atSeq;
|
|
663
|
+
try {
|
|
664
|
+
atSeq = request.atSeq === void 0 ? void 0 : SessionSeq(request.atSeq);
|
|
665
|
+
} catch {
|
|
666
|
+
throw new RemoteError("gateway/bad-request", "atSeq must be a non-negative safe integer", {});
|
|
667
|
+
}
|
|
662
668
|
let observed;
|
|
663
669
|
try {
|
|
664
670
|
observed = await this.ctx.sessionQuery.observeSession(request.sessionId);
|
|
@@ -668,11 +674,10 @@ var SessionCommandController = class {
|
|
|
668
674
|
}
|
|
669
675
|
const source = __addDisposableResource$4(env_1, observed, false);
|
|
670
676
|
const lastSeq = source.events.at(-1)?.seq ?? -1;
|
|
671
|
-
const atSeq = request.atSeq;
|
|
672
677
|
const boundary = (atSeq === void 0 ? void 0 : source.events.find((event) => event.type === "turn/end" && event.seq >= atSeq)) ?? (atSeq === void 0 || atSeq > lastSeq ? source.events.findLast((event) => event.type === "turn/end") : void 0);
|
|
673
678
|
if (boundary === void 0) throw new RemoteError("session/fork-unavailable", atSeq !== void 0 && atSeq <= lastSeq ? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}` : `session "${request.sessionId}" has no completed turn to fork from`, { sessionId: request.sessionId });
|
|
674
|
-
let cut = boundary.seq + 1;
|
|
675
|
-
while (cut < source.events.length && source.events[cut]?.type !== "turn/start") cut
|
|
679
|
+
let cut = SessionLogOffset(boundary.seq + 1);
|
|
680
|
+
while (cut < source.events.length && source.events[cut]?.type !== "turn/start") cut = SessionLogOffset(cut + 1);
|
|
676
681
|
let workspace;
|
|
677
682
|
try {
|
|
678
683
|
workspace = await this.forkWorkspace(source.header);
|
|
@@ -686,10 +691,11 @@ var SessionCommandController = class {
|
|
|
686
691
|
await this.ctx.agents.create({
|
|
687
692
|
sessionId: childId,
|
|
688
693
|
seed: source.events.slice(0, cut),
|
|
694
|
+
inheritedEventCount: cut,
|
|
689
695
|
meta: {
|
|
690
696
|
...source.header.cwd === void 0 ? {} : { cwd: source.header.cwd },
|
|
691
697
|
parentSession: source.header.id,
|
|
692
|
-
|
|
698
|
+
isSeeded: true,
|
|
693
699
|
...composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset }
|
|
694
700
|
},
|
|
695
701
|
agentOptions: {
|
|
@@ -745,7 +751,7 @@ var SessionCommandController = class {
|
|
|
745
751
|
if (model.inputModalities !== void 0 && !model.inputModalities.includes("image")) throw new RemoteError("session/attachment-invalid", `Model "${current.model}" does not support image input.`, { reason: "MODEL_DOES_NOT_SUPPORT_IMAGES" });
|
|
746
752
|
}
|
|
747
753
|
const message = createUserMessage({
|
|
748
|
-
content: await
|
|
754
|
+
content: await admitPromptContent(this.ctx.attachments, request.content),
|
|
749
755
|
source
|
|
750
756
|
});
|
|
751
757
|
if (request.mode === "steer") agent.steer(message);
|
|
@@ -854,7 +860,7 @@ var SessionCommandController = class {
|
|
|
854
860
|
if (attached !== void 0) return {
|
|
855
861
|
id: attached.id,
|
|
856
862
|
header: attached.header,
|
|
857
|
-
events:
|
|
863
|
+
events: attached.snapshotEvents()
|
|
858
864
|
};
|
|
859
865
|
const inspected = await inspectApiSession(this.ctx, sessionId);
|
|
860
866
|
return {
|
|
@@ -874,21 +880,6 @@ var SessionCommandController = class {
|
|
|
874
880
|
}
|
|
875
881
|
}
|
|
876
882
|
};
|
|
877
|
-
async function durablePromptContent(ctx, content) {
|
|
878
|
-
if (content.every((part) => part.type === "text")) return content.map((part) => ({
|
|
879
|
-
type: "text",
|
|
880
|
-
text: part.text
|
|
881
|
-
}));
|
|
882
|
-
const refs = await admitEncodedImages(ctx.attachments, content.filter((part) => part.type === "image"));
|
|
883
|
-
let next = 0;
|
|
884
|
-
return content.map((part) => part.type === "text" ? {
|
|
885
|
-
type: "text",
|
|
886
|
-
text: part.text
|
|
887
|
-
} : {
|
|
888
|
-
type: "image",
|
|
889
|
-
attachment: refs[next++]
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
883
|
function imageBlockIn(content, match) {
|
|
893
884
|
if (!Array.isArray(content)) return void 0;
|
|
894
885
|
for (const value of content) {
|
|
@@ -1218,14 +1209,16 @@ var SessionHistoryController = class {
|
|
|
1218
1209
|
};
|
|
1219
1210
|
try {
|
|
1220
1211
|
validatePageRequest(request);
|
|
1212
|
+
const throughSeq = request.throughSeq === -1 ? -1 : SessionSeq(request.throughSeq);
|
|
1213
|
+
const beforeSeq = request.beforeSeq === void 0 ? void 0 : SessionLogOffset(request.beforeSeq);
|
|
1221
1214
|
const source = __addDisposableResource$3(env_1, await this.sourceFor(request.address, signal, false), false);
|
|
1222
1215
|
signal.throwIfAborted();
|
|
1223
1216
|
const sourceLog = source.events;
|
|
1224
1217
|
const sourceCursor = sourceLog.at(-1)?.seq ?? -1;
|
|
1225
|
-
if (
|
|
1218
|
+
if (throughSeq > sourceCursor) throw new RemoteError("gateway/bad-request", `session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`, {});
|
|
1226
1219
|
/* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
|
|
1227
|
-
if (
|
|
1228
|
-
const page = paginate(sourceLog,
|
|
1220
|
+
if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) throw new RemoteError("gateway/internal", `session log does not contain through seq ${String(throughSeq)}`, {});
|
|
1221
|
+
const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
|
|
1229
1222
|
return {
|
|
1230
1223
|
records: pageRecords(page.events),
|
|
1231
1224
|
hasMore: page.hasMore
|
|
@@ -1268,7 +1261,7 @@ var SessionHistoryController = class {
|
|
|
1268
1261
|
}, { global: true });
|
|
1269
1262
|
const disposeCreated = this.ctx.on("session/created", (session) => {
|
|
1270
1263
|
if (session.id !== target) return;
|
|
1271
|
-
const suffix = session.
|
|
1264
|
+
const suffix = session.snapshotEvents(snapshotCursor === void 0 ? session.firstLiveSeq : SessionLogOffset(snapshotCursor + 1));
|
|
1272
1265
|
for (let index = suffix.length - 1; index >= 0; index -= 1) buffered.pushFront(suffix[index]);
|
|
1273
1266
|
notify();
|
|
1274
1267
|
}, { global: true });
|
|
@@ -1291,7 +1284,7 @@ var SessionHistoryController = class {
|
|
|
1291
1284
|
const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
|
|
1292
1285
|
yield {
|
|
1293
1286
|
type: "snapshot",
|
|
1294
|
-
header: source.header,
|
|
1287
|
+
header: wireHeader(source.header, source.inheritedEventCount),
|
|
1295
1288
|
cursor,
|
|
1296
1289
|
records: pageRecords(page.events),
|
|
1297
1290
|
hasMore: page.hasMore,
|
|
@@ -1309,7 +1302,7 @@ var SessionHistoryController = class {
|
|
|
1309
1302
|
throw error;
|
|
1310
1303
|
}
|
|
1311
1304
|
}
|
|
1312
|
-
let
|
|
1305
|
+
let nextOffset = SessionLogOffset(cursor + 1);
|
|
1313
1306
|
while (!follower.closed && !signal.aborted) {
|
|
1314
1307
|
const item = buffered.popFront();
|
|
1315
1308
|
if (item === void 0) {
|
|
@@ -1318,9 +1311,10 @@ var SessionHistoryController = class {
|
|
|
1318
1311
|
});
|
|
1319
1312
|
continue;
|
|
1320
1313
|
}
|
|
1321
|
-
|
|
1322
|
-
if (item.seq
|
|
1323
|
-
|
|
1314
|
+
const expectedSeq = SessionSeq(nextOffset);
|
|
1315
|
+
if (item.seq < expectedSeq) continue;
|
|
1316
|
+
if (item.seq !== expectedSeq) throw new RemoteError("gateway/internal", `session event stream skipped seq ${String(expectedSeq)}`, {});
|
|
1317
|
+
nextOffset = SessionLogOffset(nextOffset + 1);
|
|
1324
1318
|
yield entryFor(item);
|
|
1325
1319
|
}
|
|
1326
1320
|
} catch (e_2) {
|
|
@@ -1348,7 +1342,7 @@ var SessionHistoryController = class {
|
|
|
1348
1342
|
rejectNotFound(address);
|
|
1349
1343
|
}
|
|
1350
1344
|
try {
|
|
1351
|
-
validateAddress(address, observation.header, observation.projections);
|
|
1345
|
+
validateAddress(address, observation.header, observation.inheritedEventCount, observation.projections);
|
|
1352
1346
|
} catch (error) {
|
|
1353
1347
|
observation[Symbol.dispose]();
|
|
1354
1348
|
throw error;
|
|
@@ -1367,8 +1361,8 @@ function projectionBlock(snapshot) {
|
|
|
1367
1361
|
};
|
|
1368
1362
|
}
|
|
1369
1363
|
function validatePageRequest(request) {
|
|
1370
|
-
if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1) throw new RemoteError("gateway/bad-request", "throughSeq must be an integer greater than or equal to -1", {});
|
|
1371
|
-
if (request.beforeSeq !== void 0 && (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0)) throw new RemoteError("gateway/bad-request", "beforeSeq must be a non-negative safe integer", {});
|
|
1364
|
+
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", {});
|
|
1365
|
+
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", {});
|
|
1372
1366
|
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", {});
|
|
1373
1367
|
}
|
|
1374
1368
|
function validateFollowRequest(request) {
|
|
@@ -1377,7 +1371,7 @@ function validateFollowRequest(request) {
|
|
|
1377
1371
|
function addressId(address) {
|
|
1378
1372
|
return address.kind === "session" ? address.sessionId : address.childSessionId;
|
|
1379
1373
|
}
|
|
1380
|
-
function validateAddress(address, header, projections) {
|
|
1374
|
+
function validateAddress(address, header, inheritedEventCount, projections) {
|
|
1381
1375
|
if (address.kind === "session") {
|
|
1382
1376
|
if (header.origin === "subagent") throw new RemoteError("session/agent-busy", "subagent Sessions require their durable parent address", { reason: "use subagent delivery for this child session" });
|
|
1383
1377
|
return;
|
|
@@ -1389,7 +1383,7 @@ function validateAddress(address, header, projections) {
|
|
|
1389
1383
|
childSessionId: address.childSessionId,
|
|
1390
1384
|
reason: "corrupt"
|
|
1391
1385
|
});
|
|
1392
|
-
if (identity === void 0 || identity.seq <
|
|
1386
|
+
if (identity === void 0 || identity.seq < inheritedEventCount) throw new RemoteError("subagent/catalog-diagnostic", "subagent descriptor is unavailable", {
|
|
1393
1387
|
parentSessionId: address.parentSessionId,
|
|
1394
1388
|
childSessionId: address.childSessionId,
|
|
1395
1389
|
reason: "unsupported"
|
|
@@ -1404,18 +1398,20 @@ function rejectNotFound(address) {
|
|
|
1404
1398
|
});
|
|
1405
1399
|
}
|
|
1406
1400
|
function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
|
|
1407
|
-
const end = Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1);
|
|
1401
|
+
const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
|
|
1408
1402
|
let count = 0;
|
|
1409
|
-
let cut = 0;
|
|
1403
|
+
let cut = SessionLogOffset(0);
|
|
1410
1404
|
for (let index = end - 1; index >= 0; index--) {
|
|
1411
1405
|
const event = events[index];
|
|
1412
1406
|
if (!MESSAGE_TYPES$1.has(event.type) || !isAppendSurfaceEvent(event)) continue;
|
|
1413
1407
|
count++;
|
|
1414
1408
|
const sources = event.sourceEventSeqs;
|
|
1415
1409
|
let groupStart = event.seq;
|
|
1416
|
-
if (sources !== void 0)
|
|
1410
|
+
if (sources !== void 0) {
|
|
1411
|
+
for (const source of sources) if (source < groupStart) groupStart = source;
|
|
1412
|
+
}
|
|
1417
1413
|
if (count >= maxMessages) {
|
|
1418
|
-
cut = groupStart;
|
|
1414
|
+
cut = SessionLogOffset(groupStart);
|
|
1419
1415
|
break;
|
|
1420
1416
|
}
|
|
1421
1417
|
}
|
|
@@ -1424,6 +1420,14 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
|
|
|
1424
1420
|
hasMore: cut > 0
|
|
1425
1421
|
};
|
|
1426
1422
|
}
|
|
1423
|
+
/** Translate logical Session metadata to the unchanged v0 browser wire. */
|
|
1424
|
+
function wireHeader(header, inheritedEventCount) {
|
|
1425
|
+
const { isSeeded, ...wire } = header;
|
|
1426
|
+
return {
|
|
1427
|
+
...wire,
|
|
1428
|
+
...isSeeded ? { seedLength: inheritedEventCount } : {}
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1427
1431
|
function entryFor(event) {
|
|
1428
1432
|
return {
|
|
1429
1433
|
type: "event",
|
|
@@ -1889,7 +1893,7 @@ var ApiSessionList = class {
|
|
|
1889
1893
|
}
|
|
1890
1894
|
projectionsFor(header, session) {
|
|
1891
1895
|
try {
|
|
1892
|
-
const block = session === void 0 ? this.ctx.get("sessionProjectionCache")?.cachedSnapshot(header) : this.ctx.sessionProjections.cachedSnapshot(session);
|
|
1896
|
+
const block = session === void 0 ? header.isSeeded ? void 0 : this.ctx.get("sessionProjectionCache")?.cachedSnapshot(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
|
|
1893
1897
|
return block !== void 0 && Object.keys(block.values).length > 0 ? {
|
|
1894
1898
|
asOfSeq: block.asOfSeq,
|
|
1895
1899
|
values: block.values
|
|
@@ -2672,7 +2676,8 @@ let SessionController = (() => {
|
|
|
2672
2676
|
const attached = this.ctx.sessions.get(sessionId);
|
|
2673
2677
|
if (attached !== void 0) return Promise.resolve({
|
|
2674
2678
|
meta: attached.header,
|
|
2675
|
-
|
|
2679
|
+
inheritedEventCount: attached.inheritedEventCount,
|
|
2680
|
+
events: attached.snapshotEvents()
|
|
2676
2681
|
});
|
|
2677
2682
|
return inspectApiSession(this.ctx, sessionId, signal);
|
|
2678
2683
|
}
|