@deepseek-ai/dsh-api-session-controller 0.1.2-rc.1 → 0.1.5-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +16 -6
  3. package/README.zh.md +16 -6
  4. package/lib/client.js +838 -58
  5. package/lib/index.js +376 -243
  6. package/lib/typert.host.js +236 -178
  7. package/lib/typert.remote-client.js +97 -115
  8. package/lib/types/agent.js +6 -8
  9. package/lib/types/assistant-stream.d.ts +26 -0
  10. package/lib/types/assistant-stream.js +83 -0
  11. package/lib/types/client/contract/events.d.ts +38 -7
  12. package/lib/types/client/contract/events.js +21 -0
  13. package/lib/types/client/contract/session.d.ts +7 -7
  14. package/lib/types/client/contract/snapshot.d.ts +15 -2
  15. package/lib/types/client/index.d.ts +2 -2
  16. package/lib/types/client/index.js +2 -0
  17. package/lib/types/client/session-wire-event.d.ts +11 -0
  18. package/lib/types/client/session-wire-event.js +43 -0
  19. package/lib/types/client/sessions/assistant-stream.d.ts +51 -0
  20. package/lib/types/client/sessions/assistant-stream.js +168 -0
  21. package/lib/types/client/sessions/history-records.d.ts +2 -2
  22. package/lib/types/client/sessions/history-records.js +3 -8
  23. package/lib/types/client/sessions/queue-mirror.js +3 -3
  24. package/lib/types/client/sessions/remotes.d.ts +2 -2
  25. package/lib/types/client/sessions/session.d.ts +3 -1
  26. package/lib/types/client/sessions/session.js +63 -15
  27. package/lib/types/client/transport.d.ts +7 -3
  28. package/lib/types/client/transport.js +24 -3
  29. package/lib/types/commands.d.ts +1 -1
  30. package/lib/types/commands.js +112 -32
  31. package/lib/types/control.d.ts +0 -1
  32. package/lib/types/control.js +19 -22
  33. package/lib/types/history.d.ts +2 -1
  34. package/lib/types/history.js +66 -37
  35. package/lib/types/index.d.ts +4 -4
  36. package/lib/types/index.js +15 -5
  37. package/lib/types/list.d.ts +2 -9
  38. package/lib/types/list.js +10 -124
  39. package/lib/types/media-references.d.ts +16 -0
  40. package/lib/types/media-references.js +77 -0
  41. package/lib/types/types.d.ts +85 -35
  42. package/package.json +71 -58
package/lib/index.js CHANGED
@@ -1,23 +1,26 @@
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
+ import { isAbsolute } from "node:path";
19
+ import { FsError } from "@deepseek-ai/dsh-fs";
20
+ import mime from "mime-types";
18
21
  //#region lib/types/agent.js
19
22
  /** Agent activation, composition, and model-selection policy owned by API Session. */
20
- var __addDisposableResource$5 = function(env, value, async) {
23
+ var __addDisposableResource$4 = function(env, value, async) {
21
24
  if (value !== null && value !== void 0) {
22
25
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
23
26
  var dispose, inner;
@@ -46,7 +49,7 @@ var __addDisposableResource$5 = function(env, value, async) {
46
49
  } else if (async) env.stack.push({ async: true });
47
50
  return value;
48
51
  };
49
- var __disposeResources$5 = (function(SuppressedError) {
52
+ var __disposeResources$4 = (function(SuppressedError) {
50
53
  return function(env) {
51
54
  function fail(e) {
52
55
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -147,7 +150,7 @@ async function inspectApiSession(ctx, sessionId, signal) {
147
150
  hasError: false
148
151
  };
149
152
  try {
150
- const observation = __addDisposableResource$5(env_1, await ctx.sessionQuery.observeSession(sessionId, {
153
+ const observation = __addDisposableResource$4(env_1, await ctx.sessionQuery.observeSession(sessionId, {
151
154
  ...signal === void 0 ? {} : { signal },
152
155
  projectionMode: "none"
153
156
  }), false);
@@ -161,7 +164,7 @@ async function inspectApiSession(ctx, sessionId, signal) {
161
164
  env_1.error = e_1;
162
165
  env_1.hasError = true;
163
166
  } finally {
164
- __disposeResources$5(env_1);
167
+ __disposeResources$4(env_1);
165
168
  }
166
169
  } catch (error) {
167
170
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new ApiSessionNotFound(`session "${sessionId}" not found`);
@@ -349,14 +352,14 @@ var ApiSessionAgentController = class {
349
352
  */
350
353
  async composeAgent(presetId) {
351
354
  const presets = this.ctx.get("agentPresets");
352
- if (presets === void 0) return { setup: (agentCtx) => {
353
- this.installSelection(agentCtx);
355
+ if (presets === void 0) return { setup: (_agentCtx, agent) => {
356
+ this.installSelection(agent);
354
357
  } };
355
358
  const resolvedId = (await presets.resolve(presetId)).id;
356
359
  return {
357
360
  agentPreset: resolvedId,
358
- setup: async (agentCtx) => {
359
- this.installSelection(agentCtx);
361
+ setup: async (agentCtx, agent) => {
362
+ this.installSelection(agent);
360
363
  await presets.mount(agentCtx, resolvedId);
361
364
  }
362
365
  };
@@ -375,13 +378,13 @@ var ApiSessionAgentController = class {
375
378
  hasError: false
376
379
  };
377
380
  try {
378
- const observation = __addDisposableResource$5(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
381
+ const observation = __addDisposableResource$4(env_2, await this.ctx.sessionQuery.observeSession(sessionId), false);
379
382
  return await this.resumeObserved(sessionId, observation);
380
383
  } catch (e_2) {
381
384
  env_2.error = e_2;
382
385
  env_2.hasError = true;
383
386
  } finally {
384
- __disposeResources$5(env_2);
387
+ __disposeResources$4(env_2);
385
388
  }
386
389
  } catch (error) {
387
390
  if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new ApiSessionNotFound(`session "${sessionId}" not found`);
@@ -413,7 +416,7 @@ var ApiSessionAgentController = class {
413
416
  hasError: false
414
417
  };
415
418
  try {
416
- const observation = __addDisposableResource$5(env_3, await this.ctx.sessionQuery.observeSession(sessionId), false);
419
+ const observation = __addDisposableResource$4(env_3, await this.ctx.sessionQuery.observeSession(sessionId), false);
417
420
  if (hasApiSessionSubagentOwner(this.ctx, { header: observation.header }, void 0)) throw new ApiSessionSubagentOwnership(sessionId);
418
421
  if (observation.header.cwd !== cwd) throw new ApiSessionCwdConflict(sessionId, cwd, observation.header.cwd);
419
422
  const storedPreset = this.presetForObservation(observation);
@@ -428,7 +431,7 @@ var ApiSessionAgentController = class {
428
431
  env_3.error = e_3;
429
432
  env_3.hasError = true;
430
433
  } finally {
431
- __disposeResources$5(env_3);
434
+ __disposeResources$4(env_3);
432
435
  }
433
436
  } catch (error) {
434
437
  if (!(error instanceof SessionQueryError) || error.code !== "SESSION_QUERY_SESSION_NOT_FOUND") throw error;
@@ -456,9 +459,7 @@ var ApiSessionAgentController = class {
456
459
  model
457
460
  };
458
461
  }
459
- installSelection(agentCtx) {
460
- const agent = agentCtx.agent;
461
- if (agent === void 0) throw new Error("api-session: Agent setup has no scoped Agent");
462
+ installSelection(agent) {
462
463
  this.selectionFor(agent);
463
464
  }
464
465
  /**
@@ -485,7 +486,7 @@ function agentModelSelection(selection) {
485
486
  //#endregion
486
487
  //#region lib/types/commands.js
487
488
  /** Session commands whose activation policy is explicit at each Remote method. */
488
- var __addDisposableResource$4 = function(env, value, async) {
489
+ var __addDisposableResource$3 = function(env, value, async) {
489
490
  if (value !== null && value !== void 0) {
490
491
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
491
492
  var dispose, inner;
@@ -514,7 +515,7 @@ var __addDisposableResource$4 = function(env, value, async) {
514
515
  } else if (async) env.stack.push({ async: true });
515
516
  return value;
516
517
  };
517
- var __disposeResources$4 = (function(SuppressedError) {
518
+ var __disposeResources$3 = (function(SuppressedError) {
518
519
  return function(env) {
519
520
  function fail(e) {
520
521
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -543,6 +544,9 @@ var __disposeResources$4 = (function(SuppressedError) {
543
544
  var e = new Error(message);
544
545
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
545
546
  });
547
+ function hasPromptContent(content) {
548
+ return content.some((part) => part.type !== "text" || part.text.trim().length > 0);
549
+ }
546
550
  /** Implements Session business commands delegated by the Session Controller Remote service. */
547
551
  var SessionCommandController = class {
548
552
  ctx;
@@ -672,7 +676,7 @@ var SessionCommandController = class {
672
676
  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
677
  throw new RemoteError("gateway/internal", `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
674
678
  }
675
- const source = __addDisposableResource$4(env_1, observed, false);
679
+ const source = __addDisposableResource$3(env_1, observed, false);
676
680
  const lastSeq = source.events.at(-1)?.seq ?? -1;
677
681
  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
682
  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,18 +724,20 @@ var SessionCommandController = class {
720
724
  env_1.error = e_1;
721
725
  env_1.hasError = true;
722
726
  } finally {
723
- __disposeResources$4(env_1);
727
+ __disposeResources$3(env_1);
724
728
  }
725
729
  }
726
730
  /**
727
- * Admit one browser prompt after explicit Agent resume and image validation.
731
+ * Reject empty content, then admit one prompt after Agent and attachment validation.
728
732
  * @param request - Session identity, prompt content, source metadata, and delivery mode.
729
733
  * @returns acknowledgement that the Agent accepted the prompt.
730
734
  */
731
735
  async prompt(request) {
736
+ if (!hasPromptContent(request.content)) throw new RemoteError("gateway/bad-request", "prompt content must include non-whitespace text or an attachment", {});
732
737
  const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
733
738
  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
739
  const agent = await this.resolveAgent(request.sessionId);
740
+ if (hasPromptRequest(agent, request.requestId)) return { accepted: true };
735
741
  const selection = this.agents.selectionFor(agent).current;
736
742
  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
743
  provider: selection.provider,
@@ -745,17 +751,33 @@ var SessionCommandController = class {
745
751
  const hasImage = request.content.some((part) => part.type === "image");
746
752
  const admit = async () => {
747
753
  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" });
754
+ const env_2 = {
755
+ stack: [],
756
+ error: void 0,
757
+ hasError: false
758
+ };
759
+ try {
760
+ if (hasImage) {
761
+ const current = this.agents.selectionFor(agent).current;
762
+ const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
763
+ 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" });
764
+ }
765
+ const admission = resolvePromptFileReceipts(request.content, (receiptId) => this.ctx.fileUploads.resolve(agent, receiptId));
766
+ const message = createUserMessage({
767
+ content: await this.ctx.attachments.admitPromptContent(admission.content),
768
+ source
769
+ });
770
+ 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 });
771
+ const binding = __addDisposableResource$3(env_2, this.ctx.fileUploads.bindPrompt(agent, admission.receiptIds, request.requestId), false);
772
+ if (request.mode === "steer") agent.steer(message);
773
+ else agent.followup(message);
774
+ binding.commit();
775
+ } catch (e_2) {
776
+ env_2.error = e_2;
777
+ env_2.hasError = true;
778
+ } finally {
779
+ __disposeResources$3(env_2);
752
780
  }
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
781
  } catch (error) {
760
782
  if (remoteErrorOf(error) !== void 0) throw error;
761
783
  if (error instanceof AttachmentError) throw new RemoteError("session/attachment-invalid", error.message, { reason: error.code });
@@ -797,10 +819,16 @@ var SessionCommandController = class {
797
819
  * @returns acknowledgement that the queue mutation was applied.
798
820
  */
799
821
  updateQueue(request) {
800
- 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" });
822
+ if (request.action.kind === "edit") {
823
+ if (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" });
824
+ if (!hasPromptContent(request.action.content)) throw new RemoteError("gateway/bad-request", "queue edit content must include non-whitespace text", {});
825
+ }
801
826
  const agent = this.ctx.agents.get(request.sessionId);
802
- if (agent !== void 0 && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) throw apiSessionSubagentOwnershipError(request.sessionId);
803
827
  if (agent === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
828
+ if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
829
+ const identity = this.ctx.sessionProjections.snapshot(agent.session, ["subagent"]).values.subagent;
830
+ if (identity?.mode !== "continuable" || !agent.session.isOwnSeq(identity.seq)) throw apiSessionSubagentOwnershipError(request.sessionId);
831
+ }
804
832
  const nextTurn = agent.inbox.nextTurn.find((message) => message.id === request.itemId);
805
833
  const nextStep = agent.inbox.nextStep.find((message) => message.id === request.itemId);
806
834
  const located = nextTurn === void 0 ? nextStep === void 0 ? void 0 : {
@@ -813,13 +841,25 @@ var SessionCommandController = class {
813
841
  if (located === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
814
842
  const { target, message } = located;
815
843
  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);
844
+ switch (request.action.kind) {
845
+ case "edit":
846
+ agent.inbox.replace(request.itemId, freezeMessage({
847
+ ...message,
848
+ content: [...request.action.content]
849
+ }));
850
+ break;
851
+ case "remove": {
852
+ agent.inbox.remove(request.itemId);
853
+ const source = message.source;
854
+ if (source.kind === "user" && "rpcId" in source) this.ctx.fileUploads.retirePrompt(agent, source.rpcId);
855
+ break;
856
+ }
857
+ case "steer":
858
+ agent.inbox.remove(request.itemId);
859
+ agent.steer(message);
860
+ break;
861
+ /* v8 ignore next 2 -- closed-union exhaustiveness guard */
862
+ default: assertNever(request.action, "queue action");
823
863
  }
824
864
  return { accepted: true };
825
865
  }
@@ -880,6 +920,34 @@ var SessionCommandController = class {
880
920
  }
881
921
  }
882
922
  };
923
+ function resolvePromptFileReceipts(content, stagedFile) {
924
+ const receiptIds = /* @__PURE__ */ new Set();
925
+ return {
926
+ content: content.map((part) => {
927
+ if (part.type !== "file") return part;
928
+ const attachment = stagedFile(part.receiptId);
929
+ if (attachment === void 0) throw new RemoteError("session/attachment-invalid", "File was not uploaded for this session.", { reason: "FILE_NOT_STAGED" });
930
+ receiptIds.add(part.receiptId);
931
+ return {
932
+ type: "file",
933
+ attachment
934
+ };
935
+ }),
936
+ receiptIds: [...receiptIds]
937
+ };
938
+ }
939
+ function hasPromptRequest(agent, requestId) {
940
+ const matches = (message) => {
941
+ const source = message.source;
942
+ return source.kind === "user" && "rpcId" in source && source.rpcId === requestId;
943
+ };
944
+ if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches)) return true;
945
+ return agent.session.snapshotEvents().some((event) => {
946
+ if (event.type !== "user/message") return false;
947
+ const source = event.data.source;
948
+ return source.kind === "user" && "rpcId" in source && source.rpcId === requestId;
949
+ });
950
+ }
883
951
  function imageBlockIn(content, match) {
884
952
  if (!Array.isArray(content)) return void 0;
885
953
  for (const value of content) {
@@ -905,7 +973,10 @@ function imageInEvent(event, match) {
905
973
  const found = imageBlockIn(inserted.content, match);
906
974
  if (found !== void 0) return found;
907
975
  }
908
- return event.type === "assistant/chunk" && data.chunk?.type === "block-end" ? imageBlockIn([data.chunk.block], match) : void 0;
976
+ if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) {
977
+ const found = imageBlockIn([chunk.block], match);
978
+ if (found !== void 0) return found;
979
+ }
909
980
  }
910
981
  function referencedImage(events, attachmentId) {
911
982
  for (const event of events) {
@@ -926,9 +997,6 @@ var SessionControlController = class {
926
997
  /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
927
998
  constructor(ctx) {
928
999
  this.ctx = ctx;
929
- ctx.on("session/event", (session, event) => {
930
- this.onSessionEvent(session, event);
931
- });
932
1000
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
933
1001
  this.broadcast({
934
1002
  type: "projection",
@@ -937,6 +1005,13 @@ var SessionControlController = class {
937
1005
  value,
938
1006
  seq
939
1007
  });
1008
+ if (key !== "inbox") return;
1009
+ if (this.ctx.agents.get(session.id)?.session !== session) return;
1010
+ this.broadcast({
1011
+ type: "queue",
1012
+ sessionId: session.id,
1013
+ items: queueItemsFromInbox(value)
1014
+ });
940
1015
  });
941
1016
  ctx.inject(["jobs"], (jobsCtx) => {
942
1017
  jobsCtx.jobs.onJobsChanged((owner) => {
@@ -1002,16 +1077,6 @@ var SessionControlController = class {
1002
1077
  }
1003
1078
  return blocks;
1004
1079
  }
1005
- onSessionEvent(session, event) {
1006
- if (event.type !== "agent/inbox/spliced") return;
1007
- const agent = this.ctx.agents.get(session.id);
1008
- if (agent?.session !== session) return;
1009
- this.broadcast({
1010
- type: "queue",
1011
- sessionId: session.id,
1012
- items: queueItems(agent, event.data)
1013
- });
1014
- }
1015
1080
  onJobsChanged(owner) {
1016
1081
  if (owner !== void 0) {
1017
1082
  this.broadcast({
@@ -1076,12 +1141,14 @@ var ControlQueue = class {
1076
1141
  }
1077
1142
  }
1078
1143
  };
1079
- function queueItems(agent, splice) {
1080
- const project = (target) => {
1081
- const messages = target === "next-turn" ? agent.inbox.nextTurn : agent.inbox.nextStep;
1082
- return splice?.target === target ? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted) : messages;
1083
- };
1084
- return [...project("next-turn").map((message) => ({
1144
+ function queueItems(agent) {
1145
+ return queueItemsFromInbox({
1146
+ "next-turn": agent.inbox.nextTurn,
1147
+ "next-step": agent.inbox.nextStep
1148
+ });
1149
+ }
1150
+ function queueItemsFromInbox(inbox) {
1151
+ return [...inbox["next-turn"].map((message) => ({
1085
1152
  id: message.id,
1086
1153
  placement: "queued",
1087
1154
  ...promptRpcId(message),
@@ -1089,7 +1156,7 @@ function queueItems(agent, splice) {
1089
1156
  id: message.id,
1090
1157
  content: message.content
1091
1158
  }
1092
- })), ...project("next-step").map((message) => ({
1159
+ })), ...inbox["next-step"].map((message) => ({
1093
1160
  id: message.id,
1094
1161
  placement: message.source.kind === "user" ? "steering" : "context",
1095
1162
  ...promptRpcId(message),
@@ -1116,9 +1183,90 @@ function jobView(job) {
1116
1183
  };
1117
1184
  }
1118
1185
  //#endregion
1186
+ //#region lib/types/assistant-stream.js
1187
+ /** Process-local assistant state retained for reconnecting Web followers. */
1188
+ const EMPTY_BASELINE = { revision: 0 };
1189
+ /**
1190
+ * Folds dense Agent frames and materializes one shared immutable reconnect
1191
+ * baseline per accepted revision.
1192
+ */
1193
+ var SessionAssistantStreamAccumulator = class {
1194
+ activeAttempt;
1195
+ revision = 0;
1196
+ snapshotValue = EMPTY_BASELINE;
1197
+ dirty = false;
1198
+ /**
1199
+ * Fold one trusted frame from the current attached Agent lifecycle.
1200
+ * @param frame - next dense process-local Assistant frame.
1201
+ * @param durableCursor - last committed Session seq when this frame was observed.
1202
+ */
1203
+ accept(frame, durableCursor) {
1204
+ if (frame.type === "start" && frame.revision === 1 && this.revision !== 0) {
1205
+ this.activeAttempt = void 0;
1206
+ this.revision = 0;
1207
+ }
1208
+ if (frame.revision !== this.revision + 1) {
1209
+ this.activeAttempt = void 0;
1210
+ this.revision = frame.revision;
1211
+ this.dirty = true;
1212
+ return;
1213
+ }
1214
+ this.revision = frame.revision;
1215
+ switch (frame.type) {
1216
+ case "start":
1217
+ this.activeAttempt = {
1218
+ attemptId: frame.attemptId,
1219
+ startedAfterSeq: durableCursor,
1220
+ turn: frame.turn,
1221
+ step: frame.step,
1222
+ stream: new AssistantStreamAccumulator(),
1223
+ nextIndex: 0
1224
+ };
1225
+ break;
1226
+ case "chunk": {
1227
+ const attempt = this.activeAttempt;
1228
+ if (attempt === void 0 || attempt.attemptId !== frame.attemptId || frame.index !== attempt.nextIndex) {
1229
+ this.activeAttempt = void 0;
1230
+ break;
1231
+ }
1232
+ attempt.stream.push({
1233
+ time: frame.time,
1234
+ chunk: frame.chunk
1235
+ });
1236
+ attempt.nextIndex += 1;
1237
+ break;
1238
+ }
1239
+ case "end":
1240
+ this.activeAttempt = void 0;
1241
+ break;
1242
+ }
1243
+ this.dirty = true;
1244
+ }
1245
+ /**
1246
+ * Read the cached reconnect baseline, materializing it after a state change.
1247
+ * @returns the identity-stable baseline for the latest accepted revision.
1248
+ */
1249
+ snapshot() {
1250
+ if (!this.dirty) return this.snapshotValue;
1251
+ this.snapshotValue = {
1252
+ revision: this.revision,
1253
+ ...this.activeAttempt === void 0 ? {} : { activeAttempt: {
1254
+ attemptId: this.activeAttempt.attemptId,
1255
+ startedAfterSeq: this.activeAttempt.startedAfterSeq,
1256
+ turn: this.activeAttempt.turn,
1257
+ step: this.activeAttempt.step,
1258
+ nextIndex: this.activeAttempt.nextIndex,
1259
+ stream: this.activeAttempt.stream.snapshot()
1260
+ } }
1261
+ };
1262
+ this.dirty = false;
1263
+ return this.snapshotValue;
1264
+ }
1265
+ };
1266
+ //#endregion
1119
1267
  //#region lib/types/history.js
1120
1268
  /** Cold Session history pagination and live-event source. */
1121
- var __addDisposableResource$3 = function(env, value, async) {
1269
+ var __addDisposableResource$2 = function(env, value, async) {
1122
1270
  if (value !== null && value !== void 0) {
1123
1271
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1124
1272
  var dispose, inner;
@@ -1147,7 +1295,7 @@ var __addDisposableResource$3 = function(env, value, async) {
1147
1295
  } else if (async) env.stack.push({ async: true });
1148
1296
  return value;
1149
1297
  };
1150
- var __disposeResources$3 = (function(SuppressedError) {
1298
+ var __disposeResources$2 = (function(SuppressedError) {
1151
1299
  return function(env) {
1152
1300
  function fail(e) {
1153
1301
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -1183,6 +1331,7 @@ var SessionHistoryController = class {
1183
1331
  ctx;
1184
1332
  promote;
1185
1333
  closeFollowers = /* @__PURE__ */ new Set();
1334
+ assistantStreams = /* @__PURE__ */ new Map();
1186
1335
  /**
1187
1336
  * @param ctx - Host context carrying Session query and projection services.
1188
1337
  * @param promote - starts ordinary Session activation after snapshot delivery.
@@ -1190,6 +1339,17 @@ var SessionHistoryController = class {
1190
1339
  constructor(ctx, promote) {
1191
1340
  this.ctx = ctx;
1192
1341
  this.promote = promote;
1342
+ ctx.on("agent/assistant-stream", ({ agent, frame }) => {
1343
+ let stream = this.assistantStreams.get(agent.session.id);
1344
+ if (stream === void 0) {
1345
+ stream = new SessionAssistantStreamAccumulator();
1346
+ this.assistantStreams.set(agent.session.id, stream);
1347
+ }
1348
+ stream.accept(frame, cursorBeforeNext(agent.session.seq));
1349
+ }, { global: true });
1350
+ ctx.on("agent/disposed", ({ agent }) => {
1351
+ this.assistantStreams.delete(agent.session.id);
1352
+ }, { global: true });
1193
1353
  ctx.effect(() => () => {
1194
1354
  for (const close of this.closeFollowers) close();
1195
1355
  this.closeFollowers.clear();
@@ -1211,7 +1371,7 @@ var SessionHistoryController = class {
1211
1371
  validatePageRequest(request);
1212
1372
  const throughSeq = request.throughSeq === -1 ? -1 : SessionSeq(request.throughSeq);
1213
1373
  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);
1374
+ const source = __addDisposableResource$2(env_1, await this.sourceFor(request.address, signal, false), false);
1215
1375
  signal.throwIfAborted();
1216
1376
  const sourceLog = source.events;
1217
1377
  const sourceCursor = sourceLog.at(-1)?.seq ?? -1;
@@ -1227,14 +1387,14 @@ var SessionHistoryController = class {
1227
1387
  env_1.error = e_1;
1228
1388
  env_1.hasError = true;
1229
1389
  } finally {
1230
- __disposeResources$3(env_1);
1390
+ __disposeResources$2(env_1);
1231
1391
  }
1232
1392
  }
1233
1393
  /**
1234
1394
  * Follow events appended after an initial cursor on one durable address.
1235
1395
  * @param request - durable address and last committed sequence already held by the caller.
1236
1396
  * @param signal - stream cancellation owned by the Remote carrier.
1237
- * @returns a complete opening snapshot followed by gap-free event frames.
1397
+ * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
1238
1398
  */
1239
1399
  async *follow(request, signal) {
1240
1400
  validateFollowRequest(request);
@@ -1242,6 +1402,7 @@ var SessionHistoryController = class {
1242
1402
  const target = addressId(address);
1243
1403
  const buffered = new Deque();
1244
1404
  let snapshotCursor;
1405
+ let assistantStreamOrdinal = 0;
1245
1406
  let wake;
1246
1407
  const notify = () => {
1247
1408
  const resume = wake;
@@ -1256,13 +1417,28 @@ var SessionHistoryController = class {
1256
1417
  this.closeFollowers.add(close);
1257
1418
  const disposeEvent = this.ctx.on("session/event", (session, event) => {
1258
1419
  if (session.id !== target) return;
1259
- buffered.pushBack(event);
1420
+ buffered.pushBack({
1421
+ type: "event",
1422
+ event
1423
+ });
1260
1424
  notify();
1261
1425
  }, { global: true });
1262
1426
  const disposeCreated = this.ctx.on("session/created", (session) => {
1263
1427
  if (session.id !== target) return;
1264
1428
  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]);
1429
+ for (let index = suffix.length - 1; index >= 0; index -= 1) buffered.pushFront({
1430
+ type: "event",
1431
+ event: suffix[index]
1432
+ });
1433
+ notify();
1434
+ }, { global: true });
1435
+ const disposeAssistantStream = request.assistantStream !== true ? void 0 : this.ctx.on("agent/assistant-stream", ({ agent, frame }) => {
1436
+ if (agent.session.id !== target) return;
1437
+ buffered.pushBack({
1438
+ type: "assistant-stream",
1439
+ frame: wireAssistantStreamFrame(frame, cursorBeforeNext(agent.session.seq)),
1440
+ ordinal: ++assistantStreamOrdinal
1441
+ });
1266
1442
  notify();
1267
1443
  }, { global: true });
1268
1444
  const onAbort = () => {
@@ -1276,22 +1452,25 @@ var SessionHistoryController = class {
1276
1452
  hasError: false
1277
1453
  };
1278
1454
  try {
1279
- const source = __addDisposableResource$3(env_2, await this.sourceFor(address, signal, true), false);
1455
+ const source = __addDisposableResource$2(env_2, await this.sourceFor(address, signal, true), false);
1280
1456
  const events = source.events;
1281
1457
  signal.throwIfAborted();
1282
1458
  const cursor = source.cursor;
1283
1459
  snapshotCursor = cursor;
1284
1460
  const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
1461
+ const assistantStream = request.assistantStream === true ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 } : void 0;
1462
+ const assistantStreamOrdinalCut = assistantStreamOrdinal;
1285
1463
  yield {
1286
1464
  type: "snapshot",
1287
- header: wireHeader(source.header, source.inheritedEventCount),
1465
+ header: wireHeader(source.header),
1288
1466
  cursor,
1289
1467
  records: pageRecords(page.events),
1290
1468
  hasMore: page.hasMore,
1291
1469
  projections: source.projections === void 0 ? {
1292
1470
  asOfSeq: cursor,
1293
1471
  values: {}
1294
- } : projectionBlock(source.projections)
1472
+ } : projectionBlock(source.projections),
1473
+ ...assistantStream === void 0 ? {} : { assistantStream }
1295
1474
  };
1296
1475
  if (address.kind === "session" && source.source === "prepared") {
1297
1476
  const promotion = source.retain();
@@ -1311,23 +1490,31 @@ var SessionHistoryController = class {
1311
1490
  });
1312
1491
  continue;
1313
1492
  }
1493
+ if (item.type === "assistant-stream") {
1494
+ if (item.ordinal > assistantStreamOrdinalCut) yield {
1495
+ type: "assistant-stream",
1496
+ frame: item.frame
1497
+ };
1498
+ continue;
1499
+ }
1314
1500
  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)}`, {});
1501
+ if (item.event.seq < expectedSeq) continue;
1502
+ if (item.event.seq !== expectedSeq) throw new RemoteError("gateway/internal", `session event stream skipped seq ${String(expectedSeq)}`, {});
1317
1503
  nextOffset = SessionLogOffset(nextOffset + 1);
1318
- yield entryFor(item);
1504
+ yield entryFor(item.event);
1319
1505
  }
1320
1506
  } catch (e_2) {
1321
1507
  env_2.error = e_2;
1322
1508
  env_2.hasError = true;
1323
1509
  } finally {
1324
- __disposeResources$3(env_2);
1510
+ __disposeResources$2(env_2);
1325
1511
  }
1326
1512
  } finally {
1327
1513
  this.closeFollowers.delete(close);
1328
1514
  signal.removeEventListener("abort", onAbort);
1329
1515
  disposeCreated();
1330
1516
  disposeEvent();
1517
+ disposeAssistantStream?.();
1331
1518
  }
1332
1519
  }
1333
1520
  async sourceFor(address, signal, withProjections) {
@@ -1354,6 +1541,20 @@ var SessionHistoryController = class {
1354
1541
  }
1355
1542
  }
1356
1543
  };
1544
+ function cursorBeforeNext(nextSeq) {
1545
+ return nextSeq === 0 ? -1 : SessionSeq(nextSeq - 1);
1546
+ }
1547
+ function wireAssistantStreamFrame(frame, durableCursor) {
1548
+ if (frame.type === "start") return {
1549
+ ...frame,
1550
+ startedAfterSeq: durableCursor
1551
+ };
1552
+ if (frame.type === "end") return frame;
1553
+ return {
1554
+ ...frame,
1555
+ chunk: frame.chunk
1556
+ };
1557
+ }
1357
1558
  function projectionBlock(snapshot) {
1358
1559
  return {
1359
1560
  asOfSeq: snapshot.asOfSeq,
@@ -1420,13 +1621,9 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
1420
1621
  hasMore: cut > 0
1421
1622
  };
1422
1623
  }
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
- };
1624
+ /** Translate current logical Session metadata to the browser wire. */
1625
+ function wireHeader(header) {
1626
+ return { ...header };
1430
1627
  }
1431
1628
  function entryFor(event) {
1432
1629
  return {
@@ -1434,40 +1631,9 @@ function entryFor(event) {
1434
1631
  event
1435
1632
  };
1436
1633
  }
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
1634
  /** Encode one bounded logical page without changing its pagination cut. */
1469
1635
  function pageRecords(events) {
1470
- return packChunkRuns(events).map((record) => isChunkRow(record) ? chunkEntryFor(record) : entryFor(record));
1636
+ return events.map(entryFor);
1471
1637
  }
1472
1638
  //#endregion
1473
1639
  //#region lib/types/file-references.js
@@ -1558,67 +1724,6 @@ let SessionFileReferences = (() => {
1558
1724
  //#endregion
1559
1725
  //#region lib/types/list.js
1560
1726
  /** 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
1727
  const SEARCH_PROVIDER_CALL_LIMIT = 100;
1623
1728
  const SESSION_SEARCH_QUERY_MAX_CHARS = 500;
1624
1729
  const MESSAGE_TYPES = new Set(["user/message", "assistant/message"]);
@@ -1667,14 +1772,9 @@ function truncateUnicodeCodePoints(value, maximum) {
1667
1772
  /** Owns list projection registration, bounded cold summaries, and authorized search. */
1668
1773
  var ApiSessionList = class {
1669
1774
  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) {
1775
+ /** @param ctx - Host context carrying Session, query, persistence, and projection services. */
1776
+ constructor(ctx) {
1676
1777
  this.ctx = ctx;
1677
- this.coldBlankProbeMaxBytes = coldBlankProbeMaxBytes;
1678
1778
  ctx.sessionProjections.register({
1679
1779
  key: "sessionListMetadata",
1680
1780
  stateSchema: sessionListMetadataSchema,
@@ -1740,21 +1840,12 @@ var ApiSessionList = class {
1740
1840
  if (record.header.cwd === void 0) continue;
1741
1841
  cold.push(record.header);
1742
1842
  }
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
- }
1843
+ for (const header of cold) items.push(this.summarizeCold(header));
1750
1844
  items.sort((left, right) => right.updatedAt - left.updatedAt);
1751
1845
  return items;
1752
1846
  }
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);
1847
+ summarizeCold(header) {
1848
+ const projections = this.projectionsFor(header, void 0);
1758
1849
  const metadata = projections?.values.sessionListMetadata;
1759
1850
  return {
1760
1851
  sessionId: header.id,
@@ -1765,44 +1856,6 @@ var ApiSessionList = class {
1765
1856
  ...projections === void 0 ? {} : { projections }
1766
1857
  };
1767
1858
  }
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
1859
  /**
1807
1860
  * Search current visible message content without activating any matching Session.
1808
1861
  * @param query - literal message-content query.
@@ -1893,7 +1946,8 @@ var ApiSessionList = class {
1893
1946
  }
1894
1947
  projectionsFor(header, session) {
1895
1948
  try {
1896
- const block = session === void 0 ? header.isSeeded ? void 0 : this.ctx.get("sessionProjectionCache")?.cachedSnapshot(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1949
+ const cache = this.ctx.get("sessionProjectionCache");
1950
+ 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
1951
  return block !== void 0 && Object.keys(block.values).length > 0 ? {
1898
1952
  asOfSeq: block.asOfSeq,
1899
1953
  values: block.values
@@ -2247,6 +2301,79 @@ let SessionSkillCatalog = (() => {
2247
2301
  };
2248
2302
  })();
2249
2303
  //#endregion
2304
+ //#region lib/types/media-references.js
2305
+ /**
2306
+ * Authenticated GET/HEAD /api/file reads bounded file responses through
2307
+ * the composed filesystem provider. Paths and MIME types do not restrict access;
2308
+ * the connection service authenticates requests before this handler.
2309
+ * @module @deepseek-ai/dsh-api-session-controller/media-references
2310
+ */
2311
+ const BASE_HEADERS = {
2312
+ "Cache-Control": "private, no-store",
2313
+ "X-Content-Type-Options": "nosniff",
2314
+ "Content-Security-Policy": "sandbox; default-src 'none'"
2315
+ };
2316
+ async function serveFile(request, fs, maxBytes) {
2317
+ const fail = (status, text) => new Response(request.method === "HEAD" ? null : text, {
2318
+ status,
2319
+ headers: BASE_HEADERS
2320
+ });
2321
+ const path = new URL(request.url).searchParams.get("path");
2322
+ if (path === null || path.length === 0) return fail(400, "missing path");
2323
+ if (path.includes("\0") || !isAbsolute(path)) return fail(400, "absolute path required");
2324
+ try {
2325
+ const target = await fs.resolve(path, { signal: request.signal });
2326
+ const mediaType = mime.lookup(target.displayPath) || "application/octet-stream";
2327
+ const headers = {
2328
+ ...BASE_HEADERS,
2329
+ "Content-Type": mediaType
2330
+ };
2331
+ if (request.method === "HEAD") {
2332
+ const info = await fs.stat(target, request.signal);
2333
+ if (info === void 0) return fail(404, "not found");
2334
+ if (info.type !== "file") return fail(403, "not a regular file");
2335
+ if (info.size !== void 0) {
2336
+ if (info.size > maxBytes) return fail(413, "file exceeds byte limit");
2337
+ headers["Content-Length"] = String(info.size);
2338
+ }
2339
+ return new Response(null, { headers });
2340
+ }
2341
+ const bytes = await fs.readBytes(target, request.signal, maxBytes);
2342
+ headers["Content-Length"] = String(bytes.byteLength);
2343
+ return new Response(bytes.slice(), { headers });
2344
+ } catch (error) {
2345
+ if (!(error instanceof FsError)) throw error;
2346
+ return fail({
2347
+ FS_NOT_FOUND: 404,
2348
+ FS_NOT_REGULAR_FILE: 403,
2349
+ FS_PERMISSION_DENIED: 403,
2350
+ FS_SANDBOX_DENIED: 403,
2351
+ FS_TOO_LARGE: 413,
2352
+ FS_ABORTED: 499
2353
+ }[error.code] ?? 500, error.code);
2354
+ }
2355
+ }
2356
+ /**
2357
+ * File-display contribution. The connection service supplies authentication;
2358
+ * `ctx.fs` supplies the execution world's paths, reads, and access policy.
2359
+ */
2360
+ const SessionMediaReferences = {
2361
+ inject: [
2362
+ "connection",
2363
+ "fs",
2364
+ "attachments"
2365
+ ],
2366
+ apply(ctx) {
2367
+ const maxBytes = ctx.attachments.imageLimits.maxImageBytes;
2368
+ ctx.effect(() => ctx.connection.fetch.register({
2369
+ path: "/api/file",
2370
+ methods: ["GET", "HEAD"],
2371
+ requestBody: "buffered",
2372
+ fetch: (request) => serveFile(request, ctx.fs, maxBytes)
2373
+ }), "session-controller: /api/file");
2374
+ }
2375
+ };
2376
+ //#endregion
2250
2377
  //#region lib/types/index.js
2251
2378
  /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
2252
2379
  var __runInitializers = function(thisArg, initializers, value) {
@@ -2571,6 +2698,7 @@ let SessionController = (() => {
2571
2698
  "agentDefaultModel",
2572
2699
  "agents",
2573
2700
  "attachments",
2701
+ "fileUploads",
2574
2702
  "llm",
2575
2703
  "sessions",
2576
2704
  "sessionProjections",
@@ -2578,10 +2706,7 @@ let SessionController = (() => {
2578
2706
  "typert",
2579
2707
  "workspaceRegistry"
2580
2708
  ];
2581
- static Config = z.object({
2582
- coldBlankProbeMaxBytes: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_BYTES),
2583
- nativeOpen: z.boolean()
2584
- });
2709
+ static Config = z.object({ nativeOpen: z.boolean() });
2585
2710
  agents = __runInitializers(this, _instanceExtraInitializers);
2586
2711
  commands;
2587
2712
  controlState;
@@ -2592,13 +2717,19 @@ let SessionController = (() => {
2592
2717
  promotions = /* @__PURE__ */ new Set();
2593
2718
  /**
2594
2719
  * @param ctx - Host context containing the Session capability assembly.
2595
- * @param config - cold-list observation policy.
2720
+ * @param config - native-opener deployment policy.
2721
+ * @param internals - host integrations replaceable by direct unit tests.
2596
2722
  */
2597
2723
  constructor(ctx, config, internals = {}) {
2598
2724
  super(ctx, "sessionController", { namespace: "session" });
2599
2725
  installModelSelectionProjection(ctx);
2600
2726
  this.agents = new ApiSessionAgentController(ctx);
2601
2727
  this.commands = new SessionCommandController(ctx, this.agents, process.cwd());
2728
+ ctx.effect(() => ctx.fileUploads.registerAgentResolver(async (sessionId) => {
2729
+ const result = await this.agents.resolveAgent(sessionId);
2730
+ if ("error" in result) throw result.error;
2731
+ return result.agent;
2732
+ }), "session-controller: file-upload Agent resolver");
2602
2733
  this.controlState = new SessionControlController(ctx);
2603
2734
  ctx.effect(() => async () => {
2604
2735
  await Promise.allSettled([...this.promotions]);
@@ -2606,10 +2737,11 @@ let SessionController = (() => {
2606
2737
  this.history = new SessionHistoryController(ctx, (observation) => {
2607
2738
  this.promote(observation);
2608
2739
  });
2609
- this.listState = new ApiSessionList(ctx, config.coldBlankProbeMaxBytes ?? 1024);
2740
+ this.listState = new ApiSessionList(ctx);
2610
2741
  this.openPath = internals.openPath ?? openNativePath;
2611
2742
  this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
2612
2743
  ctx.plugin(SessionFileReferences);
2744
+ ctx.plugin(SessionMediaReferences);
2613
2745
  ctx.plugin(SessionSkillCatalog);
2614
2746
  ctx.on("session/created", (session) => {
2615
2747
  ctx.emit("api-session/added", this.listState.summaryFor(session));
@@ -2810,7 +2942,8 @@ let SessionController = (() => {
2810
2942
  * Follow one Session log from its opening or resume cursor.
2811
2943
  * @param request - durable address and last committed sequence already held by the caller.
2812
2944
  * @param signal - cancellation owned by the Remote stream carrier.
2813
- * @returns a complete opening snapshot followed by gap-free event frames.
2945
+ * @returns a complete opening snapshot followed by gap-free durable event
2946
+ * frames and optional cursorless assistant-stream frames.
2814
2947
  */
2815
2948
  follow(request, signal) {
2816
2949
  return this.history.follow(request, signal);