@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 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: 689635b83bce71e3aecfedf371685a24bede55ab
6
- README.zh.md: 68da7db60b38aca1b72c991005438ced45b348ea
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 the Host already 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 transcript node is), 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.
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 缺口。普通 record 覆盖 `[event.seq, event.seq]`,packed row 覆盖 `[event.seq, event.seq + memberCount - 1]`。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作 durable event。
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` 就是关联标识,Host 本就把它回显为 durable user source 的 `rpcId`,queue occurrence 也把它投影为 `SessionQueuedItem.rpcId`。回显在观察到其 durable event 或 queue occurrence 后延迟一个动画帧退休(该延迟保证 transcript 节点可渲染之前回显仍在),带标识的 prompt 失败或被放弃时立即退休,销毁时按 failed 退休;每次退休恰好触发一次注册的 `onRetire` 回调。回显只存在于 Client 内存,刷新与重连只从 durable event 重建会话。
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) this.projections.apply("title", result.value.title, result.value.seq);
971
- return result;
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]?.event.seq ?? this.baseSeq;
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
- store.truncate(block.asOfSeq);
1823
- store.seed(block);
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, admitEncodedImages } from "@deepseek-ai/dsh-attachment";
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
- if (request.atSeq !== void 0 && (!Number.isInteger(request.atSeq) || request.atSeq < 0)) throw new RemoteError("gateway/bad-request", "atSeq must be a non-negative integer", {});
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
- seedLength: cut,
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 durablePromptContent(this.ctx, request.content),
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: [...attached.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 (request.throughSeq > sourceCursor) throw new RemoteError("gateway/bad-request", `session page through seq ${String(request.throughSeq)} is past cursor ${String(sourceCursor)}`, {});
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 (request.throughSeq >= 0 && sourceLog[request.throughSeq]?.seq !== request.throughSeq) throw new RemoteError("gateway/internal", `session log does not contain through seq ${String(request.throughSeq)}`, {});
1228
- const page = paginate(sourceLog, request.beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, request.throughSeq);
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.events.slice(snapshotCursor === void 0 ? session.firstLiveSeq : snapshotCursor + 1);
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 nextSeq = cursor + 1;
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
- if (item.seq < nextSeq) continue;
1322
- if (item.seq !== nextSeq) throw new RemoteError("gateway/internal", `session event stream skipped seq ${String(nextSeq)}`, {});
1323
- nextSeq++;
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 < (header.seedLength ?? 0)) throw new RemoteError("subagent/catalog-diagnostic", "subagent descriptor is unavailable", {
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) for (const source of sources) groupStart = Math.min(groupStart, source);
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
- events: [...attached.events]
2679
+ inheritedEventCount: attached.inheritedEventCount,
2680
+ events: attached.snapshotEvents()
2676
2681
  });
2677
2682
  return inspectApiSession(this.ctx, sessionId, signal);
2678
2683
  }