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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +6 -6
  3. package/README.zh.md +6 -6
  4. package/lib/client.js +592 -28
  5. package/lib/index.js +269 -214
  6. package/lib/typert.host.js +183 -141
  7. package/lib/typert.remote-client.js +79 -97
  8. package/lib/types/assistant-stream.d.ts +26 -0
  9. package/lib/types/assistant-stream.js +83 -0
  10. package/lib/types/client/contract/events.d.ts +38 -7
  11. package/lib/types/client/contract/events.js +21 -0
  12. package/lib/types/client/contract/session.d.ts +7 -7
  13. package/lib/types/client/contract/snapshot.d.ts +15 -2
  14. package/lib/types/client/index.d.ts +2 -2
  15. package/lib/types/client/index.js +2 -0
  16. package/lib/types/client/sessions/assistant-stream.d.ts +51 -0
  17. package/lib/types/client/sessions/assistant-stream.js +168 -0
  18. package/lib/types/client/sessions/history-records.d.ts +2 -2
  19. package/lib/types/client/sessions/history-records.js +3 -8
  20. package/lib/types/client/sessions/queue-mirror.js +3 -3
  21. package/lib/types/client/sessions/remotes.d.ts +2 -2
  22. package/lib/types/client/sessions/session.d.ts +3 -1
  23. package/lib/types/client/sessions/session.js +63 -15
  24. package/lib/types/client/transport.d.ts +7 -3
  25. package/lib/types/client/transport.js +18 -3
  26. package/lib/types/commands.js +98 -28
  27. package/lib/types/history.d.ts +2 -1
  28. package/lib/types/history.js +66 -37
  29. package/lib/types/index.d.ts +4 -4
  30. package/lib/types/index.js +13 -5
  31. package/lib/types/list.d.ts +2 -9
  32. package/lib/types/list.js +10 -124
  33. package/lib/types/types.d.ts +71 -29
  34. package/package.json +63 -57
package/lib/client.js CHANGED
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
47
47
  }
48
48
  /**
49
49
  * Read the first logical sequence represented by one wire record.
50
- * @param record - validated scalar event or packed Assistant delta run.
50
+ * @param record - validated Session event.
51
51
  * @returns inclusive first Session sequence.
52
52
  */
53
53
  function historyRecordFirstSeq(record) {
@@ -55,13 +55,11 @@ window.__ModuleLoader__.load({
55
55
  }
56
56
  /**
57
57
  * Read the final logical sequence represented by one wire record.
58
- * @param record - validated scalar event or packed Assistant delta run.
58
+ * @param record - validated Session event.
59
59
  * @returns inclusive final Session sequence.
60
60
  */
61
61
  function historyRecordLastSeq(record) {
62
- if (record.type === "event") return record.event.seq;
63
- const length = record.event.type === "chunkrow/tool-call-chunks" ? record.event.data.args.length : record.event.data.texts.length;
64
- return record.event.seq + length - 1;
62
+ return record.event.seq;
65
63
  }
66
64
  //#endregion
67
65
  //#region lib/types/types.js
@@ -80,12 +78,14 @@ window.__ModuleLoader__.load({
80
78
  ...change,
81
79
  entries: historyEntries(change.entries)
82
80
  };
83
- case "append":
84
- if (change.entry.type !== "event") throw new RemoteError("gateway/internal", "session live stream emitted a packed history record", {});
85
- return {
86
- type: "append",
87
- entry: change.entry
88
- };
81
+ case "append": return {
82
+ type: "append",
83
+ entry: change.entry
84
+ };
85
+ case "notification": return {
86
+ type: "assistant-stream",
87
+ frame: change.notification
88
+ };
89
89
  }
90
90
  }
91
91
  /**
@@ -138,22 +138,37 @@ window.__ModuleLoader__.load({
138
138
  }
139
139
  /** @inheritdoc */
140
140
  async *follow(request, signal) {
141
+ let assistantRevision;
141
142
  for await (const frame of this.remote.session.follow({
142
143
  address: this.address,
144
+ assistantStream: true,
143
145
  ...request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages }
144
146
  }, signal)) {
145
147
  if (frame.type === "snapshot") {
148
+ if (frame.assistantStream === void 0) throw new RemoteError("gateway/internal", "session assistant stream omitted its opted-in opening baseline", {});
149
+ assistantRevision = frame.assistantStream.revision;
146
150
  yield {
147
151
  type: "opened",
148
152
  cursor: frame.cursor,
149
153
  page: {
150
154
  records: frame.records,
151
155
  hasMore: frame.hasMore,
152
- projections: frame.projections
156
+ projections: frame.projections,
157
+ assistantStream: frame.assistantStream
153
158
  }
154
159
  };
155
160
  continue;
156
161
  }
162
+ if (frame.type === "assistant-stream") {
163
+ const expected = (assistantRevision ?? 0) + 1;
164
+ if (frame.frame.revision !== expected) throw new _deepseek_ai_dsh_api_gateway_client.RemoteStreamCarrierError(`session assistant stream skipped revision ${String(expected)}`);
165
+ assistantRevision = frame.frame.revision;
166
+ yield {
167
+ type: "notification",
168
+ notification: frame.frame
169
+ };
170
+ continue;
171
+ }
157
172
  yield {
158
173
  type: "entry",
159
174
  entry: frame
@@ -679,6 +694,25 @@ window.__ModuleLoader__.load({
679
694
  entries
680
695
  });
681
696
  }
697
+ /**
698
+ * Replace one attempt's transient rows with its committed durable settlement.
699
+ * @param attemptId - process-local attempt whose live rows are now redundant.
700
+ * @param entry - durable settlement committed for that attempt.
701
+ */
702
+ settleAssistant(attemptId, entry) {
703
+ const entries = materialize(this.window).filter((candidate) => candidate.type !== "transient" || candidate.event.data.attemptId !== attemptId);
704
+ if (entry !== void 0) {
705
+ const index = entries.findIndex((candidate) => candidate.event.seq > entry.event.seq);
706
+ if (index < 0) entries.push(entry);
707
+ else entries.splice(index, 0, entry);
708
+ }
709
+ this.window = leaf(entries);
710
+ this.publish(this.snapshot.hasMore, {
711
+ kind: "settle-assistant",
712
+ attemptId,
713
+ ...entry === void 0 ? {} : { entry }
714
+ });
715
+ }
682
716
  publish(hasMore, change) {
683
717
  this.snapshot = windowSnapshot(this.window, hasMore, this.snapshot.revision + 1, change);
684
718
  (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller] event feed");
@@ -701,7 +735,7 @@ window.__ModuleLoader__.load({
701
735
  //#region lib/types/client/sessions/queue-mirror.js
702
736
  const QUEUE_PREVIEW_CHARS = 200;
703
737
  function previewOf(content) {
704
- const flat = content.filter((block) => block.type !== "image").map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
738
+ const flat = content.filter((block) => block.type !== "image" && block.type !== "file").map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
705
739
  const chars = Array.from(flat);
706
740
  return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}…` : flat;
707
741
  }
@@ -752,6 +786,498 @@ window.__ModuleLoader__.load({
752
786
  }
753
787
  };
754
788
  //#endregion
789
+ //#region ../../util/values/lib/index.js
790
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
791
+ function hasIntrinsicConstructor(prototype, name) {
792
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
793
+ if (typeof constructor !== "function") return false;
794
+ try {
795
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
796
+ } catch {
797
+ return false;
798
+ }
799
+ }
800
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
801
+ function isIntrinsicObjectPrototype(value) {
802
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
803
+ }
804
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
805
+ function hasPlainArrayPrototype(value) {
806
+ const prototype = Object.getPrototypeOf(value);
807
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
808
+ const objectPrototype = Object.getPrototypeOf(prototype);
809
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
810
+ }
811
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
812
+ function hasPlainObjectPrototype(value) {
813
+ const prototype = Object.getPrototypeOf(value);
814
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
815
+ }
816
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
817
+ function enumerableStringKeys(value) {
818
+ const keys = Reflect.ownKeys(value);
819
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
820
+ return keys;
821
+ }
822
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
823
+ function walkJsonValue(value, detach) {
824
+ const ancestors = /* @__PURE__ */ new Set();
825
+ let root;
826
+ const assign = (destination, item) => {
827
+ if (destination === void 0) return;
828
+ if (destination.kind === "root") root = item;
829
+ else if (destination.kind === "array") destination.target[destination.index] = item;
830
+ else Object.defineProperty(destination.target, destination.key, {
831
+ value: item,
832
+ enumerable: true,
833
+ configurable: true,
834
+ writable: true
835
+ });
836
+ };
837
+ const tasks = [{
838
+ kind: "visit",
839
+ value,
840
+ ...detach ? { destination: { kind: "root" } } : {}
841
+ }];
842
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
843
+ if (task.kind === "leave") {
844
+ ancestors.delete(task.source);
845
+ continue;
846
+ }
847
+ if (task.kind === "array-item") {
848
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
849
+ tasks.push({
850
+ kind: "visit",
851
+ value: task.source[task.index],
852
+ ...task.target === void 0 ? {} : { destination: {
853
+ kind: "array",
854
+ target: task.target,
855
+ index: task.index
856
+ } }
857
+ });
858
+ continue;
859
+ }
860
+ if (task.kind === "object-property") {
861
+ tasks.push({
862
+ kind: "visit",
863
+ value: task.source[task.key],
864
+ ...task.target === void 0 ? {} : { destination: {
865
+ kind: "object",
866
+ target: task.target,
867
+ key: task.key
868
+ } }
869
+ });
870
+ continue;
871
+ }
872
+ const current = task.value;
873
+ if (current === null) {
874
+ assign(task.destination, null);
875
+ continue;
876
+ }
877
+ if (typeof current === "boolean" || typeof current === "string") {
878
+ assign(task.destination, current);
879
+ continue;
880
+ }
881
+ if (typeof current === "number") {
882
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
883
+ assign(task.destination, current);
884
+ continue;
885
+ }
886
+ if (typeof current !== "object") return void 0;
887
+ if (ancestors.has(current)) return void 0;
888
+ if (Array.isArray(current)) {
889
+ if (!hasPlainArrayPrototype(current)) return void 0;
890
+ const length = current.length;
891
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
892
+ const target = detach ? [] : void 0;
893
+ if (target !== void 0) assign(task.destination, target);
894
+ ancestors.add(current);
895
+ tasks.push({
896
+ kind: "leave",
897
+ source: current
898
+ });
899
+ for (let index = length - 1; index >= 0; index--) tasks.push({
900
+ kind: "array-item",
901
+ source: current,
902
+ index,
903
+ ...target === void 0 ? {} : { target }
904
+ });
905
+ continue;
906
+ }
907
+ if (!hasPlainObjectPrototype(current)) return void 0;
908
+ const keys = enumerableStringKeys(current);
909
+ if (keys === void 0) return void 0;
910
+ const target = detach ? {} : void 0;
911
+ if (target !== void 0) assign(task.destination, target);
912
+ ancestors.add(current);
913
+ tasks.push({
914
+ kind: "leave",
915
+ source: current
916
+ });
917
+ for (let index = keys.length - 1; index >= 0; index--) {
918
+ const key = keys[index];
919
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
920
+ if (key === void 0) return void 0;
921
+ tasks.push({
922
+ kind: "object-property",
923
+ source: current,
924
+ key,
925
+ ...target === void 0 ? {} : { target }
926
+ });
927
+ }
928
+ }
929
+ return detach ? root : true;
930
+ }
931
+ /**
932
+ * Validate and detach lossless JSON in one read per property.
933
+ * @param value - candidate value to validate and detach.
934
+ * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
935
+ */
936
+ function snapshotJsonValue(value) {
937
+ return walkJsonValue(value, true);
938
+ }
939
+ /**
940
+ * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
941
+ * @param value - value to freeze.
942
+ * @returns the same value after every reachable enumerable child is frozen.
943
+ */
944
+ function deepFreeze(value) {
945
+ const seen = /* @__PURE__ */ new WeakSet();
946
+ const pending = [{
947
+ kind: "visit",
948
+ node: value
949
+ }];
950
+ while (pending.length > 0) {
951
+ const task = pending.pop();
952
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
953
+ if (task === void 0) continue;
954
+ if (task.kind === "property") {
955
+ pending.push({
956
+ kind: "visit",
957
+ node: task.source[task.key]
958
+ });
959
+ continue;
960
+ }
961
+ const node = task.node;
962
+ if (node === null || typeof node !== "object") continue;
963
+ if (node instanceof AbortSignal) continue;
964
+ if (seen.has(node)) continue;
965
+ seen.add(node);
966
+ Object.freeze(node);
967
+ const keys = Object.keys(node);
968
+ for (let index = keys.length - 1; index >= 0; index--) {
969
+ const key = keys[index];
970
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
971
+ if (key === void 0) continue;
972
+ pending.push({
973
+ kind: "property",
974
+ source: node,
975
+ key
976
+ });
977
+ }
978
+ }
979
+ return value;
980
+ }
981
+ //#endregion
982
+ //#region ../../llm/llm/lib/types/assistant-stream.js
983
+ /**
984
+ * Lossless compact representation of one model-stream attempt, plus record-level
985
+ * readers that answer common consumer questions without materializing members.
986
+ * Readers trust the static record type; expandAssistantStream is the validating
987
+ * path for records read at a durable boundary.
988
+ */
989
+ function safeTime(value) {
990
+ if (!Number.isSafeInteger(value)) throw new TypeError(`Assistant stream time must be a safe integer, got ${String(value)}`);
991
+ return value;
992
+ }
993
+ function safeIndex(value, label) {
994
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`${label} index must be a non-negative safe integer`);
995
+ return value;
996
+ }
997
+ function snapshotChunk(chunk) {
998
+ const snapshot = snapshotJsonValue(chunk);
999
+ if (snapshot === void 0) throw new TypeError("Assistant stream chunk must be losslessly JSON-serializable");
1000
+ return snapshot;
1001
+ }
1002
+ /**
1003
+ * Expand compact records into the exact timed chunk sequence.
1004
+ * @param stream - compact records from one durable Assistant settlement.
1005
+ * @returns detached timed chunks with every original delta boundary preserved.
1006
+ * @throws {TypeError} when a record or reconstructed timestamp is invalid.
1007
+ */
1008
+ function expandAssistantStream(stream) {
1009
+ const chunks = [];
1010
+ for (const candidate of stream) {
1011
+ const record = validateRecord(candidate);
1012
+ if (record.type === "chunk") {
1013
+ chunks.push({
1014
+ time: record.time,
1015
+ chunk: record.chunk
1016
+ });
1017
+ continue;
1018
+ }
1019
+ const members = record.type === "tool-call-chunks" ? record.args : record.texts;
1020
+ let time = record.time0;
1021
+ for (let index = 0; index < members.length; index += 1) {
1022
+ if (index > 0) time += record.dt[index - 1];
1023
+ let chunk;
1024
+ if (record.type === "text-chunks") chunk = {
1025
+ type: "text-delta",
1026
+ index: record.index,
1027
+ text: members[index]
1028
+ };
1029
+ else if (record.type === "reasoning-chunks") chunk = {
1030
+ type: "reasoning-delta",
1031
+ index: record.index,
1032
+ text: members[index]
1033
+ };
1034
+ else chunk = {
1035
+ type: "tool-call-delta",
1036
+ index: record.index,
1037
+ id: record.id,
1038
+ ...Object.hasOwn(record, "name") ? { name: record.name } : {},
1039
+ argumentsDelta: members[index]
1040
+ };
1041
+ chunks.push({
1042
+ time,
1043
+ chunk
1044
+ });
1045
+ }
1046
+ }
1047
+ return chunks;
1048
+ }
1049
+ function validateRecord(value) {
1050
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Assistant stream record must be an object");
1051
+ const record = value;
1052
+ switch (record.type) {
1053
+ case "text-chunks":
1054
+ case "reasoning-chunks": {
1055
+ exactKeys(record, [
1056
+ "type",
1057
+ "time0",
1058
+ "index",
1059
+ "dt",
1060
+ "texts"
1061
+ ], record.type);
1062
+ const texts = stringArray(record.texts, `${record.type} texts`);
1063
+ if (texts.length === 0) throw new TypeError(`${record.type} texts must be non-empty`);
1064
+ validateRun(record, texts.length, record.type);
1065
+ return record;
1066
+ }
1067
+ case "tool-call-chunks": {
1068
+ exactKeys(record, Object.hasOwn(record, "name") ? [
1069
+ "type",
1070
+ "time0",
1071
+ "index",
1072
+ "dt",
1073
+ "id",
1074
+ "name",
1075
+ "args"
1076
+ ] : [
1077
+ "type",
1078
+ "time0",
1079
+ "index",
1080
+ "dt",
1081
+ "id",
1082
+ "args"
1083
+ ], record.type);
1084
+ const args = stringArray(record.args, "tool-call-chunks args");
1085
+ if (args.length === 0) throw new TypeError("tool-call-chunks args must be non-empty");
1086
+ if (typeof record.id !== "string" || record.id.length === 0) throw new TypeError("tool-call-chunks id must be a non-empty string");
1087
+ if (record.name !== void 0 && (typeof record.name !== "string" || record.name.length === 0)) throw new TypeError("tool-call-chunks name must be a non-empty string");
1088
+ validateRun(record, args.length, record.type);
1089
+ return record;
1090
+ }
1091
+ case "chunk": {
1092
+ exactKeys(record, [
1093
+ "type",
1094
+ "time",
1095
+ "chunk"
1096
+ ], "chunk");
1097
+ const time = safeTime(record.time);
1098
+ if (typeof record.chunk !== "object" || record.chunk === null || Array.isArray(record.chunk)) throw new TypeError("Assistant stream raw chunk must be a lossless JSON object");
1099
+ let chunk;
1100
+ try {
1101
+ chunk = snapshotChunk(record.chunk);
1102
+ } catch (error) {
1103
+ throw new TypeError("Assistant stream raw chunk must be a lossless JSON object", { cause: error });
1104
+ }
1105
+ return deepFreeze({
1106
+ type: "chunk",
1107
+ time,
1108
+ chunk
1109
+ });
1110
+ }
1111
+ default: throw new TypeError(`Unsupported Assistant stream record ${JSON.stringify(record.type)}`);
1112
+ }
1113
+ }
1114
+ function validateRun(record, members, label) {
1115
+ safeTime(record.time0);
1116
+ safeIndex(record.index, label);
1117
+ if (!Array.isArray(record.dt) || record.dt.some((value) => !Number.isSafeInteger(value))) throw new TypeError(`${label} dt must contain safe integers`);
1118
+ if (record.dt.length !== members - 1) throw new TypeError(`${label} dt length must be one less than its members`);
1119
+ let time = record.time0;
1120
+ for (const gap of record.dt) {
1121
+ time += gap;
1122
+ if (!Number.isSafeInteger(time)) throw new TypeError(`${label} member times must stay safe integers`);
1123
+ }
1124
+ }
1125
+ function stringArray(value, label) {
1126
+ if (!Array.isArray(value) || value.some((member) => typeof member !== "string")) throw new TypeError(`${label} must be a string array`);
1127
+ return value;
1128
+ }
1129
+ function exactKeys(record, keys, label) {
1130
+ if (Object.keys(record).length !== keys.length || !keys.every((key) => Object.hasOwn(record, key))) throw new TypeError(`${label} Assistant stream record must contain exactly ${keys.join(", ")}`);
1131
+ }
1132
+ //#endregion
1133
+ //#region lib/types/client/sessions/assistant-stream.js
1134
+ /** Web presentation fold joining transient Assistant frames to one durable v2 settlement. */
1135
+ /** Keeps transient Assistant presentation behind one settlement-aware interface. */
1136
+ var ClientAssistantStream = class {
1137
+ activeAttempt;
1138
+ pending = /* @__PURE__ */ new Map();
1139
+ publishedSeqs = /* @__PURE__ */ new Set();
1140
+ durableCursor = -1;
1141
+ transientInGap = 0;
1142
+ /**
1143
+ * Replace the durable Web window and adopt an optional reconnect baseline.
1144
+ * @param entries - durable entries in the replacement window.
1145
+ * @param baseline - compact prefix for an Assistant attempt that is still live.
1146
+ * @returns immediately visible durable entries plus reconstructed transient chunks.
1147
+ */
1148
+ replace(entries, baseline) {
1149
+ this.pending.clear();
1150
+ this.transientInGap = 0;
1151
+ this.activeAttempt = void 0;
1152
+ const opening = baseline?.activeAttempt;
1153
+ if (opening !== void 0) this.activeAttempt = {
1154
+ attemptId: opening.attemptId,
1155
+ startedAfterSeq: opening.startedAfterSeq,
1156
+ turn: opening.turn,
1157
+ step: opening.step,
1158
+ nextIndex: opening.nextIndex
1159
+ };
1160
+ const visible = [...entries];
1161
+ this.publishedSeqs = new Set(visible.map((entry) => entry.event.seq));
1162
+ this.durableCursor = visible.reduce((cursor, entry) => Math.max(cursor, entry.event.seq), -1);
1163
+ if (opening !== void 0) for (const [index, member] of expandAssistantStream(opening.stream).entries()) {
1164
+ this.transientInGap += 1;
1165
+ visible.push({
1166
+ type: "transient",
1167
+ event: {
1168
+ type: "assistant/live-chunk",
1169
+ seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
1170
+ time: member.time,
1171
+ data: {
1172
+ attemptId: opening.attemptId,
1173
+ turn: opening.turn,
1174
+ step: opening.step,
1175
+ chunk: member.chunk
1176
+ }
1177
+ }
1178
+ });
1179
+ if (index + 1 >= opening.nextIndex) break;
1180
+ }
1181
+ return visible;
1182
+ }
1183
+ /**
1184
+ * Stage one durable v2 settlement while its matching live attempt is open.
1185
+ * @param entry - newly followed durable entry.
1186
+ * @returns a publication decision, or `undefined` when no entry becomes visible.
1187
+ */
1188
+ acceptDurable(entry) {
1189
+ const event = entry.event;
1190
+ this.durableCursor = Math.max(this.durableCursor, event.seq);
1191
+ this.transientInGap = 0;
1192
+ const settlement = assistantSettlementEntry(entry);
1193
+ if (settlement !== void 0 && this.attemptForSettlement(settlement.event) !== void 0) {
1194
+ if (this.pending.has(event.seq)) return { type: "rebaseline" };
1195
+ this.pending.set(event.seq, settlement);
1196
+ return;
1197
+ }
1198
+ return this.publish(entry);
1199
+ }
1200
+ /**
1201
+ * Fold one dense transient frame and release its named durable settlement.
1202
+ * @param frame - next Assistant stream frame received by the follow connection.
1203
+ * @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
1204
+ */
1205
+ acceptFrame(frame) {
1206
+ switch (frame.type) {
1207
+ case "start":
1208
+ if (this.activeAttempt !== void 0 || this.pending.size > 0) return { type: "rebaseline" };
1209
+ this.pending.clear();
1210
+ this.activeAttempt = {
1211
+ attemptId: frame.attemptId,
1212
+ startedAfterSeq: frame.startedAfterSeq,
1213
+ turn: frame.turn,
1214
+ step: frame.step,
1215
+ nextIndex: 0
1216
+ };
1217
+ return;
1218
+ case "chunk": {
1219
+ const attempt = this.activeAttempt;
1220
+ if (attempt === void 0 || attempt.attemptId !== frame.attemptId) return void 0;
1221
+ if (frame.index !== attempt.nextIndex) return { type: "rebaseline" };
1222
+ attempt.nextIndex += 1;
1223
+ this.transientInGap += 1;
1224
+ return {
1225
+ type: "transient",
1226
+ entry: {
1227
+ type: "transient",
1228
+ event: {
1229
+ type: "assistant/live-chunk",
1230
+ seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
1231
+ time: frame.time,
1232
+ data: {
1233
+ attemptId: frame.attemptId,
1234
+ turn: attempt.turn,
1235
+ step: attempt.step,
1236
+ chunk: frame.chunk
1237
+ }
1238
+ }
1239
+ }
1240
+ };
1241
+ }
1242
+ case "end": {
1243
+ const attempt = this.activeAttempt;
1244
+ if (attempt === void 0 || attempt.attemptId !== frame.attemptId) return;
1245
+ this.activeAttempt = void 0;
1246
+ if (frame.index !== attempt.nextIndex) return { type: "rebaseline" };
1247
+ if (frame.outcome.kind === "abandoned") return this.pending.size === 0 ? {
1248
+ type: "abandonment",
1249
+ attemptId: attempt.attemptId
1250
+ } : { type: "rebaseline" };
1251
+ if (this.publishedSeqs.has(frame.outcome.seq)) return void 0;
1252
+ const entry = this.pending.get(frame.outcome.seq);
1253
+ if (entry === void 0 || entry.event.type !== frame.outcome.eventType) return { type: "rebaseline" };
1254
+ this.pending.delete(frame.outcome.seq);
1255
+ this.publishedSeqs.add(entry.event.seq);
1256
+ return {
1257
+ type: "settlement",
1258
+ attemptId: attempt.attemptId,
1259
+ entry
1260
+ };
1261
+ }
1262
+ }
1263
+ }
1264
+ attemptForSettlement(event) {
1265
+ const attempt = this.activeAttempt;
1266
+ if (attempt === void 0 || event.type === "assistant/message" && event.surfaceOp !== "append" || event.seq <= attempt.startedAfterSeq || attempt.turn !== event.data.turn || attempt.step !== event.data.step) return void 0;
1267
+ return attempt;
1268
+ }
1269
+ publish(entry) {
1270
+ this.publishedSeqs.add(entry.event.seq);
1271
+ return {
1272
+ type: "publish",
1273
+ entry
1274
+ };
1275
+ }
1276
+ };
1277
+ function assistantSettlementEntry(entry) {
1278
+ return entry.event.type === "assistant/message" || entry.event.type === "assistant/attempt" ? entry : void 0;
1279
+ }
1280
+ //#endregion
755
1281
  //#region lib/types/client/sessions/session.js
756
1282
  function projectionsBaseline(value) {
757
1283
  return {
@@ -784,6 +1310,7 @@ window.__ModuleLoader__.load({
784
1310
  jumpPromise = null;
785
1311
  /** Authoritative stream-only inbox snapshot; pending work never hits history. */
786
1312
  queueMirror = new SessionQueueMirror();
1313
+ assistantStream = new ClientAssistantStream();
787
1314
  running = false;
788
1315
  address;
789
1316
  parentAvailable;
@@ -879,7 +1406,7 @@ window.__ModuleLoader__.load({
879
1406
  placement: this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript",
880
1407
  time: Date.now(),
881
1408
  text: input.text,
882
- images: input.images
1409
+ attachments: input.attachments
883
1410
  }];
884
1411
  this.submissionSettlements.set(requestId, {
885
1412
  onRetire: input.onRetire,
@@ -896,7 +1423,7 @@ window.__ModuleLoader__.load({
896
1423
  }
897
1424
  /**
898
1425
  * Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
899
- * @param content - text plus browser-owned temporary image uploads.
1426
+ * @param content - text, browser-owned temporary image uploads, and staged-file receipts.
900
1427
  * @param mode - queue appends after the current turn; steer interrupts it.
901
1428
  * @param signal - optional caller cancellation for the complete admission round-trip.
902
1429
  * @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
@@ -918,13 +1445,19 @@ window.__ModuleLoader__.load({
918
1445
  content,
919
1446
  clientTimeZone
920
1447
  }, signal);
921
- } else {
1448
+ } else if (content.some((part) => part.type === "file")) result = {
1449
+ ok: false,
1450
+ error: new RemoteError("subagent/attachment-invalid", "subagent continuation does not accept files", { reason: "SUBAGENT_FILE_UNSUPPORTED" })
1451
+ };
1452
+ else {
1453
+ const routedContent = content;
922
1454
  const routed = await this.remote.subagents.prompt({
923
1455
  requestId: randomUUID(),
924
1456
  parentSessionId: this.address.parentSessionId,
925
1457
  childSessionId: this.address.childSessionId,
926
1458
  mode: "continuable",
927
- content,
1459
+ delivery: mode,
1460
+ content: routedContent,
928
1461
  clientTimeZone: resolvedClientTimeZone()
929
1462
  }, signal);
930
1463
  result = routed.ok ? {
@@ -1257,24 +1790,53 @@ window.__ModuleLoader__.load({
1257
1790
  acceptEventChange(change) {
1258
1791
  switch (change.type) {
1259
1792
  case "replace":
1260
- this.installWindow(change.entries, change.hasMore, change.page.projections === void 0 ? void 0 : projectionsBaseline(change.page.projections));
1793
+ this.installWindow(change.entries, change.hasMore, change.page.projections === void 0 ? void 0 : projectionsBaseline(change.page.projections), change.page.assistantStream);
1261
1794
  return;
1262
1795
  case "prepend":
1263
1796
  this.prependWindow(change.entries, change.hasMore);
1264
1797
  return;
1265
- case "append": if (this.appendLive(change.entry)) this.notifier.markDirty();
1798
+ case "append":
1799
+ this.publishAssistantEntry(this.assistantStream.acceptDurable(change.entry));
1800
+ return;
1801
+ case "assistant-stream": this.publishAssistantEntry(this.assistantStream.acceptFrame(change.frame));
1266
1802
  }
1267
1803
  }
1268
1804
  /** Replace the complete contiguous window and apply page-owned projection metadata. */
1269
- installWindow(entries, hasMore, projections) {
1805
+ installWindow(entries, hasMore, projections, assistantStream) {
1806
+ const visible = this.assistantStream.replace(entries, assistantStream);
1270
1807
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
1271
1808
  this.hasMore = hasMore;
1272
- if (entries.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
1809
+ if (visible.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
1273
1810
  if (projections !== void 0) this.projections.seed(projections);
1274
- this.eventSource.replace(entries, hasMore);
1275
- for (const entry of entries) this.observeSubmissionEvent(entry.event);
1811
+ this.eventSource.replace(visible, hasMore);
1812
+ for (const entry of visible) this.observeSubmissionEvent(entry.event);
1276
1813
  this.notifier.markDirty();
1277
1814
  }
1815
+ publishAssistantEntry(result) {
1816
+ if (result?.type === "rebaseline") {
1817
+ const events = this.events;
1818
+ queueMicrotask(() => {
1819
+ if (events !== void 0 && this.events === events) events.restart();
1820
+ });
1821
+ return;
1822
+ }
1823
+ if (result?.type === "settlement") {
1824
+ this.eventSource.settleAssistant(result.attemptId, result.entry);
1825
+ this.observeSubmissionEvent(result.entry.event);
1826
+ this.notifier.markDirty();
1827
+ return;
1828
+ }
1829
+ if (result?.type === "abandonment") {
1830
+ this.eventSource.settleAssistant(result.attemptId);
1831
+ this.notifier.markDirty();
1832
+ return;
1833
+ }
1834
+ if (result?.type === "publish" && this.appendLive(result.entry)) this.notifier.markDirty();
1835
+ else if (result?.type === "transient") {
1836
+ this.eventSource.append(result.entry);
1837
+ this.notifier.markDirty();
1838
+ }
1839
+ }
1278
1840
  /** Prepend one stream-validated history page. */
1279
1841
  prependWindow(entries, hasMore) {
1280
1842
  this.baseSeq = entries[0] === void 0 ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
@@ -1297,12 +1859,12 @@ window.__ModuleLoader__.load({
1297
1859
  const data = event.data;
1298
1860
  const source = data?.source;
1299
1861
  if (source?.kind !== "user" || typeof source.rpcId !== "string") return;
1300
- this.scheduleObservedRetirement(source.rpcId, imageRefsIn(data?.content));
1862
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
1301
1863
  }
1302
1864
  /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
1303
1865
  observeSubmissionQueue(items) {
1304
1866
  if (this.submissionSettlements.size === 0) return;
1305
- for (const item of items) if (item.rpcId !== void 0) this.scheduleObservedRetirement(item.rpcId, imageRefsIn(item.message.content));
1867
+ for (const item of items) if (item.rpcId !== void 0) this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
1306
1868
  }
1307
1869
  /**
1308
1870
  * Latch one observed settlement and remove the echo an animation frame
@@ -1389,14 +1951,14 @@ window.__ModuleLoader__.load({
1389
1951
  });
1390
1952
  else setTimeout(fn, 0);
1391
1953
  }
1392
- /** Image attachment references in one structurally-read content block list, in block order. */
1393
- function imageRefsIn(content) {
1954
+ /** Attachment references in one structurally-read content block list, in block order. */
1955
+ function attachmentRefsIn(content) {
1394
1956
  if (!Array.isArray(content)) return [];
1395
1957
  const refs = [];
1396
1958
  for (const block of content) {
1397
1959
  if (typeof block !== "object" || block === null) continue;
1398
1960
  const candidate = block;
1399
- if (candidate.type === "image" && typeof candidate.attachment === "object" && candidate.attachment !== null) refs.push(candidate.attachment);
1961
+ if ((candidate.type === "image" || candidate.type === "file") && typeof candidate.attachment === "object" && candidate.attachment !== null) refs.push(candidate.attachment);
1400
1962
  }
1401
1963
  return refs;
1402
1964
  }
@@ -2709,6 +3271,8 @@ window.__ModuleLoader__.load({
2709
3271
  /** Client Session object layer, Agent scopes, and Remote lifecycle wiring. */
2710
3272
  /** Required Remote and Context projection services. */
2711
3273
  const inject = [
3274
+ "connection",
3275
+ "fileUpload",
2712
3276
  "typert",
2713
3277
  "remote",
2714
3278
  "remote.commands",