@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
@@ -7,6 +7,8 @@ export { SessionCreateError, SessionForkError } from "./sessions/service.js";
7
7
  export { MutableSessionEventSource } from "./contract/events.js";
8
8
  /** Required Remote and Context projection services. */
9
9
  export const inject = [
10
+ 'connection',
11
+ 'fileUpload',
10
12
  'typert',
11
13
  'remote',
12
14
  'remote.commands',
@@ -0,0 +1,51 @@
1
+ /** Web presentation fold joining transient Assistant frames to one durable v2 settlement. */
2
+ import type { SessionAssistantStreamBaseline, SessionAssistantStreamFrame } from '../../types.ts';
3
+ import type { LlmAttemptId } from '@deepseek-ai/dsh-llm/brand';
4
+ import type { SessionAssistantSettlementEntry, SessionEventLikeEntry, SessionLiveEventEntry, SessionTransientEventEntry } from '../contract/events.ts';
5
+ /** One Web publication decision from the assistant stream fold. */
6
+ export type ClientAssistantStreamResult = {
7
+ readonly type: 'publish';
8
+ readonly entry: SessionLiveEventEntry;
9
+ } | {
10
+ readonly type: 'settlement';
11
+ readonly attemptId: LlmAttemptId;
12
+ readonly entry: SessionAssistantSettlementEntry;
13
+ } | {
14
+ readonly type: 'abandonment';
15
+ readonly attemptId: LlmAttemptId;
16
+ } | {
17
+ readonly type: 'transient';
18
+ readonly entry: SessionTransientEventEntry;
19
+ } | {
20
+ readonly type: 'rebaseline';
21
+ } | undefined;
22
+ /** Keeps transient Assistant presentation behind one settlement-aware interface. */
23
+ export declare class ClientAssistantStream {
24
+ private activeAttempt;
25
+ private readonly pending;
26
+ private publishedSeqs;
27
+ private durableCursor;
28
+ private transientInGap;
29
+ /**
30
+ * Replace the durable Web window and adopt an optional reconnect baseline.
31
+ * @param entries - durable entries in the replacement window.
32
+ * @param baseline - compact prefix for an Assistant attempt that is still live.
33
+ * @returns immediately visible durable entries plus reconstructed transient chunks.
34
+ */
35
+ replace(entries: readonly SessionEventLikeEntry[], baseline?: SessionAssistantStreamBaseline): readonly SessionEventLikeEntry[];
36
+ /**
37
+ * Stage one durable v2 settlement while its matching live attempt is open.
38
+ * @param entry - newly followed durable entry.
39
+ * @returns a publication decision, or `undefined` when no entry becomes visible.
40
+ */
41
+ acceptDurable(entry: SessionLiveEventEntry): ClientAssistantStreamResult;
42
+ /**
43
+ * Fold one dense transient frame and release its named durable settlement.
44
+ * @param frame - next Assistant stream frame received by the follow connection.
45
+ * @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
46
+ */
47
+ acceptFrame(frame: SessionAssistantStreamFrame): ClientAssistantStreamResult;
48
+ private attemptForSettlement;
49
+ private publish;
50
+ }
51
+ //# sourceMappingURL=assistant-stream.d.ts.map
@@ -0,0 +1,168 @@
1
+ /** Web presentation fold joining transient Assistant frames to one durable v2 settlement. */
2
+ import { expandAssistantStream } from '@deepseek-ai/dsh-llm/assistant-stream';
3
+ /** Keeps transient Assistant presentation behind one settlement-aware interface. */
4
+ export class ClientAssistantStream {
5
+ activeAttempt;
6
+ pending = new Map();
7
+ publishedSeqs = new Set();
8
+ durableCursor = -1;
9
+ transientInGap = 0;
10
+ /**
11
+ * Replace the durable Web window and adopt an optional reconnect baseline.
12
+ * @param entries - durable entries in the replacement window.
13
+ * @param baseline - compact prefix for an Assistant attempt that is still live.
14
+ * @returns immediately visible durable entries plus reconstructed transient chunks.
15
+ */
16
+ replace(entries, baseline) {
17
+ this.pending.clear();
18
+ this.transientInGap = 0;
19
+ this.activeAttempt = undefined;
20
+ const opening = baseline?.activeAttempt;
21
+ if (opening !== undefined) {
22
+ this.activeAttempt = {
23
+ attemptId: opening.attemptId,
24
+ startedAfterSeq: opening.startedAfterSeq,
25
+ turn: opening.turn,
26
+ step: opening.step,
27
+ nextIndex: opening.nextIndex,
28
+ };
29
+ }
30
+ const visible = [...entries];
31
+ this.publishedSeqs = new Set(visible.map(entry => entry.event.seq));
32
+ this.durableCursor = visible.reduce((cursor, entry) => Math.max(cursor, entry.event.seq), -1);
33
+ if (opening !== undefined) {
34
+ for (const [index, member] of expandAssistantStream(opening.stream).entries()) {
35
+ this.transientInGap += 1;
36
+ visible.push({
37
+ type: 'transient',
38
+ event: {
39
+ type: 'assistant/live-chunk',
40
+ seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
41
+ time: member.time,
42
+ data: {
43
+ attemptId: opening.attemptId,
44
+ turn: opening.turn,
45
+ step: opening.step,
46
+ chunk: member.chunk,
47
+ },
48
+ },
49
+ });
50
+ if (index + 1 >= opening.nextIndex)
51
+ break;
52
+ }
53
+ }
54
+ return visible;
55
+ }
56
+ /**
57
+ * Stage one durable v2 settlement while its matching live attempt is open.
58
+ * @param entry - newly followed durable entry.
59
+ * @returns a publication decision, or `undefined` when no entry becomes visible.
60
+ */
61
+ acceptDurable(entry) {
62
+ const event = entry.event;
63
+ this.durableCursor = Math.max(this.durableCursor, event.seq);
64
+ this.transientInGap = 0;
65
+ const settlement = assistantSettlementEntry(entry);
66
+ if (settlement !== undefined && this.attemptForSettlement(settlement.event) !== undefined) {
67
+ if (this.pending.has(event.seq))
68
+ return { type: 'rebaseline' };
69
+ this.pending.set(event.seq, settlement);
70
+ return undefined;
71
+ }
72
+ return this.publish(entry);
73
+ }
74
+ /**
75
+ * Fold one dense transient frame and release its named durable settlement.
76
+ * @param frame - next Assistant stream frame received by the follow connection.
77
+ * @returns a transient, publication, or rebaseline decision, or `undefined` when no entry becomes visible.
78
+ */
79
+ acceptFrame(frame) {
80
+ switch (frame.type) {
81
+ case 'start':
82
+ if (this.activeAttempt !== undefined || this.pending.size > 0)
83
+ return { type: 'rebaseline' };
84
+ this.pending.clear();
85
+ this.activeAttempt = {
86
+ attemptId: frame.attemptId,
87
+ startedAfterSeq: frame.startedAfterSeq,
88
+ turn: frame.turn,
89
+ step: frame.step,
90
+ nextIndex: 0,
91
+ };
92
+ return undefined;
93
+ case 'chunk': {
94
+ const attempt = this.activeAttempt;
95
+ // A controller mounted after the Host saw this attempt has no start
96
+ // frame to reconstruct. Its durable settlement publishes directly;
97
+ // ignore the transient suffix until the next known start.
98
+ if (attempt === undefined || attempt.attemptId !== frame.attemptId)
99
+ return undefined;
100
+ if (frame.index !== attempt.nextIndex)
101
+ return { type: 'rebaseline' };
102
+ attempt.nextIndex += 1;
103
+ this.transientInGap += 1;
104
+ return {
105
+ type: 'transient',
106
+ entry: {
107
+ type: 'transient',
108
+ event: {
109
+ type: 'assistant/live-chunk',
110
+ seq: this.durableCursor + 1 - 1 / (this.transientInGap + 1),
111
+ time: frame.time,
112
+ data: {
113
+ attemptId: frame.attemptId,
114
+ turn: attempt.turn,
115
+ step: attempt.step,
116
+ chunk: frame.chunk,
117
+ },
118
+ },
119
+ },
120
+ };
121
+ }
122
+ case 'end': {
123
+ const attempt = this.activeAttempt;
124
+ if (attempt === undefined || attempt.attemptId !== frame.attemptId) {
125
+ return undefined;
126
+ }
127
+ this.activeAttempt = undefined;
128
+ if (frame.index !== attempt.nextIndex)
129
+ return { type: 'rebaseline' };
130
+ if (frame.outcome.kind === 'abandoned') {
131
+ return this.pending.size === 0
132
+ ? { type: 'abandonment', attemptId: attempt.attemptId }
133
+ : { type: 'rebaseline' };
134
+ }
135
+ if (this.publishedSeqs.has(frame.outcome.seq))
136
+ return undefined;
137
+ const entry = this.pending.get(frame.outcome.seq);
138
+ if (entry === undefined
139
+ || entry.event.type !== frame.outcome.eventType) {
140
+ return { type: 'rebaseline' };
141
+ }
142
+ this.pending.delete(frame.outcome.seq);
143
+ this.publishedSeqs.add(entry.event.seq);
144
+ return { type: 'settlement', attemptId: attempt.attemptId, entry };
145
+ }
146
+ }
147
+ }
148
+ attemptForSettlement(event) {
149
+ const attempt = this.activeAttempt;
150
+ if (attempt === undefined
151
+ || (event.type === 'assistant/message' && event.surfaceOp !== 'append')
152
+ || event.seq <= attempt.startedAfterSeq
153
+ || attempt.turn !== event.data.turn
154
+ || attempt.step !== event.data.step)
155
+ return undefined;
156
+ return attempt;
157
+ }
158
+ publish(entry) {
159
+ this.publishedSeqs.add(entry.event.seq);
160
+ return { type: 'publish', entry };
161
+ }
162
+ }
163
+ function assistantSettlementEntry(entry) {
164
+ return entry.event.type === 'assistant/message' || entry.event.type === 'assistant/attempt'
165
+ ? entry
166
+ : undefined;
167
+ }
168
+ //# sourceMappingURL=assistant-stream.js.map
@@ -9,13 +9,13 @@ import type { SessionEventLikeEntry } from '../contract/events.ts';
9
9
  export declare function historyEntries(records: readonly SessionHistoryRecord[]): readonly SessionEventLikeEntry[];
10
10
  /**
11
11
  * Read the first logical sequence represented by one wire record.
12
- * @param record - validated scalar event or packed Assistant delta run.
12
+ * @param record - validated Session event.
13
13
  * @returns inclusive first Session sequence.
14
14
  */
15
15
  export declare function historyRecordFirstSeq(record: SessionHistoryRecord): number;
16
16
  /**
17
17
  * Read the final logical sequence represented by one wire record.
18
- * @param record - validated scalar event or packed Assistant delta run.
18
+ * @param record - validated Session event.
19
19
  * @returns inclusive final Session sequence.
20
20
  */
21
21
  export declare function historyRecordLastSeq(record: SessionHistoryRecord): number;
@@ -9,7 +9,7 @@ export function historyEntries(records) {
9
9
  }
10
10
  /**
11
11
  * Read the first logical sequence represented by one wire record.
12
- * @param record - validated scalar event or packed Assistant delta run.
12
+ * @param record - validated Session event.
13
13
  * @returns inclusive first Session sequence.
14
14
  */
15
15
  export function historyRecordFirstSeq(record) {
@@ -17,15 +17,10 @@ export function historyRecordFirstSeq(record) {
17
17
  }
18
18
  /**
19
19
  * Read the final logical sequence represented by one wire record.
20
- * @param record - validated scalar event or packed Assistant delta run.
20
+ * @param record - validated Session event.
21
21
  * @returns inclusive final Session sequence.
22
22
  */
23
23
  export function historyRecordLastSeq(record) {
24
- if (record.type === 'event')
25
- return record.event.seq;
26
- const length = record.event.type === 'chunkrow/tool-call-chunks'
27
- ? record.event.data.args.length
28
- : record.event.data.texts.length;
29
- return record.event.seq + length - 1;
24
+ return record.event.seq;
30
25
  }
31
26
  //# sourceMappingURL=history-records.js.map
@@ -1,9 +1,9 @@
1
1
  const QUEUE_PREVIEW_CHARS = 200;
2
- // Image blocks are excluded: queue presentation renders them as thumbnails
3
- // from `content`, so the text preview covers only what has no visual form.
2
+ // Attachment blocks are excluded: queue presentation renders them from
3
+ // `content`, so the text preview covers only what has no visual form.
4
4
  function previewOf(content) {
5
5
  const flat = content
6
- .filter(block => block.type !== 'image')
6
+ .filter(block => block.type !== 'image' && block.type !== 'file')
7
7
  .map(block => (block.type === 'text' ? block.text : `[${block.type}]`))
8
8
  .join(' ').replace(/\s+/g, ' ').trim();
9
9
  const chars = Array.from(flat);
@@ -4,15 +4,15 @@
4
4
  *
5
5
  * @module @deepseek-ai/dsh-api-session-controller/client/sessions/remotes
6
6
  */
7
- import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types';
8
7
  import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client';
8
+ import type { CommandSubmitAttachment } from '@deepseek-ai/dsh-commands/types';
9
9
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
10
10
  import type { SubagentCatalog, SubagentInterruptReceipt, SubagentPromptReceipt, SubagentPromptRequest } from '@deepseek-ai/dsh-subagent/client';
11
11
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
12
12
  import type { SessionRemote } from '../transport.ts';
13
13
  /** Narrow Commands namespace consumed by a Client Session. */
14
14
  export interface SessionCommandsRemote {
15
- execute(agentId: SessionId, line: string, images: readonly EncodedImageAttachment[], signal?: AbortSignal): Promise<RemoteResult<object | undefined>>;
15
+ execute(agentId: SessionId, line: string, attachments: readonly CommandSubmitAttachment[], signal?: AbortSignal): Promise<RemoteResult<object | undefined>>;
16
16
  }
17
17
  /** Narrow subagent namespace consumed by a Client Session and its manager. */
18
18
  export interface SessionSubagentsRemote {
@@ -61,6 +61,7 @@ export declare class Session implements SessionFace {
61
61
  private jumpPromise;
62
62
  /** Authoritative stream-only inbox snapshot; pending work never hits history. */
63
63
  private readonly queueMirror;
64
+ private readonly assistantStream;
64
65
  private running;
65
66
  private address;
66
67
  private parentAvailable;
@@ -136,7 +137,7 @@ export declare class Session implements SessionFace {
136
137
  beginSubmission(input: BeginSubmissionInput): SubmissionHandle;
137
138
  /**
138
139
  * Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
139
- * @param content - text plus browser-owned temporary image uploads.
140
+ * @param content - text, browser-owned temporary image uploads, and staged-file receipts.
140
141
  * @param mode - queue appends after the current turn; steer interrupts it.
141
142
  * @param signal - optional caller cancellation for the complete admission round-trip.
142
143
  * @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
@@ -267,6 +268,7 @@ export declare class Session implements SessionFace {
267
268
  private acceptEventChange;
268
269
  /** Replace the complete contiguous window and apply page-owned projection metadata. */
269
270
  private installWindow;
271
+ private publishAssistantEntry;
270
272
  /** Prepend one stream-validated history page. */
271
273
  private prependWindow;
272
274
  /** Append one stream-validated live event. */
@@ -5,9 +5,11 @@ import { SessionEventStream } from "../transport.js";
5
5
  import { MutableSessionEventSource } from "../contract/events.js";
6
6
  import { Notifier } from "./notifier.js";
7
7
  import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
8
+ import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
8
9
  import { ProjectionValueStore } from "./projection-store.js";
9
10
  import { resolvedClientTimeZone } from "../time-zone.js";
10
11
  import { SessionQueueMirror } from "./queue-mirror.js";
12
+ import { ClientAssistantStream, } from "./assistant-stream.js";
11
13
  function projectionsBaseline(value) {
12
14
  return {
13
15
  ...value,
@@ -44,6 +46,7 @@ export class Session {
44
46
  jumpPromise = null;
45
47
  /** Authoritative stream-only inbox snapshot; pending work never hits history. */
46
48
  queueMirror = new SessionQueueMirror();
49
+ assistantStream = new ClientAssistantStream();
47
50
  running = false;
48
51
  address;
49
52
  parentAvailable;
@@ -143,7 +146,7 @@ export class Session {
143
146
  : 'transcript',
144
147
  time: Date.now(),
145
148
  text: input.text,
146
- images: input.images,
149
+ attachments: input.attachments,
147
150
  }];
148
151
  this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
149
152
  // The blank → engaging edge flips here, ahead of prompt(): the composer
@@ -154,7 +157,7 @@ export class Session {
154
157
  }
155
158
  /**
156
159
  * Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
157
- * @param content - text plus browser-owned temporary image uploads.
160
+ * @param content - text, browser-owned temporary image uploads, and staged-file receipts.
158
161
  * @param mode - queue appends after the current turn; steer interrupts it.
159
162
  * @param signal - optional caller cancellation for the complete admission round-trip.
160
163
  * @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
@@ -181,13 +184,23 @@ export class Session {
181
184
  clientTimeZone,
182
185
  }, signal);
183
186
  }
187
+ else if (content.some(part => part.type === 'file')) {
188
+ result = {
189
+ ok: false,
190
+ error: new RemoteError('subagent/attachment-invalid', 'subagent continuation does not accept files', { reason: 'SUBAGENT_FILE_UNSUPPORTED' }),
191
+ };
192
+ }
184
193
  else {
194
+ // The preceding branch rejects file parts before the narrower subagent
195
+ // wire type is used; this array is not filtered or reordered.
196
+ const routedContent = content;
185
197
  const routed = await this.remote.subagents.prompt({
186
198
  requestId: randomUUID(),
187
199
  parentSessionId: this.address.parentSessionId,
188
200
  childSessionId: this.address.childSessionId,
189
201
  mode: 'continuable',
190
- content,
202
+ delivery: mode,
203
+ content: routedContent,
191
204
  clientTimeZone: resolvedClientTimeZone(),
192
205
  }, signal);
193
206
  result = routed.ok ? { ok: true, value: { accepted: true } } : routed;
@@ -553,29 +566,63 @@ export class Session {
553
566
  acceptEventChange(change) {
554
567
  switch (change.type) {
555
568
  case 'replace':
556
- this.installWindow(change.entries, change.hasMore, change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections));
569
+ this.installWindow(change.entries, change.hasMore, change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections), change.page.assistantStream);
557
570
  return;
558
571
  case 'prepend':
559
572
  this.prependWindow(change.entries, change.hasMore);
560
573
  return;
561
574
  case 'append':
562
- if (this.appendLive(change.entry))
563
- this.notifier.markDirty();
575
+ this.publishAssistantEntry(this.assistantStream.acceptDurable(change.entry));
576
+ return;
577
+ case 'assistant-stream':
578
+ this.publishAssistantEntry(this.assistantStream.acceptFrame(change.frame));
564
579
  }
565
580
  }
566
581
  /** Replace the complete contiguous window and apply page-owned projection metadata. */
567
- installWindow(entries, hasMore, projections) {
582
+ installWindow(entries, hasMore, projections, assistantStream) {
583
+ // A durable gap-repair page has no assistant baseline. Clearing transient
584
+ // attempts makes a held notification reopen follow once for an atomic
585
+ // page/baseline pair instead of applying it to an unrelated repair cut.
586
+ const visible = this.assistantStream.replace(entries, assistantStream);
568
587
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
569
588
  this.hasMore = hasMore;
570
- if (entries.some(entry => entry.event.type === 'turn/start'))
589
+ if (visible.some(entry => entry.event.type === 'turn/start'))
571
590
  this.firstPromptPendingTurn = false;
572
591
  if (projections !== undefined)
573
592
  this.projections.seed(projections);
574
- this.eventSource.replace(entries, hasMore);
575
- for (const entry of entries)
593
+ this.eventSource.replace(visible, hasMore);
594
+ for (const entry of visible)
576
595
  this.observeSubmissionEvent(entry.event);
577
596
  this.notifier.markDirty();
578
597
  }
598
+ publishAssistantEntry(result) {
599
+ if (result?.type === 'rebaseline') {
600
+ const events = this.events;
601
+ queueMicrotask(() => {
602
+ if (events !== undefined && this.events === events)
603
+ events.restart();
604
+ });
605
+ return;
606
+ }
607
+ if (result?.type === 'settlement') {
608
+ this.eventSource.settleAssistant(result.attemptId, result.entry);
609
+ this.observeSubmissionEvent(result.entry.event);
610
+ this.notifier.markDirty();
611
+ return;
612
+ }
613
+ if (result?.type === 'abandonment') {
614
+ this.eventSource.settleAssistant(result.attemptId);
615
+ this.notifier.markDirty();
616
+ return;
617
+ }
618
+ if (result?.type === 'publish' && this.appendLive(result.entry)) {
619
+ this.notifier.markDirty();
620
+ }
621
+ else if (result?.type === 'transient') {
622
+ this.eventSource.append(result.entry);
623
+ this.notifier.markDirty();
624
+ }
625
+ }
579
626
  /** Prepend one stream-validated history page. */
580
627
  prependWindow(entries, hasMore) {
581
628
  this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
@@ -607,7 +654,7 @@ export class Session {
607
654
  const source = data?.source;
608
655
  if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
609
656
  return;
610
- this.scheduleObservedRetirement(source.rpcId, imageRefsIn(data?.content));
657
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
611
658
  }
612
659
  /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
613
660
  observeSubmissionQueue(items) {
@@ -615,7 +662,7 @@ export class Session {
615
662
  return;
616
663
  for (const item of items) {
617
664
  if (item.rpcId !== undefined) {
618
- this.scheduleObservedRetirement(item.rpcId, imageRefsIn(item.message.content));
665
+ this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
619
666
  }
620
667
  }
621
668
  }
@@ -702,8 +749,8 @@ function scheduleFrame(fn) {
702
749
  else
703
750
  setTimeout(fn, 0);
704
751
  }
705
- /** Image attachment references in one structurally-read content block list, in block order. */
706
- function imageRefsIn(content) {
752
+ /** Attachment references in one structurally-read content block list, in block order. */
753
+ function attachmentRefsIn(content) {
707
754
  if (!Array.isArray(content))
708
755
  return [];
709
756
  const refs = [];
@@ -711,7 +758,8 @@ function imageRefsIn(content) {
711
758
  if (typeof block !== 'object' || block === null)
712
759
  continue;
713
760
  const candidate = block;
714
- if (candidate.type === 'image' && typeof candidate.attachment === 'object' && candidate.attachment !== null) {
761
+ if ((candidate.type === 'image' || candidate.type === 'file')
762
+ && typeof candidate.attachment === 'object' && candidate.attachment !== null) {
715
763
  refs.push(candidate.attachment);
716
764
  }
717
765
  }
@@ -1,6 +1,6 @@
1
1
  /** Session-specific adapters for Gateway-owned Remote stream lifecycles. */
2
2
  import { RemoteJournalStream, RemoteSnapshotStream, RemoteStreamCarrierError, type ClientRemote, type RemoteJournalFrame } from '@deepseek-ai/dsh-api-gateway/client';
3
- import type { SessionAddress, SessionControlFrame, SessionHistoryRecord, SessionPage, SessionPageRequest, SessionProjectionBaseline } from '../types.ts';
3
+ import type { SessionAddress, SessionAssistantStreamBaseline, SessionAssistantStreamFrame, SessionControlFrame, SessionHistoryRecord, SessionPage, SessionPageRequest, SessionProjectionBaseline } from '../types.ts';
4
4
  import type { SessionEventLikeEntry, SessionLiveEventEntry } from './contract/events.ts';
5
5
  import type { SessionRemotes } from './sessions/remotes.ts';
6
6
  export { SESSION_SEARCH_RESULT_LIMIT, SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS, } from '../types.ts';
@@ -11,6 +11,7 @@ export type SessionRemote = ClientRemote['session'];
11
11
  /** Opening metadata carried only by a follow snapshot, never by loadOlder pages. */
12
12
  interface SessionJournalPage extends SessionPage {
13
13
  readonly projections?: SessionProjectionBaseline;
14
+ readonly assistantStream?: SessionAssistantStreamBaseline;
14
15
  }
15
16
  /** One complete publication from the Session journal stream. */
16
17
  export type SessionJournalChange = {
@@ -21,6 +22,9 @@ export type SessionJournalChange = {
21
22
  } | {
22
23
  readonly type: 'append';
23
24
  readonly entry: SessionLiveEventEntry;
25
+ } | {
26
+ readonly type: 'assistant-stream';
27
+ readonly frame: SessionAssistantStreamFrame;
24
28
  };
25
29
  type SessionControlBaselineFrame = Extract<SessionControlFrame, {
26
30
  type: 'baseline';
@@ -54,7 +58,7 @@ export interface SessionEventStreamOptions {
54
58
  */
55
59
  export declare function createSessionControlStream(remote: SessionRemotes, options: SessionControlStreamOptions): SessionControlStream;
56
60
  /** Gateway-owned event journal bound to one ordinary or direct-subagent Session address. */
57
- export declare class SessionEventStream extends RemoteJournalStream<SessionJournalPage, SessionHistoryRecord, number, ClientSessionPageRequest> {
61
+ export declare class SessionEventStream extends RemoteJournalStream<SessionJournalPage, SessionHistoryRecord, number, ClientSessionPageRequest, SessionAssistantStreamFrame> {
58
62
  private readonly remote;
59
63
  private readonly address;
60
64
  /**
@@ -64,7 +68,7 @@ export declare class SessionEventStream extends RemoteJournalStream<SessionJourn
64
68
  */
65
69
  constructor(remote: SessionRemotes, address: SessionAddress, options: SessionEventStreamOptions);
66
70
  /** @inheritdoc */
67
- protected follow(request: ClientSessionPageRequest, signal: AbortSignal): AsyncIterable<RemoteJournalFrame<SessionHistoryRecord, number, SessionJournalPage>>;
71
+ protected follow(request: ClientSessionPageRequest, signal: AbortSignal): AsyncIterable<RemoteJournalFrame<SessionHistoryRecord, number, SessionJournalPage, SessionAssistantStreamFrame>>;
68
72
  /** @inheritdoc */
69
73
  protected readPage(request: ClientSessionPageRequest, throughSeq: number, signal: AbortSignal): Promise<SessionJournalPage>;
70
74
  /** @inheritdoc */
@@ -9,14 +9,13 @@ function toSessionJournalChange(change) {
9
9
  case 'prepend':
10
10
  return { ...change, entries: historyEntries(change.entries) };
11
11
  case 'append': {
12
- if (change.entry.type !== 'event') {
13
- throw new RemoteError('gateway/internal', 'session live stream emitted a packed history record', {});
14
- }
15
12
  return {
16
13
  type: 'append',
17
14
  entry: change.entry,
18
15
  };
19
16
  }
17
+ case 'notification':
18
+ return { type: 'assistant-stream', frame: change.notification };
20
19
  }
21
20
  }
22
21
  /**
@@ -72,11 +71,17 @@ export class SessionEventStream extends RemoteJournalStream {
72
71
  }
73
72
  /** @inheritdoc */
74
73
  async *follow(request, signal) {
74
+ let assistantRevision;
75
75
  for await (const frame of this.remote.session.follow({
76
76
  address: this.address,
77
+ assistantStream: true,
77
78
  ...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
78
79
  }, signal)) {
79
80
  if (frame.type === 'snapshot') {
81
+ if (frame.assistantStream === undefined) {
82
+ throw new RemoteError('gateway/internal', 'session assistant stream omitted its opted-in opening baseline', {});
83
+ }
84
+ assistantRevision = frame.assistantStream.revision;
80
85
  yield {
81
86
  type: 'opened',
82
87
  cursor: frame.cursor,
@@ -84,10 +89,20 @@ export class SessionEventStream extends RemoteJournalStream {
84
89
  records: frame.records,
85
90
  hasMore: frame.hasMore,
86
91
  projections: frame.projections,
92
+ assistantStream: frame.assistantStream,
87
93
  },
88
94
  };
89
95
  continue;
90
96
  }
97
+ if (frame.type === 'assistant-stream') {
98
+ const expected = (assistantRevision ?? 0) + 1;
99
+ if (frame.frame.revision !== expected) {
100
+ throw new RemoteStreamCarrierError(`session assistant stream skipped revision ${String(expected)}`);
101
+ }
102
+ assistantRevision = frame.frame.revision;
103
+ yield { type: 'notification', notification: frame.frame };
104
+ continue;
105
+ }
91
106
  yield { type: 'entry', entry: frame };
92
107
  }
93
108
  }