@deepseek-ai/dsh-api-session-controller 0.1.2-alpha.5 → 0.1.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +6 -6
  3. package/README.zh.md +6 -6
  4. package/lib/client.js +592 -28
  5. package/lib/index.js +269 -214
  6. package/lib/typert.host.js +183 -141
  7. package/lib/typert.remote-client.js +79 -97
  8. package/lib/types/assistant-stream.d.ts +26 -0
  9. package/lib/types/assistant-stream.js +83 -0
  10. package/lib/types/client/contract/events.d.ts +38 -7
  11. package/lib/types/client/contract/events.js +21 -0
  12. package/lib/types/client/contract/session.d.ts +7 -7
  13. package/lib/types/client/contract/snapshot.d.ts +15 -2
  14. package/lib/types/client/index.d.ts +2 -2
  15. package/lib/types/client/index.js +2 -0
  16. package/lib/types/client/sessions/assistant-stream.d.ts +51 -0
  17. package/lib/types/client/sessions/assistant-stream.js +168 -0
  18. package/lib/types/client/sessions/history-records.d.ts +2 -2
  19. package/lib/types/client/sessions/history-records.js +3 -8
  20. package/lib/types/client/sessions/queue-mirror.js +3 -3
  21. package/lib/types/client/sessions/remotes.d.ts +2 -2
  22. package/lib/types/client/sessions/session.d.ts +3 -1
  23. package/lib/types/client/sessions/session.js +63 -15
  24. package/lib/types/client/transport.d.ts +7 -3
  25. package/lib/types/client/transport.js +18 -3
  26. package/lib/types/commands.js +98 -28
  27. package/lib/types/history.d.ts +2 -1
  28. package/lib/types/history.js +66 -37
  29. package/lib/types/index.d.ts +4 -4
  30. package/lib/types/index.js +13 -5
  31. package/lib/types/list.d.ts +2 -9
  32. package/lib/types/list.js +10 -124
  33. package/lib/types/types.d.ts +71 -29
  34. package/package.json +63 -57
package/lib/index.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { ReasoningEffortId, createUserMessage, errorChain, freezeMessage } from "@deepseek-ai/dsh-llm";
2
+ import { AssistantStreamAccumulator, ReasoningEffortId, assistantStreamChunks, createUserMessage, errorChain, freezeMessage } from "@deepseek-ai/dsh-llm";
3
3
  import { canOpenNativePath, openNativePath } from "@deepseek-ai/dsh-native-command";
4
4
  import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
5
- import { mkdir, stat } from "node:fs/promises";
5
+ import { mkdir } from "node:fs/promises";
6
6
  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, admitPromptContent } from "@deepseek-ai/dsh-attachment";
10
+ import { AttachmentError } from "@deepseek-ai/dsh-attachment";
11
11
  import { SessionLogOffset, SessionSeq, isAppendSurfaceEvent } from "@deepseek-ai/dsh-session";
12
12
  import { SessionTitleInvalidError } from "@deepseek-ai/dsh-session-title";
13
13
  import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
14
+ import { assertNever } from "@deepseek-ai/dsh-util-values";
14
15
  import { Deque } from "@deepseek-ai/dsh-deque";
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";
18
18
  //#region lib/types/agent.js
19
19
  /** Agent activation, composition, and model-selection policy owned by API Session. */
20
- var __addDisposableResource$5 = function(env, value, async) {
20
+ var __addDisposableResource$4 = function(env, value, async) {
21
21
  if (value !== null && value !== void 0) {
22
22
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
23
23
  var dispose, inner;
@@ -46,7 +46,7 @@ var __addDisposableResource$5 = function(env, value, async) {
46
46
  } else if (async) env.stack.push({ async: true });
47
47
  return value;
48
48
  };
49
- var __disposeResources$5 = (function(SuppressedError) {
49
+ var __disposeResources$4 = (function(SuppressedError) {
50
50
  return function(env) {
51
51
  function fail(e) {
52
52
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -147,7 +147,7 @@ async function inspectApiSession(ctx, sessionId, signal) {
147
147
  hasError: false
148
148
  };
149
149
  try {
150
- const observation = __addDisposableResource$5(env_1, await ctx.sessionQuery.observeSession(sessionId, {
150
+ const observation = __addDisposableResource$4(env_1, await ctx.sessionQuery.observeSession(sessionId, {
151
151
  ...signal === void 0 ? {} : { signal },
152
152
  projectionMode: "none"
153
153
  }), false);
@@ -161,7 +161,7 @@ async function inspectApiSession(ctx, sessionId, signal) {
161
161
  env_1.error = e_1;
162
162
  env_1.hasError = true;
163
163
  } finally {
164
- __disposeResources$5(env_1);
164
+ __disposeResources$4(env_1);
165
165
  }
166
166
  } catch (error) {
167
167
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new ApiSessionNotFound(`session "${sessionId}" not found`);
@@ -375,13 +375,13 @@ var ApiSessionAgentController = class {
375
375
  hasError: false
376
376
  };
377
377
  try {
378
- const observation = __addDisposableResource$5(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
378
+ const observation = __addDisposableResource$4(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
379
379
  return await this.resumeObserved(sessionId, observation);
380
380
  } catch (e_2) {
381
381
  env_2.error = e_2;
382
382
  env_2.hasError = true;
383
383
  } finally {
384
- __disposeResources$5(env_2);
384
+ __disposeResources$4(env_2);
385
385
  }
386
386
  } catch (error) {
387
387
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new ApiSessionNotFound(`session "${sessionId}" not found`);
@@ -413,7 +413,7 @@ var ApiSessionAgentController = class {
413
413
  hasError: false
414
414
  };
415
415
  try {
416
- const observation = __addDisposableResource$5(env_3, await this.ctx.sessionQuery.observeSession(sessionId), false);
416
+ const observation = __addDisposableResource$4(env_3, await this.ctx.sessionQuery.observeSession(sessionId), false);
417
417
  if (hasApiSessionSubagentOwner(this.ctx, { header: observation.header }, void 0)) throw new ApiSessionSubagentOwnership(sessionId);
418
418
  if (observation.header.cwd !== cwd) throw new ApiSessionCwdConflict(sessionId, cwd, observation.header.cwd);
419
419
  const storedPreset = this.presetForObservation(observation);
@@ -428,7 +428,7 @@ var ApiSessionAgentController = class {
428
428
  env_3.error = e_3;
429
429
  env_3.hasError = true;
430
430
  } finally {
431
- __disposeResources$5(env_3);
431
+ __disposeResources$4(env_3);
432
432
  }
433
433
  } catch (error) {
434
434
  if (!(error instanceof SessionQueryError) || error.code !== "SESSION_QUERY_SESSION_NOT_FOUND") throw error;
@@ -485,7 +485,7 @@ function agentModelSelection(selection) {
485
485
  //#endregion
486
486
  //#region lib/types/commands.js
487
487
  /** Session commands whose activation policy is explicit at each Remote method. */
488
- var __addDisposableResource$4 = function(env, value, async) {
488
+ var __addDisposableResource$3 = function(env, value, async) {
489
489
  if (value !== null && value !== void 0) {
490
490
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
491
491
  var dispose, inner;
@@ -514,7 +514,7 @@ var __addDisposableResource$4 = function(env, value, async) {
514
514
  } else if (async) env.stack.push({ async: true });
515
515
  return value;
516
516
  };
517
- var __disposeResources$4 = (function(SuppressedError) {
517
+ var __disposeResources$3 = (function(SuppressedError) {
518
518
  return function(env) {
519
519
  function fail(e) {
520
520
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -672,7 +672,7 @@ var SessionCommandController = class {
672
672
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${request.sessionId}" not found`, { sessionId: request.sessionId });
673
673
  throw new RemoteError("gateway/internal", `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
674
674
  }
675
- const source = __addDisposableResource$4(env_1, observed, false);
675
+ const source = __addDisposableResource$3(env_1, observed, false);
676
676
  const lastSeq = source.events.at(-1)?.seq ?? -1;
677
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);
678
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 });
@@ -720,7 +720,7 @@ var SessionCommandController = class {
720
720
  env_1.error = e_1;
721
721
  env_1.hasError = true;
722
722
  } finally {
723
- __disposeResources$4(env_1);
723
+ __disposeResources$3(env_1);
724
724
  }
725
725
  }
726
726
  /**
@@ -732,6 +732,7 @@ var SessionCommandController = class {
732
732
  const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
733
733
  if (request.clientTimeZone !== void 0 && clientTimeZone === void 0) throw new RemoteError("session/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: request.clientTimeZone });
734
734
  const agent = await this.resolveAgent(request.sessionId);
735
+ if (hasPromptRequest(agent, request.requestId)) return { accepted: true };
735
736
  const selection = this.agents.selectionFor(agent).current;
736
737
  if (!routeServed(this.ctx, selection.provider)) throw new RemoteError("session/model-unavailable", `no adapter serves provider "${selection.provider}"; select a model for this session`, {
737
738
  provider: selection.provider,
@@ -745,17 +746,33 @@ var SessionCommandController = class {
745
746
  const hasImage = request.content.some((part) => part.type === "image");
746
747
  const admit = async () => {
747
748
  try {
748
- if (hasImage) {
749
- const current = this.agents.selectionFor(agent).current;
750
- const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
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" });
749
+ const env_2 = {
750
+ stack: [],
751
+ error: void 0,
752
+ hasError: false
753
+ };
754
+ try {
755
+ if (hasImage) {
756
+ const current = this.agents.selectionFor(agent).current;
757
+ const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
758
+ 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" });
759
+ }
760
+ const admission = resolvePromptFileReceipts(request.content, (receiptId) => this.ctx.fileUploads.resolve(agent, receiptId));
761
+ const message = createUserMessage({
762
+ content: await this.ctx.attachments.admitPromptContent(admission.content),
763
+ source
764
+ });
765
+ if (this.ctx.agents.get(agent.id) !== agent) throw new RemoteError("session/not-found", `session "${agent.id}" was disposed during prompt admission`, { sessionId: agent.id });
766
+ const binding = __addDisposableResource$3(env_2, this.ctx.fileUploads.bindPrompt(agent, admission.receiptIds, request.requestId), false);
767
+ if (request.mode === "steer") agent.steer(message);
768
+ else agent.followup(message);
769
+ binding.commit();
770
+ } catch (e_2) {
771
+ env_2.error = e_2;
772
+ env_2.hasError = true;
773
+ } finally {
774
+ __disposeResources$3(env_2);
752
775
  }
753
- const message = createUserMessage({
754
- content: await admitPromptContent(this.ctx.attachments, request.content),
755
- source
756
- });
757
- if (request.mode === "steer") agent.steer(message);
758
- else agent.followup(message);
759
776
  } catch (error) {
760
777
  if (remoteErrorOf(error) !== void 0) throw error;
761
778
  if (error instanceof AttachmentError) throw new RemoteError("session/attachment-invalid", error.message, { reason: error.code });
@@ -799,8 +816,11 @@ var SessionCommandController = class {
799
816
  updateQueue(request) {
800
817
  if (request.action.kind === "edit" && request.action.content.some((block) => block.type !== "text")) throw new RemoteError("session/attachment-invalid", "queue edits accept text content only", { reason: "QUEUE_EDIT_NON_TEXT" });
801
818
  const agent = this.ctx.agents.get(request.sessionId);
802
- if (agent !== void 0 && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) throw apiSessionSubagentOwnershipError(request.sessionId);
803
819
  if (agent === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
820
+ if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
821
+ const identity = this.ctx.sessionProjections.snapshot(agent.session, ["subagent"]).values.subagent;
822
+ if (identity?.mode !== "continuable" || !agent.session.isOwnSeq(identity.seq)) throw apiSessionSubagentOwnershipError(request.sessionId);
823
+ }
804
824
  const nextTurn = agent.inbox.nextTurn.find((message) => message.id === request.itemId);
805
825
  const nextStep = agent.inbox.nextStep.find((message) => message.id === request.itemId);
806
826
  const located = nextTurn === void 0 ? nextStep === void 0 ? void 0 : {
@@ -813,13 +833,25 @@ var SessionCommandController = class {
813
833
  if (located === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
814
834
  const { target, message } = located;
815
835
  if (request.action.kind === "steer" && (target !== "next-turn" || agent.status !== "running")) throw new RemoteError("session/steer-unavailable", "current turn no longer accepts steering", { itemId: request.itemId });
816
- if (request.action.kind === "edit") agent.inbox.replace(request.itemId, freezeMessage({
817
- ...message,
818
- content: [...request.action.content]
819
- }));
820
- else {
821
- agent.inbox.remove(request.itemId);
822
- if (request.action.kind === "steer") agent.steer(message);
836
+ switch (request.action.kind) {
837
+ case "edit":
838
+ agent.inbox.replace(request.itemId, freezeMessage({
839
+ ...message,
840
+ content: [...request.action.content]
841
+ }));
842
+ break;
843
+ case "remove": {
844
+ agent.inbox.remove(request.itemId);
845
+ const source = message.source;
846
+ if (source.kind === "user" && "rpcId" in source) this.ctx.fileUploads.retirePrompt(agent, source.rpcId);
847
+ break;
848
+ }
849
+ case "steer":
850
+ agent.inbox.remove(request.itemId);
851
+ agent.steer(message);
852
+ break;
853
+ /* v8 ignore next 2 -- closed-union exhaustiveness guard */
854
+ default: assertNever(request.action, "queue action");
823
855
  }
824
856
  return { accepted: true };
825
857
  }
@@ -880,6 +912,34 @@ var SessionCommandController = class {
880
912
  }
881
913
  }
882
914
  };
915
+ function resolvePromptFileReceipts(content, stagedFile) {
916
+ const receiptIds = /* @__PURE__ */ new Set();
917
+ return {
918
+ content: content.map((part) => {
919
+ if (part.type !== "file") return part;
920
+ const attachment = stagedFile(part.receiptId);
921
+ if (attachment === void 0) throw new RemoteError("session/attachment-invalid", "File was not uploaded for this session.", { reason: "FILE_NOT_STAGED" });
922
+ receiptIds.add(part.receiptId);
923
+ return {
924
+ type: "file",
925
+ attachment
926
+ };
927
+ }),
928
+ receiptIds: [...receiptIds]
929
+ };
930
+ }
931
+ function hasPromptRequest(agent, requestId) {
932
+ const matches = (message) => {
933
+ const source = message.source;
934
+ return source.kind === "user" && "rpcId" in source && source.rpcId === requestId;
935
+ };
936
+ if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches)) return true;
937
+ return agent.session.snapshotEvents().some((event) => {
938
+ if (event.type !== "user/message") return false;
939
+ const source = event.data.source;
940
+ return source.kind === "user" && "rpcId" in source && source.rpcId === requestId;
941
+ });
942
+ }
883
943
  function imageBlockIn(content, match) {
884
944
  if (!Array.isArray(content)) return void 0;
885
945
  for (const value of content) {
@@ -905,7 +965,10 @@ function imageInEvent(event, match) {
905
965
  const found = imageBlockIn(inserted.content, match);
906
966
  if (found !== void 0) return found;
907
967
  }
908
- return event.type === "assistant/chunk" && data.chunk?.type === "block-end" ? imageBlockIn([data.chunk.block], match) : void 0;
968
+ if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) {
969
+ const found = imageBlockIn([chunk.block], match);
970
+ if (found !== void 0) return found;
971
+ }
909
972
  }
910
973
  function referencedImage(events, attachmentId) {
911
974
  for (const event of events) {
@@ -1116,9 +1179,90 @@ function jobView(job) {
1116
1179
  };
1117
1180
  }
1118
1181
  //#endregion
1182
+ //#region lib/types/assistant-stream.js
1183
+ /** Process-local assistant state retained for reconnecting Web followers. */
1184
+ const EMPTY_BASELINE = { revision: 0 };
1185
+ /**
1186
+ * Folds dense Agent frames and materializes one shared immutable reconnect
1187
+ * baseline per accepted revision.
1188
+ */
1189
+ var SessionAssistantStreamAccumulator = class {
1190
+ activeAttempt;
1191
+ revision = 0;
1192
+ snapshotValue = EMPTY_BASELINE;
1193
+ dirty = false;
1194
+ /**
1195
+ * Fold one trusted frame from the current attached Agent lifecycle.
1196
+ * @param frame - next dense process-local Assistant frame.
1197
+ * @param durableCursor - last committed Session seq when this frame was observed.
1198
+ */
1199
+ accept(frame, durableCursor) {
1200
+ if (frame.type === "start" && frame.revision === 1 && this.revision !== 0) {
1201
+ this.activeAttempt = void 0;
1202
+ this.revision = 0;
1203
+ }
1204
+ if (frame.revision !== this.revision + 1) {
1205
+ this.activeAttempt = void 0;
1206
+ this.revision = frame.revision;
1207
+ this.dirty = true;
1208
+ return;
1209
+ }
1210
+ this.revision = frame.revision;
1211
+ switch (frame.type) {
1212
+ case "start":
1213
+ this.activeAttempt = {
1214
+ attemptId: frame.attemptId,
1215
+ startedAfterSeq: durableCursor,
1216
+ turn: frame.turn,
1217
+ step: frame.step,
1218
+ stream: new AssistantStreamAccumulator(),
1219
+ nextIndex: 0
1220
+ };
1221
+ break;
1222
+ case "chunk": {
1223
+ const attempt = this.activeAttempt;
1224
+ if (attempt === void 0 || attempt.attemptId !== frame.attemptId || frame.index !== attempt.nextIndex) {
1225
+ this.activeAttempt = void 0;
1226
+ break;
1227
+ }
1228
+ attempt.stream.push({
1229
+ time: frame.time,
1230
+ chunk: frame.chunk
1231
+ });
1232
+ attempt.nextIndex += 1;
1233
+ break;
1234
+ }
1235
+ case "end":
1236
+ this.activeAttempt = void 0;
1237
+ break;
1238
+ }
1239
+ this.dirty = true;
1240
+ }
1241
+ /**
1242
+ * Read the cached reconnect baseline, materializing it after a state change.
1243
+ * @returns the identity-stable baseline for the latest accepted revision.
1244
+ */
1245
+ snapshot() {
1246
+ if (!this.dirty) return this.snapshotValue;
1247
+ this.snapshotValue = {
1248
+ revision: this.revision,
1249
+ ...this.activeAttempt === void 0 ? {} : { activeAttempt: {
1250
+ attemptId: this.activeAttempt.attemptId,
1251
+ startedAfterSeq: this.activeAttempt.startedAfterSeq,
1252
+ turn: this.activeAttempt.turn,
1253
+ step: this.activeAttempt.step,
1254
+ nextIndex: this.activeAttempt.nextIndex,
1255
+ stream: this.activeAttempt.stream.snapshot()
1256
+ } }
1257
+ };
1258
+ this.dirty = false;
1259
+ return this.snapshotValue;
1260
+ }
1261
+ };
1262
+ //#endregion
1119
1263
  //#region lib/types/history.js
1120
1264
  /** Cold Session history pagination and live-event source. */
1121
- var __addDisposableResource$3 = function(env, value, async) {
1265
+ var __addDisposableResource$2 = function(env, value, async) {
1122
1266
  if (value !== null && value !== void 0) {
1123
1267
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1124
1268
  var dispose, inner;
@@ -1147,7 +1291,7 @@ var __addDisposableResource$3 = function(env, value, async) {
1147
1291
  } else if (async) env.stack.push({ async: true });
1148
1292
  return value;
1149
1293
  };
1150
- var __disposeResources$3 = (function(SuppressedError) {
1294
+ var __disposeResources$2 = (function(SuppressedError) {
1151
1295
  return function(env) {
1152
1296
  function fail(e) {
1153
1297
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -1183,6 +1327,7 @@ var SessionHistoryController = class {
1183
1327
  ctx;
1184
1328
  promote;
1185
1329
  closeFollowers = /* @__PURE__ */ new Set();
1330
+ assistantStreams = /* @__PURE__ */ new Map();
1186
1331
  /**
1187
1332
  * @param ctx - Host context carrying Session query and projection services.
1188
1333
  * @param promote - starts ordinary Session activation after snapshot delivery.
@@ -1190,6 +1335,17 @@ var SessionHistoryController = class {
1190
1335
  constructor(ctx, promote) {
1191
1336
  this.ctx = ctx;
1192
1337
  this.promote = promote;
1338
+ ctx.on("agent/assistant-stream", ({ agent, frame }) => {
1339
+ let stream = this.assistantStreams.get(agent.session.id);
1340
+ if (stream === void 0) {
1341
+ stream = new SessionAssistantStreamAccumulator();
1342
+ this.assistantStreams.set(agent.session.id, stream);
1343
+ }
1344
+ stream.accept(frame, cursorBeforeNext(agent.session.seq));
1345
+ }, { global: true });
1346
+ ctx.on("agent/disposed", ({ agent }) => {
1347
+ this.assistantStreams.delete(agent.session.id);
1348
+ }, { global: true });
1193
1349
  ctx.effect(() => () => {
1194
1350
  for (const close of this.closeFollowers) close();
1195
1351
  this.closeFollowers.clear();
@@ -1211,7 +1367,7 @@ var SessionHistoryController = class {
1211
1367
  validatePageRequest(request);
1212
1368
  const throughSeq = request.throughSeq === -1 ? -1 : SessionSeq(request.throughSeq);
1213
1369
  const beforeSeq = request.beforeSeq === void 0 ? void 0 : SessionLogOffset(request.beforeSeq);
1214
- const source = __addDisposableResource$3(env_1, await this.sourceFor(request.address, signal, false), false);
1370
+ const source = __addDisposableResource$2(env_1, await this.sourceFor(request.address, signal, false), false);
1215
1371
  signal.throwIfAborted();
1216
1372
  const sourceLog = source.events;
1217
1373
  const sourceCursor = sourceLog.at(-1)?.seq ?? -1;
@@ -1227,14 +1383,14 @@ var SessionHistoryController = class {
1227
1383
  env_1.error = e_1;
1228
1384
  env_1.hasError = true;
1229
1385
  } finally {
1230
- __disposeResources$3(env_1);
1386
+ __disposeResources$2(env_1);
1231
1387
  }
1232
1388
  }
1233
1389
  /**
1234
1390
  * Follow events appended after an initial cursor on one durable address.
1235
1391
  * @param request - durable address and last committed sequence already held by the caller.
1236
1392
  * @param signal - stream cancellation owned by the Remote carrier.
1237
- * @returns a complete opening snapshot followed by gap-free event frames.
1393
+ * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
1238
1394
  */
1239
1395
  async *follow(request, signal) {
1240
1396
  validateFollowRequest(request);
@@ -1242,6 +1398,7 @@ var SessionHistoryController = class {
1242
1398
  const target = addressId(address);
1243
1399
  const buffered = new Deque();
1244
1400
  let snapshotCursor;
1401
+ let assistantStreamOrdinal = 0;
1245
1402
  let wake;
1246
1403
  const notify = () => {
1247
1404
  const resume = wake;
@@ -1256,13 +1413,28 @@ var SessionHistoryController = class {
1256
1413
  this.closeFollowers.add(close);
1257
1414
  const disposeEvent = this.ctx.on("session/event", (session, event) => {
1258
1415
  if (session.id !== target) return;
1259
- buffered.pushBack(event);
1416
+ buffered.pushBack({
1417
+ type: "event",
1418
+ event
1419
+ });
1260
1420
  notify();
1261
1421
  }, { global: true });
1262
1422
  const disposeCreated = this.ctx.on("session/created", (session) => {
1263
1423
  if (session.id !== target) return;
1264
1424
  const suffix = session.snapshotEvents(snapshotCursor === void 0 ? session.firstLiveSeq : SessionLogOffset(snapshotCursor + 1));
1265
- for (let index = suffix.length - 1; index >= 0; index -= 1) buffered.pushFront(suffix[index]);
1425
+ for (let index = suffix.length - 1; index >= 0; index -= 1) buffered.pushFront({
1426
+ type: "event",
1427
+ event: suffix[index]
1428
+ });
1429
+ notify();
1430
+ }, { global: true });
1431
+ const disposeAssistantStream = request.assistantStream !== true ? void 0 : this.ctx.on("agent/assistant-stream", ({ agent, frame }) => {
1432
+ if (agent.session.id !== target) return;
1433
+ buffered.pushBack({
1434
+ type: "assistant-stream",
1435
+ frame: wireAssistantStreamFrame(frame, cursorBeforeNext(agent.session.seq)),
1436
+ ordinal: ++assistantStreamOrdinal
1437
+ });
1266
1438
  notify();
1267
1439
  }, { global: true });
1268
1440
  const onAbort = () => {
@@ -1276,22 +1448,25 @@ var SessionHistoryController = class {
1276
1448
  hasError: false
1277
1449
  };
1278
1450
  try {
1279
- const source = __addDisposableResource$3(env_2, await this.sourceFor(address, signal, true), false);
1451
+ const source = __addDisposableResource$2(env_2, await this.sourceFor(address, signal, true), false);
1280
1452
  const events = source.events;
1281
1453
  signal.throwIfAborted();
1282
1454
  const cursor = source.cursor;
1283
1455
  snapshotCursor = cursor;
1284
1456
  const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
1457
+ const assistantStream = request.assistantStream === true ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 } : void 0;
1458
+ const assistantStreamOrdinalCut = assistantStreamOrdinal;
1285
1459
  yield {
1286
1460
  type: "snapshot",
1287
- header: wireHeader(source.header, source.inheritedEventCount),
1461
+ header: wireHeader(source.header),
1288
1462
  cursor,
1289
1463
  records: pageRecords(page.events),
1290
1464
  hasMore: page.hasMore,
1291
1465
  projections: source.projections === void 0 ? {
1292
1466
  asOfSeq: cursor,
1293
1467
  values: {}
1294
- } : projectionBlock(source.projections)
1468
+ } : projectionBlock(source.projections),
1469
+ ...assistantStream === void 0 ? {} : { assistantStream }
1295
1470
  };
1296
1471
  if (address.kind === "session" && source.source === "prepared") {
1297
1472
  const promotion = source.retain();
@@ -1311,23 +1486,31 @@ var SessionHistoryController = class {
1311
1486
  });
1312
1487
  continue;
1313
1488
  }
1489
+ if (item.type === "assistant-stream") {
1490
+ if (item.ordinal > assistantStreamOrdinalCut) yield {
1491
+ type: "assistant-stream",
1492
+ frame: item.frame
1493
+ };
1494
+ continue;
1495
+ }
1314
1496
  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)}`, {});
1497
+ if (item.event.seq < expectedSeq) continue;
1498
+ if (item.event.seq !== expectedSeq) throw new RemoteError("gateway/internal", `session event stream skipped seq ${String(expectedSeq)}`, {});
1317
1499
  nextOffset = SessionLogOffset(nextOffset + 1);
1318
- yield entryFor(item);
1500
+ yield entryFor(item.event);
1319
1501
  }
1320
1502
  } catch (e_2) {
1321
1503
  env_2.error = e_2;
1322
1504
  env_2.hasError = true;
1323
1505
  } finally {
1324
- __disposeResources$3(env_2);
1506
+ __disposeResources$2(env_2);
1325
1507
  }
1326
1508
  } finally {
1327
1509
  this.closeFollowers.delete(close);
1328
1510
  signal.removeEventListener("abort", onAbort);
1329
1511
  disposeCreated();
1330
1512
  disposeEvent();
1513
+ disposeAssistantStream?.();
1331
1514
  }
1332
1515
  }
1333
1516
  async sourceFor(address, signal, withProjections) {
@@ -1354,6 +1537,20 @@ var SessionHistoryController = class {
1354
1537
  }
1355
1538
  }
1356
1539
  };
1540
+ function cursorBeforeNext(nextSeq) {
1541
+ return nextSeq === 0 ? -1 : SessionSeq(nextSeq - 1);
1542
+ }
1543
+ function wireAssistantStreamFrame(frame, durableCursor) {
1544
+ if (frame.type === "start") return {
1545
+ ...frame,
1546
+ startedAfterSeq: durableCursor
1547
+ };
1548
+ if (frame.type === "end") return frame;
1549
+ return {
1550
+ ...frame,
1551
+ chunk: frame.chunk
1552
+ };
1553
+ }
1357
1554
  function projectionBlock(snapshot) {
1358
1555
  return {
1359
1556
  asOfSeq: snapshot.asOfSeq,
@@ -1420,13 +1617,9 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
1420
1617
  hasMore: cut > 0
1421
1618
  };
1422
1619
  }
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
- };
1620
+ /** Translate current logical Session metadata to the browser wire. */
1621
+ function wireHeader(header) {
1622
+ return { ...header };
1430
1623
  }
1431
1624
  function entryFor(event) {
1432
1625
  return {
@@ -1434,40 +1627,9 @@ function entryFor(event) {
1434
1627
  event
1435
1628
  };
1436
1629
  }
1437
- function chunkEntryFor(row) {
1438
- switch (row.type) {
1439
- case "text-chunks": return {
1440
- type: "chunks",
1441
- event: {
1442
- type: "chunkrow/text-chunks",
1443
- seq: row.seq0,
1444
- time: row.time0,
1445
- data: row.data
1446
- }
1447
- };
1448
- case "reasoning-chunks": return {
1449
- type: "chunks",
1450
- event: {
1451
- type: "chunkrow/reasoning-chunks",
1452
- seq: row.seq0,
1453
- time: row.time0,
1454
- data: row.data
1455
- }
1456
- };
1457
- case "tool-call-chunks": return {
1458
- type: "chunks",
1459
- event: {
1460
- type: "chunkrow/tool-call-chunks",
1461
- seq: row.seq0,
1462
- time: row.time0,
1463
- data: row.data
1464
- }
1465
- };
1466
- }
1467
- }
1468
1630
  /** Encode one bounded logical page without changing its pagination cut. */
1469
1631
  function pageRecords(events) {
1470
- return packChunkRuns(events).map((record) => isChunkRow(record) ? chunkEntryFor(record) : entryFor(record));
1632
+ return events.map(entryFor);
1471
1633
  }
1472
1634
  //#endregion
1473
1635
  //#region lib/types/file-references.js
@@ -1558,67 +1720,6 @@ let SessionFileReferences = (() => {
1558
1720
  //#endregion
1559
1721
  //#region lib/types/list.js
1560
1722
  /** Cold-safe Session list and search projection. */
1561
- var __addDisposableResource$2 = function(env, value, async) {
1562
- if (value !== null && value !== void 0) {
1563
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1564
- var dispose, inner;
1565
- if (async) {
1566
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
1567
- dispose = value[Symbol.asyncDispose];
1568
- }
1569
- if (dispose === void 0) {
1570
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
1571
- dispose = value[Symbol.dispose];
1572
- if (async) inner = dispose;
1573
- }
1574
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
1575
- if (inner) dispose = function() {
1576
- try {
1577
- inner.call(this);
1578
- } catch (e) {
1579
- return Promise.reject(e);
1580
- }
1581
- };
1582
- env.stack.push({
1583
- value,
1584
- dispose,
1585
- async
1586
- });
1587
- } else if (async) env.stack.push({ async: true });
1588
- return value;
1589
- };
1590
- var __disposeResources$2 = (function(SuppressedError) {
1591
- return function(env) {
1592
- function fail(e) {
1593
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
1594
- env.hasError = true;
1595
- }
1596
- var r, s = 0;
1597
- function next() {
1598
- while (r = env.stack.pop()) try {
1599
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
1600
- if (r.dispose) {
1601
- var result = r.dispose.call(r.value);
1602
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
1603
- fail(e);
1604
- return next();
1605
- });
1606
- } else s |= 1;
1607
- } catch (e) {
1608
- fail(e);
1609
- }
1610
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
1611
- if (env.hasError) throw env.error;
1612
- }
1613
- return next();
1614
- };
1615
- })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
1616
- var e = new Error(message);
1617
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1618
- });
1619
- /** Default maximum artifact size eligible for one cold projection observation. */
1620
- const DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024;
1621
- const COLD_SUMMARY_BATCH_SIZE = 16;
1622
1723
  const SEARCH_PROVIDER_CALL_LIMIT = 100;
1623
1724
  const SESSION_SEARCH_QUERY_MAX_CHARS = 500;
1624
1725
  const MESSAGE_TYPES = new Set(["user/message", "assistant/message"]);
@@ -1667,14 +1768,9 @@ function truncateUnicodeCodePoints(value, maximum) {
1667
1768
  /** Owns list projection registration, bounded cold summaries, and authorized search. */
1668
1769
  var ApiSessionList = class {
1669
1770
  ctx;
1670
- coldBlankProbeMaxBytes;
1671
- /**
1672
- * @param ctx - Host context carrying Session, query, persistence, and projection services.
1673
- * @param coldBlankProbeMaxBytes - maximum physical artifact size eligible for a full observation.
1674
- */
1675
- constructor(ctx, coldBlankProbeMaxBytes) {
1771
+ /** @param ctx - Host context carrying Session, query, persistence, and projection services. */
1772
+ constructor(ctx) {
1676
1773
  this.ctx = ctx;
1677
- this.coldBlankProbeMaxBytes = coldBlankProbeMaxBytes;
1678
1774
  ctx.sessionProjections.register({
1679
1775
  key: "sessionListMetadata",
1680
1776
  stateSchema: sessionListMetadataSchema,
@@ -1740,21 +1836,12 @@ var ApiSessionList = class {
1740
1836
  if (record.header.cwd === void 0) continue;
1741
1837
  cold.push(record.header);
1742
1838
  }
1743
- for (let offset = 0; offset < cold.length; offset += COLD_SUMMARY_BATCH_SIZE) {
1744
- const settled = await Promise.allSettled(cold.slice(offset, offset + COLD_SUMMARY_BATCH_SIZE).map((header) => this.summarizeCold(header, signal)));
1745
- for (const result of settled) {
1746
- if (result.status === "rejected") throw result.reason;
1747
- items.push(result.value);
1748
- }
1749
- }
1839
+ for (const header of cold) items.push(this.summarizeCold(header));
1750
1840
  items.sort((left, right) => right.updatedAt - left.updatedAt);
1751
1841
  return items;
1752
1842
  }
1753
- async summarizeCold(header, signal) {
1754
- const cached = this.projectionsFor(header, void 0);
1755
- const projections = cached?.values.sessionListMetadata?.blank === false ? cached : await this.probeSmallCold(header, signal) ?? cached;
1756
- const raced = this.ctx.sessions.get(header.id);
1757
- if (raced !== void 0) return this.summaryFor(raced);
1843
+ summarizeCold(header) {
1844
+ const projections = this.projectionsFor(header, void 0);
1758
1845
  const metadata = projections?.values.sessionListMetadata;
1759
1846
  return {
1760
1847
  sessionId: header.id,
@@ -1765,44 +1852,6 @@ var ApiSessionList = class {
1765
1852
  ...projections === void 0 ? {} : { projections }
1766
1853
  };
1767
1854
  }
1768
- async probeSmallCold(header, signal) {
1769
- if (this.coldBlankProbeMaxBytes === 0) return void 0;
1770
- const location = this.ctx.get("sessionPersistence")?.locate(header);
1771
- if (location === void 0) return void 0;
1772
- signal?.throwIfAborted();
1773
- try {
1774
- if ((await stat(location.path)).size > this.coldBlankProbeMaxBytes) return void 0;
1775
- } catch {
1776
- signal?.throwIfAborted();
1777
- return;
1778
- }
1779
- try {
1780
- const env_1 = {
1781
- stack: [],
1782
- error: void 0,
1783
- hasError: false
1784
- };
1785
- try {
1786
- const block = __addDisposableResource$2(env_1, await this.ctx.sessionQuery.observeSession(header.id, {
1787
- ...signal === void 0 ? {} : { signal },
1788
- projectionMode: "all"
1789
- }), false).projections;
1790
- return block === void 0 ? void 0 : {
1791
- asOfSeq: block.asOfSeq,
1792
- values: block.values
1793
- };
1794
- } catch (e_1) {
1795
- env_1.error = e_1;
1796
- env_1.hasError = true;
1797
- } finally {
1798
- __disposeResources$2(env_1);
1799
- }
1800
- } catch (error) {
1801
- signal?.throwIfAborted();
1802
- this.ctx.logger.warn(`api-session.list: small cold observation for "${header.id}" failed; serving it as visible: ${String(error)}`);
1803
- return;
1804
- }
1805
- }
1806
1855
  /**
1807
1856
  * Search current visible message content without activating any matching Session.
1808
1857
  * @param query - literal message-content query.
@@ -1893,7 +1942,8 @@ var ApiSessionList = class {
1893
1942
  }
1894
1943
  projectionsFor(header, session) {
1895
1944
  try {
1896
- const block = session === void 0 ? header.isSeeded ? void 0 : this.ctx.get("sessionProjectionCache")?.cachedSnapshot(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1945
+ const cache = this.ctx.get("sessionProjectionCache");
1946
+ const block = session === void 0 ? header.isSeeded ? void 0 : cache?.cachedSnapshot(header, SessionLogOffset(0)) ?? cache?.cachedPredecessorTitle(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1897
1947
  return block !== void 0 && Object.keys(block.values).length > 0 ? {
1898
1948
  asOfSeq: block.asOfSeq,
1899
1949
  values: block.values
@@ -2571,6 +2621,7 @@ let SessionController = (() => {
2571
2621
  "agentDefaultModel",
2572
2622
  "agents",
2573
2623
  "attachments",
2624
+ "fileUploads",
2574
2625
  "llm",
2575
2626
  "sessions",
2576
2627
  "sessionProjections",
@@ -2578,10 +2629,7 @@ let SessionController = (() => {
2578
2629
  "typert",
2579
2630
  "workspaceRegistry"
2580
2631
  ];
2581
- static Config = z.object({
2582
- coldBlankProbeMaxBytes: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_BYTES),
2583
- nativeOpen: z.boolean()
2584
- });
2632
+ static Config = z.object({ nativeOpen: z.boolean() });
2585
2633
  agents = __runInitializers(this, _instanceExtraInitializers);
2586
2634
  commands;
2587
2635
  controlState;
@@ -2592,13 +2640,19 @@ let SessionController = (() => {
2592
2640
  promotions = /* @__PURE__ */ new Set();
2593
2641
  /**
2594
2642
  * @param ctx - Host context containing the Session capability assembly.
2595
- * @param config - cold-list observation policy.
2643
+ * @param config - native-opener deployment policy.
2644
+ * @param internals - host integrations replaceable by direct unit tests.
2596
2645
  */
2597
2646
  constructor(ctx, config, internals = {}) {
2598
2647
  super(ctx, "sessionController", { namespace: "session" });
2599
2648
  installModelSelectionProjection(ctx);
2600
2649
  this.agents = new ApiSessionAgentController(ctx);
2601
2650
  this.commands = new SessionCommandController(ctx, this.agents, process.cwd());
2651
+ ctx.effect(() => ctx.fileUploads.registerAgentResolver(async (sessionId) => {
2652
+ const result = await this.agents.resolveAgent(sessionId);
2653
+ if ("error" in result) throw result.error;
2654
+ return result.agent;
2655
+ }), "session-controller: file-upload Agent resolver");
2602
2656
  this.controlState = new SessionControlController(ctx);
2603
2657
  ctx.effect(() => async () => {
2604
2658
  await Promise.allSettled([...this.promotions]);
@@ -2606,7 +2660,7 @@ let SessionController = (() => {
2606
2660
  this.history = new SessionHistoryController(ctx, (observation) => {
2607
2661
  this.promote(observation);
2608
2662
  });
2609
- this.listState = new ApiSessionList(ctx, config.coldBlankProbeMaxBytes ?? 1024);
2663
+ this.listState = new ApiSessionList(ctx);
2610
2664
  this.openPath = internals.openPath ?? openNativePath;
2611
2665
  this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
2612
2666
  ctx.plugin(SessionFileReferences);
@@ -2810,7 +2864,8 @@ let SessionController = (() => {
2810
2864
  * Follow one Session log from its opening or resume cursor.
2811
2865
  * @param request - durable address and last committed sequence already held by the caller.
2812
2866
  * @param signal - cancellation owned by the Remote stream carrier.
2813
- * @returns a complete opening snapshot followed by gap-free event frames.
2867
+ * @returns a complete opening snapshot followed by gap-free durable event
2868
+ * frames and optional cursorless assistant-stream frames.
2814
2869
  */
2815
2870
  follow(request, signal) {
2816
2871
  return this.history.follow(request, signal);