@crazx/dsh-api-session-controller 0.1.2-rc.1.zw.2 → 0.1.5-alpha.1.zw.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 (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 +70 -57
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;
@@ -689,7 +693,7 @@ var SessionCommandController = class {
689
693
  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 });
690
694
  throw new RemoteError("gateway/internal", `fork source unavailable for session "${request.sessionId}": ${String(error)}`, {});
691
695
  }
692
- const source = __addDisposableResource$4(env_1, observed, false);
696
+ const source = __addDisposableResource$3(env_1, observed, false);
693
697
  const lastSeq = source.events.at(-1)?.seq ?? -1;
694
698
  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);
695
699
  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 });
@@ -737,18 +741,20 @@ var SessionCommandController = class {
737
741
  env_1.error = e_1;
738
742
  env_1.hasError = true;
739
743
  } finally {
740
- __disposeResources$4(env_1);
744
+ __disposeResources$3(env_1);
741
745
  }
742
746
  }
743
747
  /**
744
- * Admit one browser prompt after explicit Agent resume and image validation.
748
+ * Reject empty content, then admit one prompt after Agent and attachment validation.
745
749
  * @param request - Session identity, prompt content, source metadata, and delivery mode.
746
750
  * @returns acknowledgement that the Agent accepted the prompt.
747
751
  */
748
752
  async prompt(request) {
753
+ if (!hasPromptContent(request.content)) throw new RemoteError("gateway/bad-request", "prompt content must include non-whitespace text or an attachment", {});
749
754
  const clientTimeZone = request.clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(request.clientTimeZone);
750
755
  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 });
751
756
  const agent = await this.resolveAgent(request.sessionId);
757
+ if (hasPromptRequest(agent, request.requestId)) return { accepted: true };
752
758
  const selection = this.agents.selectionFor(agent).current;
753
759
  if (!routeServed(this.ctx, selection.provider)) throw new RemoteError("session/model-unavailable", `no adapter serves provider "${selection.provider}"; select a model for this session`, {
754
760
  provider: selection.provider,
@@ -762,17 +768,33 @@ var SessionCommandController = class {
762
768
  const hasImage = request.content.some((part) => part.type === "image");
763
769
  const admit = async () => {
764
770
  try {
765
- if (hasImage) {
766
- const current = this.agents.selectionFor(agent).current;
767
- const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
768
- 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" });
771
+ const env_2 = {
772
+ stack: [],
773
+ error: void 0,
774
+ hasError: false
775
+ };
776
+ try {
777
+ if (hasImage) {
778
+ const current = this.agents.selectionFor(agent).current;
779
+ const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model);
780
+ 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" });
781
+ }
782
+ const admission = resolvePromptFileReceipts(request.content, (receiptId) => this.ctx.fileUploads.resolve(agent, receiptId));
783
+ const message = createUserMessage({
784
+ content: await this.ctx.attachments.admitPromptContent(admission.content),
785
+ source
786
+ });
787
+ 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 });
788
+ const binding = __addDisposableResource$3(env_2, this.ctx.fileUploads.bindPrompt(agent, admission.receiptIds, request.requestId), false);
789
+ if (request.mode === "steer") agent.steer(message);
790
+ else agent.followup(message);
791
+ binding.commit();
792
+ } catch (e_2) {
793
+ env_2.error = e_2;
794
+ env_2.hasError = true;
795
+ } finally {
796
+ __disposeResources$3(env_2);
769
797
  }
770
- const message = createUserMessage({
771
- content: await admitPromptContent(this.ctx.attachments, request.content),
772
- source
773
- });
774
- if (request.mode === "steer") agent.steer(message);
775
- else agent.followup(message);
776
798
  } catch (error) {
777
799
  if (remoteErrorOf(error) !== void 0) throw error;
778
800
  if (error instanceof AttachmentError) throw new RemoteError("session/attachment-invalid", error.message, { reason: error.code });
@@ -814,10 +836,16 @@ var SessionCommandController = class {
814
836
  * @returns acknowledgement that the queue mutation was applied.
815
837
  */
816
838
  updateQueue(request) {
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" });
839
+ if (request.action.kind === "edit") {
840
+ 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" });
841
+ if (!hasPromptContent(request.action.content)) throw new RemoteError("gateway/bad-request", "queue edit content must include non-whitespace text", {});
842
+ }
818
843
  const agent = this.ctx.agents.get(request.sessionId);
819
- if (agent !== void 0 && hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) throw apiSessionSubagentOwnershipError(request.sessionId);
820
844
  if (agent === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
845
+ if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
846
+ const identity = this.ctx.sessionProjections.snapshot(agent.session, ["subagent"]).values.subagent;
847
+ if (identity?.mode !== "continuable" || !agent.session.isOwnSeq(identity.seq)) throw apiSessionSubagentOwnershipError(request.sessionId);
848
+ }
821
849
  const nextTurn = agent.inbox.nextTurn.find((message) => message.id === request.itemId);
822
850
  const nextStep = agent.inbox.nextStep.find((message) => message.id === request.itemId);
823
851
  const located = nextTurn === void 0 ? nextStep === void 0 ? void 0 : {
@@ -830,13 +858,25 @@ var SessionCommandController = class {
830
858
  if (located === void 0) throw new RemoteError("session/queue-item-not-found", "queued item is no longer pending", { itemId: request.itemId });
831
859
  const { target, message } = located;
832
860
  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 });
833
- if (request.action.kind === "edit") agent.inbox.replace(request.itemId, freezeMessage({
834
- ...message,
835
- content: [...request.action.content]
836
- }));
837
- else {
838
- agent.inbox.remove(request.itemId);
839
- if (request.action.kind === "steer") agent.steer(message);
861
+ switch (request.action.kind) {
862
+ case "edit":
863
+ agent.inbox.replace(request.itemId, freezeMessage({
864
+ ...message,
865
+ content: [...request.action.content]
866
+ }));
867
+ break;
868
+ case "remove": {
869
+ agent.inbox.remove(request.itemId);
870
+ const source = message.source;
871
+ if (source.kind === "user" && "rpcId" in source) this.ctx.fileUploads.retirePrompt(agent, source.rpcId);
872
+ break;
873
+ }
874
+ case "steer":
875
+ agent.inbox.remove(request.itemId);
876
+ agent.steer(message);
877
+ break;
878
+ /* v8 ignore next 2 -- closed-union exhaustiveness guard */
879
+ default: assertNever(request.action, "queue action");
840
880
  }
841
881
  return { accepted: true };
842
882
  }
@@ -897,6 +937,34 @@ var SessionCommandController = class {
897
937
  }
898
938
  }
899
939
  };
940
+ function resolvePromptFileReceipts(content, stagedFile) {
941
+ const receiptIds = /* @__PURE__ */ new Set();
942
+ return {
943
+ content: content.map((part) => {
944
+ if (part.type !== "file") return part;
945
+ const attachment = stagedFile(part.receiptId);
946
+ if (attachment === void 0) throw new RemoteError("session/attachment-invalid", "File was not uploaded for this session.", { reason: "FILE_NOT_STAGED" });
947
+ receiptIds.add(part.receiptId);
948
+ return {
949
+ type: "file",
950
+ attachment
951
+ };
952
+ }),
953
+ receiptIds: [...receiptIds]
954
+ };
955
+ }
956
+ function hasPromptRequest(agent, requestId) {
957
+ const matches = (message) => {
958
+ const source = message.source;
959
+ return source.kind === "user" && "rpcId" in source && source.rpcId === requestId;
960
+ };
961
+ if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches)) return true;
962
+ return agent.session.snapshotEvents().some((event) => {
963
+ if (event.type !== "user/message") return false;
964
+ const source = event.data.source;
965
+ return source.kind === "user" && "rpcId" in source && source.rpcId === requestId;
966
+ });
967
+ }
900
968
  function imageBlockIn(content, match) {
901
969
  if (!Array.isArray(content)) return void 0;
902
970
  for (const value of content) {
@@ -922,7 +990,10 @@ function imageInEvent(event, match) {
922
990
  const found = imageBlockIn(inserted.content, match);
923
991
  if (found !== void 0) return found;
924
992
  }
925
- return event.type === "assistant/chunk" && data.chunk?.type === "block-end" ? imageBlockIn([data.chunk.block], match) : void 0;
993
+ if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) {
994
+ const found = imageBlockIn([chunk.block], match);
995
+ if (found !== void 0) return found;
996
+ }
926
997
  }
927
998
  function referencedImage(events, attachmentId) {
928
999
  for (const event of events) {
@@ -943,9 +1014,6 @@ var SessionControlController = class {
943
1014
  /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
944
1015
  constructor(ctx) {
945
1016
  this.ctx = ctx;
946
- ctx.on("session/event", (session, event) => {
947
- this.onSessionEvent(session, event);
948
- });
949
1017
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
950
1018
  this.broadcast({
951
1019
  type: "projection",
@@ -954,6 +1022,13 @@ var SessionControlController = class {
954
1022
  value,
955
1023
  seq
956
1024
  });
1025
+ if (key !== "inbox") return;
1026
+ if (this.ctx.agents.get(session.id)?.session !== session) return;
1027
+ this.broadcast({
1028
+ type: "queue",
1029
+ sessionId: session.id,
1030
+ items: queueItemsFromInbox(value)
1031
+ });
957
1032
  });
958
1033
  ctx.inject(["jobs"], (jobsCtx) => {
959
1034
  jobsCtx.jobs.onJobsChanged((owner) => {
@@ -1019,16 +1094,6 @@ var SessionControlController = class {
1019
1094
  }
1020
1095
  return blocks;
1021
1096
  }
1022
- onSessionEvent(session, event) {
1023
- if (event.type !== "agent/inbox/spliced") return;
1024
- const agent = this.ctx.agents.get(session.id);
1025
- if (agent?.session !== session) return;
1026
- this.broadcast({
1027
- type: "queue",
1028
- sessionId: session.id,
1029
- items: queueItems(agent, event.data)
1030
- });
1031
- }
1032
1097
  onJobsChanged(owner) {
1033
1098
  if (owner !== void 0) {
1034
1099
  this.broadcast({
@@ -1093,12 +1158,14 @@ var ControlQueue = class {
1093
1158
  }
1094
1159
  }
1095
1160
  };
1096
- function queueItems(agent, splice) {
1097
- const project = (target) => {
1098
- const messages = target === "next-turn" ? agent.inbox.nextTurn : agent.inbox.nextStep;
1099
- return splice?.target === target ? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted) : messages;
1100
- };
1101
- return [...project("next-turn").map((message) => ({
1161
+ function queueItems(agent) {
1162
+ return queueItemsFromInbox({
1163
+ "next-turn": agent.inbox.nextTurn,
1164
+ "next-step": agent.inbox.nextStep
1165
+ });
1166
+ }
1167
+ function queueItemsFromInbox(inbox) {
1168
+ return [...inbox["next-turn"].map((message) => ({
1102
1169
  id: message.id,
1103
1170
  placement: "queued",
1104
1171
  ...promptRpcId(message),
@@ -1106,7 +1173,7 @@ function queueItems(agent, splice) {
1106
1173
  id: message.id,
1107
1174
  content: message.content
1108
1175
  }
1109
- })), ...project("next-step").map((message) => ({
1176
+ })), ...inbox["next-step"].map((message) => ({
1110
1177
  id: message.id,
1111
1178
  placement: message.source.kind === "user" ? "steering" : "context",
1112
1179
  ...promptRpcId(message),
@@ -1133,9 +1200,90 @@ function jobView(job) {
1133
1200
  };
1134
1201
  }
1135
1202
  //#endregion
1203
+ //#region lib/types/assistant-stream.js
1204
+ /** Process-local assistant state retained for reconnecting Web followers. */
1205
+ const EMPTY_BASELINE = { revision: 0 };
1206
+ /**
1207
+ * Folds dense Agent frames and materializes one shared immutable reconnect
1208
+ * baseline per accepted revision.
1209
+ */
1210
+ var SessionAssistantStreamAccumulator = class {
1211
+ activeAttempt;
1212
+ revision = 0;
1213
+ snapshotValue = EMPTY_BASELINE;
1214
+ dirty = false;
1215
+ /**
1216
+ * Fold one trusted frame from the current attached Agent lifecycle.
1217
+ * @param frame - next dense process-local Assistant frame.
1218
+ * @param durableCursor - last committed Session seq when this frame was observed.
1219
+ */
1220
+ accept(frame, durableCursor) {
1221
+ if (frame.type === "start" && frame.revision === 1 && this.revision !== 0) {
1222
+ this.activeAttempt = void 0;
1223
+ this.revision = 0;
1224
+ }
1225
+ if (frame.revision !== this.revision + 1) {
1226
+ this.activeAttempt = void 0;
1227
+ this.revision = frame.revision;
1228
+ this.dirty = true;
1229
+ return;
1230
+ }
1231
+ this.revision = frame.revision;
1232
+ switch (frame.type) {
1233
+ case "start":
1234
+ this.activeAttempt = {
1235
+ attemptId: frame.attemptId,
1236
+ startedAfterSeq: durableCursor,
1237
+ turn: frame.turn,
1238
+ step: frame.step,
1239
+ stream: new AssistantStreamAccumulator(),
1240
+ nextIndex: 0
1241
+ };
1242
+ break;
1243
+ case "chunk": {
1244
+ const attempt = this.activeAttempt;
1245
+ if (attempt === void 0 || attempt.attemptId !== frame.attemptId || frame.index !== attempt.nextIndex) {
1246
+ this.activeAttempt = void 0;
1247
+ break;
1248
+ }
1249
+ attempt.stream.push({
1250
+ time: frame.time,
1251
+ chunk: frame.chunk
1252
+ });
1253
+ attempt.nextIndex += 1;
1254
+ break;
1255
+ }
1256
+ case "end":
1257
+ this.activeAttempt = void 0;
1258
+ break;
1259
+ }
1260
+ this.dirty = true;
1261
+ }
1262
+ /**
1263
+ * Read the cached reconnect baseline, materializing it after a state change.
1264
+ * @returns the identity-stable baseline for the latest accepted revision.
1265
+ */
1266
+ snapshot() {
1267
+ if (!this.dirty) return this.snapshotValue;
1268
+ this.snapshotValue = {
1269
+ revision: this.revision,
1270
+ ...this.activeAttempt === void 0 ? {} : { activeAttempt: {
1271
+ attemptId: this.activeAttempt.attemptId,
1272
+ startedAfterSeq: this.activeAttempt.startedAfterSeq,
1273
+ turn: this.activeAttempt.turn,
1274
+ step: this.activeAttempt.step,
1275
+ nextIndex: this.activeAttempt.nextIndex,
1276
+ stream: this.activeAttempt.stream.snapshot()
1277
+ } }
1278
+ };
1279
+ this.dirty = false;
1280
+ return this.snapshotValue;
1281
+ }
1282
+ };
1283
+ //#endregion
1136
1284
  //#region lib/types/history.js
1137
1285
  /** Cold Session history pagination and live-event source. */
1138
- var __addDisposableResource$3 = function(env, value, async) {
1286
+ var __addDisposableResource$2 = function(env, value, async) {
1139
1287
  if (value !== null && value !== void 0) {
1140
1288
  if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1141
1289
  var dispose, inner;
@@ -1164,7 +1312,7 @@ var __addDisposableResource$3 = function(env, value, async) {
1164
1312
  } else if (async) env.stack.push({ async: true });
1165
1313
  return value;
1166
1314
  };
1167
- var __disposeResources$3 = (function(SuppressedError) {
1315
+ var __disposeResources$2 = (function(SuppressedError) {
1168
1316
  return function(env) {
1169
1317
  function fail(e) {
1170
1318
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
@@ -1200,6 +1348,7 @@ var SessionHistoryController = class {
1200
1348
  ctx;
1201
1349
  promote;
1202
1350
  closeFollowers = /* @__PURE__ */ new Set();
1351
+ assistantStreams = /* @__PURE__ */ new Map();
1203
1352
  /**
1204
1353
  * @param ctx - Host context carrying Session query and projection services.
1205
1354
  * @param promote - starts ordinary Session activation after snapshot delivery.
@@ -1207,6 +1356,17 @@ var SessionHistoryController = class {
1207
1356
  constructor(ctx, promote) {
1208
1357
  this.ctx = ctx;
1209
1358
  this.promote = promote;
1359
+ ctx.on("agent/assistant-stream", ({ agent, frame }) => {
1360
+ let stream = this.assistantStreams.get(agent.session.id);
1361
+ if (stream === void 0) {
1362
+ stream = new SessionAssistantStreamAccumulator();
1363
+ this.assistantStreams.set(agent.session.id, stream);
1364
+ }
1365
+ stream.accept(frame, cursorBeforeNext(agent.session.seq));
1366
+ }, { global: true });
1367
+ ctx.on("agent/disposed", ({ agent }) => {
1368
+ this.assistantStreams.delete(agent.session.id);
1369
+ }, { global: true });
1210
1370
  ctx.effect(() => () => {
1211
1371
  for (const close of this.closeFollowers) close();
1212
1372
  this.closeFollowers.clear();
@@ -1228,7 +1388,7 @@ var SessionHistoryController = class {
1228
1388
  validatePageRequest(request);
1229
1389
  const throughSeq = request.throughSeq === -1 ? -1 : SessionSeq(request.throughSeq);
1230
1390
  const beforeSeq = request.beforeSeq === void 0 ? void 0 : SessionLogOffset(request.beforeSeq);
1231
- const source = __addDisposableResource$3(env_1, await this.sourceFor(request.address, signal, false), false);
1391
+ const source = __addDisposableResource$2(env_1, await this.sourceFor(request.address, signal, false), false);
1232
1392
  signal.throwIfAborted();
1233
1393
  const sourceLog = source.events;
1234
1394
  const sourceCursor = sourceLog.at(-1)?.seq ?? -1;
@@ -1244,14 +1404,14 @@ var SessionHistoryController = class {
1244
1404
  env_1.error = e_1;
1245
1405
  env_1.hasError = true;
1246
1406
  } finally {
1247
- __disposeResources$3(env_1);
1407
+ __disposeResources$2(env_1);
1248
1408
  }
1249
1409
  }
1250
1410
  /**
1251
1411
  * Follow events appended after an initial cursor on one durable address.
1252
1412
  * @param request - durable address and last committed sequence already held by the caller.
1253
1413
  * @param signal - stream cancellation owned by the Remote carrier.
1254
- * @returns a complete opening snapshot followed by gap-free event frames.
1414
+ * @returns a complete opening snapshot followed by gap-free durable events and opted-in assistant frames.
1255
1415
  */
1256
1416
  async *follow(request, signal) {
1257
1417
  validateFollowRequest(request);
@@ -1259,6 +1419,7 @@ var SessionHistoryController = class {
1259
1419
  const target = addressId(address);
1260
1420
  const buffered = new Deque();
1261
1421
  let snapshotCursor;
1422
+ let assistantStreamOrdinal = 0;
1262
1423
  let wake;
1263
1424
  const notify = () => {
1264
1425
  const resume = wake;
@@ -1273,13 +1434,28 @@ var SessionHistoryController = class {
1273
1434
  this.closeFollowers.add(close);
1274
1435
  const disposeEvent = this.ctx.on("session/event", (session, event) => {
1275
1436
  if (session.id !== target) return;
1276
- buffered.pushBack(event);
1437
+ buffered.pushBack({
1438
+ type: "event",
1439
+ event
1440
+ });
1277
1441
  notify();
1278
1442
  }, { global: true });
1279
1443
  const disposeCreated = this.ctx.on("session/created", (session) => {
1280
1444
  if (session.id !== target) return;
1281
1445
  const suffix = session.snapshotEvents(snapshotCursor === void 0 ? session.firstLiveSeq : SessionLogOffset(snapshotCursor + 1));
1282
- for (let index = suffix.length - 1; index >= 0; index -= 1) buffered.pushFront(suffix[index]);
1446
+ for (let index = suffix.length - 1; index >= 0; index -= 1) buffered.pushFront({
1447
+ type: "event",
1448
+ event: suffix[index]
1449
+ });
1450
+ notify();
1451
+ }, { global: true });
1452
+ const disposeAssistantStream = request.assistantStream !== true ? void 0 : this.ctx.on("agent/assistant-stream", ({ agent, frame }) => {
1453
+ if (agent.session.id !== target) return;
1454
+ buffered.pushBack({
1455
+ type: "assistant-stream",
1456
+ frame: wireAssistantStreamFrame(frame, cursorBeforeNext(agent.session.seq)),
1457
+ ordinal: ++assistantStreamOrdinal
1458
+ });
1283
1459
  notify();
1284
1460
  }, { global: true });
1285
1461
  const onAbort = () => {
@@ -1293,22 +1469,25 @@ var SessionHistoryController = class {
1293
1469
  hasError: false
1294
1470
  };
1295
1471
  try {
1296
- const source = __addDisposableResource$3(env_2, await this.sourceFor(address, signal, true), false);
1472
+ const source = __addDisposableResource$2(env_2, await this.sourceFor(address, signal, true), false);
1297
1473
  const events = source.events;
1298
1474
  signal.throwIfAborted();
1299
1475
  const cursor = source.cursor;
1300
1476
  snapshotCursor = cursor;
1301
1477
  const page = paginate(events, void 0, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
1478
+ const assistantStream = request.assistantStream === true ? this.assistantStreams.get(target)?.snapshot() ?? { revision: 0 } : void 0;
1479
+ const assistantStreamOrdinalCut = assistantStreamOrdinal;
1302
1480
  yield {
1303
1481
  type: "snapshot",
1304
- header: wireHeader(source.header, source.inheritedEventCount),
1482
+ header: wireHeader(source.header),
1305
1483
  cursor,
1306
1484
  records: pageRecords(page.events),
1307
1485
  hasMore: page.hasMore,
1308
1486
  projections: source.projections === void 0 ? {
1309
1487
  asOfSeq: cursor,
1310
1488
  values: {}
1311
- } : projectionBlock(source.projections)
1489
+ } : projectionBlock(source.projections),
1490
+ ...assistantStream === void 0 ? {} : { assistantStream }
1312
1491
  };
1313
1492
  if (address.kind === "session" && source.source === "prepared") {
1314
1493
  const promotion = source.retain();
@@ -1328,23 +1507,31 @@ var SessionHistoryController = class {
1328
1507
  });
1329
1508
  continue;
1330
1509
  }
1510
+ if (item.type === "assistant-stream") {
1511
+ if (item.ordinal > assistantStreamOrdinalCut) yield {
1512
+ type: "assistant-stream",
1513
+ frame: item.frame
1514
+ };
1515
+ continue;
1516
+ }
1331
1517
  const expectedSeq = SessionSeq(nextOffset);
1332
- if (item.seq < expectedSeq) continue;
1333
- if (item.seq !== expectedSeq) throw new RemoteError("gateway/internal", `session event stream skipped seq ${String(expectedSeq)}`, {});
1518
+ if (item.event.seq < expectedSeq) continue;
1519
+ if (item.event.seq !== expectedSeq) throw new RemoteError("gateway/internal", `session event stream skipped seq ${String(expectedSeq)}`, {});
1334
1520
  nextOffset = SessionLogOffset(nextOffset + 1);
1335
- yield entryFor(item);
1521
+ yield entryFor(item.event);
1336
1522
  }
1337
1523
  } catch (e_2) {
1338
1524
  env_2.error = e_2;
1339
1525
  env_2.hasError = true;
1340
1526
  } finally {
1341
- __disposeResources$3(env_2);
1527
+ __disposeResources$2(env_2);
1342
1528
  }
1343
1529
  } finally {
1344
1530
  this.closeFollowers.delete(close);
1345
1531
  signal.removeEventListener("abort", onAbort);
1346
1532
  disposeCreated();
1347
1533
  disposeEvent();
1534
+ disposeAssistantStream?.();
1348
1535
  }
1349
1536
  }
1350
1537
  async sourceFor(address, signal, withProjections) {
@@ -1371,6 +1558,20 @@ var SessionHistoryController = class {
1371
1558
  }
1372
1559
  }
1373
1560
  };
1561
+ function cursorBeforeNext(nextSeq) {
1562
+ return nextSeq === 0 ? -1 : SessionSeq(nextSeq - 1);
1563
+ }
1564
+ function wireAssistantStreamFrame(frame, durableCursor) {
1565
+ if (frame.type === "start") return {
1566
+ ...frame,
1567
+ startedAfterSeq: durableCursor
1568
+ };
1569
+ if (frame.type === "end") return frame;
1570
+ return {
1571
+ ...frame,
1572
+ chunk: frame.chunk
1573
+ };
1574
+ }
1374
1575
  function projectionBlock(snapshot) {
1375
1576
  return {
1376
1577
  asOfSeq: snapshot.asOfSeq,
@@ -1437,13 +1638,9 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
1437
1638
  hasMore: cut > 0
1438
1639
  };
1439
1640
  }
1440
- /** Translate logical Session metadata to the unchanged v0 browser wire. */
1441
- function wireHeader(header, inheritedEventCount) {
1442
- const { isSeeded, ...wire } = header;
1443
- return {
1444
- ...wire,
1445
- ...isSeeded ? { seedLength: inheritedEventCount } : {}
1446
- };
1641
+ /** Translate current logical Session metadata to the browser wire. */
1642
+ function wireHeader(header) {
1643
+ return { ...header };
1447
1644
  }
1448
1645
  function entryFor(event) {
1449
1646
  return {
@@ -1451,40 +1648,9 @@ function entryFor(event) {
1451
1648
  event
1452
1649
  };
1453
1650
  }
1454
- function chunkEntryFor(row) {
1455
- switch (row.type) {
1456
- case "text-chunks": return {
1457
- type: "chunks",
1458
- event: {
1459
- type: "chunkrow/text-chunks",
1460
- seq: row.seq0,
1461
- time: row.time0,
1462
- data: row.data
1463
- }
1464
- };
1465
- case "reasoning-chunks": return {
1466
- type: "chunks",
1467
- event: {
1468
- type: "chunkrow/reasoning-chunks",
1469
- seq: row.seq0,
1470
- time: row.time0,
1471
- data: row.data
1472
- }
1473
- };
1474
- case "tool-call-chunks": return {
1475
- type: "chunks",
1476
- event: {
1477
- type: "chunkrow/tool-call-chunks",
1478
- seq: row.seq0,
1479
- time: row.time0,
1480
- data: row.data
1481
- }
1482
- };
1483
- }
1484
- }
1485
1651
  /** Encode one bounded logical page without changing its pagination cut. */
1486
1652
  function pageRecords(events) {
1487
- return packChunkRuns(events).map((record) => isChunkRow(record) ? chunkEntryFor(record) : entryFor(record));
1653
+ return events.map(entryFor);
1488
1654
  }
1489
1655
  //#endregion
1490
1656
  //#region lib/types/file-references.js
@@ -1575,67 +1741,6 @@ let SessionFileReferences = (() => {
1575
1741
  //#endregion
1576
1742
  //#region lib/types/list.js
1577
1743
  /** Cold-safe Session list and search projection. */
1578
- var __addDisposableResource$2 = function(env, value, async) {
1579
- if (value !== null && value !== void 0) {
1580
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
1581
- var dispose, inner;
1582
- if (async) {
1583
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
1584
- dispose = value[Symbol.asyncDispose];
1585
- }
1586
- if (dispose === void 0) {
1587
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
1588
- dispose = value[Symbol.dispose];
1589
- if (async) inner = dispose;
1590
- }
1591
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
1592
- if (inner) dispose = function() {
1593
- try {
1594
- inner.call(this);
1595
- } catch (e) {
1596
- return Promise.reject(e);
1597
- }
1598
- };
1599
- env.stack.push({
1600
- value,
1601
- dispose,
1602
- async
1603
- });
1604
- } else if (async) env.stack.push({ async: true });
1605
- return value;
1606
- };
1607
- var __disposeResources$2 = (function(SuppressedError) {
1608
- return function(env) {
1609
- function fail(e) {
1610
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
1611
- env.hasError = true;
1612
- }
1613
- var r, s = 0;
1614
- function next() {
1615
- while (r = env.stack.pop()) try {
1616
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
1617
- if (r.dispose) {
1618
- var result = r.dispose.call(r.value);
1619
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
1620
- fail(e);
1621
- return next();
1622
- });
1623
- } else s |= 1;
1624
- } catch (e) {
1625
- fail(e);
1626
- }
1627
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
1628
- if (env.hasError) throw env.error;
1629
- }
1630
- return next();
1631
- };
1632
- })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
1633
- var e = new Error(message);
1634
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1635
- });
1636
- /** Default maximum artifact size eligible for one cold projection observation. */
1637
- const DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024;
1638
- const COLD_SUMMARY_BATCH_SIZE = 16;
1639
1744
  const SEARCH_PROVIDER_CALL_LIMIT = 100;
1640
1745
  const SESSION_SEARCH_QUERY_MAX_CHARS = 500;
1641
1746
  const MESSAGE_TYPES = new Set(["user/message", "assistant/message"]);
@@ -1684,14 +1789,9 @@ function truncateUnicodeCodePoints(value, maximum) {
1684
1789
  /** Owns list projection registration, bounded cold summaries, and authorized search. */
1685
1790
  var ApiSessionList = class {
1686
1791
  ctx;
1687
- coldBlankProbeMaxBytes;
1688
- /**
1689
- * @param ctx - Host context carrying Session, query, persistence, and projection services.
1690
- * @param coldBlankProbeMaxBytes - maximum physical artifact size eligible for a full observation.
1691
- */
1692
- constructor(ctx, coldBlankProbeMaxBytes) {
1792
+ /** @param ctx - Host context carrying Session, query, persistence, and projection services. */
1793
+ constructor(ctx) {
1693
1794
  this.ctx = ctx;
1694
- this.coldBlankProbeMaxBytes = coldBlankProbeMaxBytes;
1695
1795
  ctx.sessionProjections.register({
1696
1796
  key: "sessionListMetadata",
1697
1797
  stateSchema: sessionListMetadataSchema,
@@ -1757,21 +1857,12 @@ var ApiSessionList = class {
1757
1857
  if (record.header.cwd === void 0) continue;
1758
1858
  cold.push(record.header);
1759
1859
  }
1760
- for (let offset = 0; offset < cold.length; offset += COLD_SUMMARY_BATCH_SIZE) {
1761
- const settled = await Promise.allSettled(cold.slice(offset, offset + COLD_SUMMARY_BATCH_SIZE).map((header) => this.summarizeCold(header, signal)));
1762
- for (const result of settled) {
1763
- if (result.status === "rejected") throw result.reason;
1764
- items.push(result.value);
1765
- }
1766
- }
1860
+ for (const header of cold) items.push(this.summarizeCold(header));
1767
1861
  items.sort((left, right) => right.updatedAt - left.updatedAt);
1768
1862
  return items;
1769
1863
  }
1770
- async summarizeCold(header, signal) {
1771
- const cached = this.projectionsFor(header, void 0);
1772
- const projections = cached?.values.sessionListMetadata?.blank === false ? cached : await this.probeSmallCold(header, signal) ?? cached;
1773
- const raced = this.ctx.sessions.get(header.id);
1774
- if (raced !== void 0) return this.summaryFor(raced);
1864
+ summarizeCold(header) {
1865
+ const projections = this.projectionsFor(header, void 0);
1775
1866
  const metadata = projections?.values.sessionListMetadata;
1776
1867
  return {
1777
1868
  sessionId: header.id,
@@ -1782,44 +1873,6 @@ var ApiSessionList = class {
1782
1873
  ...projections === void 0 ? {} : { projections }
1783
1874
  };
1784
1875
  }
1785
- async probeSmallCold(header, signal) {
1786
- if (this.coldBlankProbeMaxBytes === 0) return void 0;
1787
- const location = this.ctx.get("sessionPersistence")?.locate(header);
1788
- if (location === void 0) return void 0;
1789
- signal?.throwIfAborted();
1790
- try {
1791
- if ((await stat(location.path)).size > this.coldBlankProbeMaxBytes) return void 0;
1792
- } catch {
1793
- signal?.throwIfAborted();
1794
- return;
1795
- }
1796
- try {
1797
- const env_1 = {
1798
- stack: [],
1799
- error: void 0,
1800
- hasError: false
1801
- };
1802
- try {
1803
- const block = __addDisposableResource$2(env_1, await this.ctx.sessionQuery.observeSession(header.id, {
1804
- ...signal === void 0 ? {} : { signal },
1805
- projectionMode: "all"
1806
- }), false).projections;
1807
- return block === void 0 ? void 0 : {
1808
- asOfSeq: block.asOfSeq,
1809
- values: block.values
1810
- };
1811
- } catch (e_1) {
1812
- env_1.error = e_1;
1813
- env_1.hasError = true;
1814
- } finally {
1815
- __disposeResources$2(env_1);
1816
- }
1817
- } catch (error) {
1818
- signal?.throwIfAborted();
1819
- this.ctx.logger.warn(`api-session.list: small cold observation for "${header.id}" failed; serving it as visible: ${String(error)}`);
1820
- return;
1821
- }
1822
- }
1823
1876
  /**
1824
1877
  * Search current visible message content without activating any matching Session.
1825
1878
  * @param query - literal message-content query.
@@ -1910,7 +1963,8 @@ var ApiSessionList = class {
1910
1963
  }
1911
1964
  projectionsFor(header, session) {
1912
1965
  try {
1913
- const block = session === void 0 ? header.isSeeded ? void 0 : this.ctx.get("sessionProjectionCache")?.cachedSnapshot(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1966
+ const cache = this.ctx.get("sessionProjectionCache");
1967
+ const block = session === void 0 ? header.isSeeded ? void 0 : cache?.cachedSnapshot(header, SessionLogOffset(0)) ?? cache?.cachedPredecessorTitle(header, SessionLogOffset(0)) : this.ctx.sessionProjections.cachedSnapshot(session);
1914
1968
  return block !== void 0 && Object.keys(block.values).length > 0 ? {
1915
1969
  asOfSeq: block.asOfSeq,
1916
1970
  values: block.values
@@ -2264,6 +2318,79 @@ let SessionSkillCatalog = (() => {
2264
2318
  };
2265
2319
  })();
2266
2320
  //#endregion
2321
+ //#region lib/types/media-references.js
2322
+ /**
2323
+ * Authenticated GET/HEAD /api/file reads bounded file responses through
2324
+ * the composed filesystem provider. Paths and MIME types do not restrict access;
2325
+ * the connection service authenticates requests before this handler.
2326
+ * @module @deepseek-ai/dsh-api-session-controller/media-references
2327
+ */
2328
+ const BASE_HEADERS = {
2329
+ "Cache-Control": "private, no-store",
2330
+ "X-Content-Type-Options": "nosniff",
2331
+ "Content-Security-Policy": "sandbox; default-src 'none'"
2332
+ };
2333
+ async function serveFile(request, fs, maxBytes) {
2334
+ const fail = (status, text) => new Response(request.method === "HEAD" ? null : text, {
2335
+ status,
2336
+ headers: BASE_HEADERS
2337
+ });
2338
+ const path = new URL(request.url).searchParams.get("path");
2339
+ if (path === null || path.length === 0) return fail(400, "missing path");
2340
+ if (path.includes("\0") || !isAbsolute(path)) return fail(400, "absolute path required");
2341
+ try {
2342
+ const target = await fs.resolve(path, { signal: request.signal });
2343
+ const mediaType = mime.lookup(target.displayPath) || "application/octet-stream";
2344
+ const headers = {
2345
+ ...BASE_HEADERS,
2346
+ "Content-Type": mediaType
2347
+ };
2348
+ if (request.method === "HEAD") {
2349
+ const info = await fs.stat(target, request.signal);
2350
+ if (info === void 0) return fail(404, "not found");
2351
+ if (info.type !== "file") return fail(403, "not a regular file");
2352
+ if (info.size !== void 0) {
2353
+ if (info.size > maxBytes) return fail(413, "file exceeds byte limit");
2354
+ headers["Content-Length"] = String(info.size);
2355
+ }
2356
+ return new Response(null, { headers });
2357
+ }
2358
+ const bytes = await fs.readBytes(target, request.signal, maxBytes);
2359
+ headers["Content-Length"] = String(bytes.byteLength);
2360
+ return new Response(bytes.slice(), { headers });
2361
+ } catch (error) {
2362
+ if (!(error instanceof FsError)) throw error;
2363
+ return fail({
2364
+ FS_NOT_FOUND: 404,
2365
+ FS_NOT_REGULAR_FILE: 403,
2366
+ FS_PERMISSION_DENIED: 403,
2367
+ FS_SANDBOX_DENIED: 403,
2368
+ FS_TOO_LARGE: 413,
2369
+ FS_ABORTED: 499
2370
+ }[error.code] ?? 500, error.code);
2371
+ }
2372
+ }
2373
+ /**
2374
+ * File-display contribution. The connection service supplies authentication;
2375
+ * `ctx.fs` supplies the execution world's paths, reads, and access policy.
2376
+ */
2377
+ const SessionMediaReferences = {
2378
+ inject: [
2379
+ "connection",
2380
+ "fs",
2381
+ "attachments"
2382
+ ],
2383
+ apply(ctx) {
2384
+ const maxBytes = ctx.attachments.imageLimits.maxImageBytes;
2385
+ ctx.effect(() => ctx.connection.fetch.register({
2386
+ path: "/api/file",
2387
+ methods: ["GET", "HEAD"],
2388
+ requestBody: "buffered",
2389
+ fetch: (request) => serveFile(request, ctx.fs, maxBytes)
2390
+ }), "session-controller: /api/file");
2391
+ }
2392
+ };
2393
+ //#endregion
2267
2394
  //#region lib/types/index.js
2268
2395
  /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
2269
2396
  var __runInitializers = function(thisArg, initializers, value) {
@@ -2588,6 +2715,7 @@ let SessionController = (() => {
2588
2715
  "agentDefaultModel",
2589
2716
  "agents",
2590
2717
  "attachments",
2718
+ "fileUploads",
2591
2719
  "llm",
2592
2720
  "sessions",
2593
2721
  "sessionProjections",
@@ -2595,10 +2723,7 @@ let SessionController = (() => {
2595
2723
  "typert",
2596
2724
  "workspaceRegistry"
2597
2725
  ];
2598
- static Config = z.object({
2599
- coldBlankProbeMaxBytes: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_BYTES),
2600
- nativeOpen: z.boolean()
2601
- });
2726
+ static Config = z.object({ nativeOpen: z.boolean() });
2602
2727
  agents = __runInitializers(this, _instanceExtraInitializers);
2603
2728
  commands;
2604
2729
  controlState;
@@ -2609,13 +2734,19 @@ let SessionController = (() => {
2609
2734
  promotions = /* @__PURE__ */ new Set();
2610
2735
  /**
2611
2736
  * @param ctx - Host context containing the Session capability assembly.
2612
- * @param config - cold-list observation policy.
2737
+ * @param config - native-opener deployment policy.
2738
+ * @param internals - host integrations replaceable by direct unit tests.
2613
2739
  */
2614
2740
  constructor(ctx, config, internals = {}) {
2615
2741
  super(ctx, "sessionController", { namespace: "session" });
2616
2742
  installModelSelectionProjection(ctx);
2617
2743
  this.agents = new ApiSessionAgentController(ctx);
2618
2744
  this.commands = new SessionCommandController(ctx, this.agents, process.cwd());
2745
+ ctx.effect(() => ctx.fileUploads.registerAgentResolver(async (sessionId) => {
2746
+ const result = await this.agents.resolveAgent(sessionId);
2747
+ if ("error" in result) throw result.error;
2748
+ return result.agent;
2749
+ }), "session-controller: file-upload Agent resolver");
2619
2750
  this.controlState = new SessionControlController(ctx);
2620
2751
  ctx.effect(() => async () => {
2621
2752
  await Promise.allSettled([...this.promotions]);
@@ -2623,10 +2754,11 @@ let SessionController = (() => {
2623
2754
  this.history = new SessionHistoryController(ctx, (observation) => {
2624
2755
  this.promote(observation);
2625
2756
  });
2626
- this.listState = new ApiSessionList(ctx, config.coldBlankProbeMaxBytes ?? 1024);
2757
+ this.listState = new ApiSessionList(ctx);
2627
2758
  this.openPath = internals.openPath ?? openNativePath;
2628
2759
  this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath()));
2629
2760
  ctx.plugin(SessionFileReferences);
2761
+ ctx.plugin(SessionMediaReferences);
2630
2762
  ctx.plugin(SessionSkillCatalog);
2631
2763
  ctx.on("session/created", (session) => {
2632
2764
  ctx.emit("api-session/added", this.listState.summaryFor(session));
@@ -2827,7 +2959,8 @@ let SessionController = (() => {
2827
2959
  * Follow one Session log from its opening or resume cursor.
2828
2960
  * @param request - durable address and last committed sequence already held by the caller.
2829
2961
  * @param signal - cancellation owned by the Remote stream carrier.
2830
- * @returns a complete opening snapshot followed by gap-free event frames.
2962
+ * @returns a complete opening snapshot followed by gap-free durable event
2963
+ * frames and optional cursorless assistant-stream frames.
2831
2964
  */
2832
2965
  follow(request, signal) {
2833
2966
  return this.history.follow(request, signal);