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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +16 -6
  3. package/README.zh.md +16 -6
  4. package/lib/client.js +838 -58
  5. package/lib/index.js +376 -243
  6. package/lib/typert.host.js +236 -178
  7. package/lib/typert.remote-client.js +97 -115
  8. package/lib/types/agent.js +6 -8
  9. package/lib/types/assistant-stream.d.ts +26 -0
  10. package/lib/types/assistant-stream.js +83 -0
  11. package/lib/types/client/contract/events.d.ts +38 -7
  12. package/lib/types/client/contract/events.js +21 -0
  13. package/lib/types/client/contract/session.d.ts +7 -7
  14. package/lib/types/client/contract/snapshot.d.ts +15 -2
  15. package/lib/types/client/index.d.ts +2 -2
  16. package/lib/types/client/index.js +2 -0
  17. package/lib/types/client/session-wire-event.d.ts +11 -0
  18. package/lib/types/client/session-wire-event.js +43 -0
  19. package/lib/types/client/sessions/assistant-stream.d.ts +51 -0
  20. package/lib/types/client/sessions/assistant-stream.js +168 -0
  21. package/lib/types/client/sessions/history-records.d.ts +2 -2
  22. package/lib/types/client/sessions/history-records.js +3 -8
  23. package/lib/types/client/sessions/queue-mirror.js +3 -3
  24. package/lib/types/client/sessions/remotes.d.ts +2 -2
  25. package/lib/types/client/sessions/session.d.ts +3 -1
  26. package/lib/types/client/sessions/session.js +63 -15
  27. package/lib/types/client/transport.d.ts +7 -3
  28. package/lib/types/client/transport.js +24 -3
  29. package/lib/types/commands.d.ts +1 -1
  30. package/lib/types/commands.js +112 -32
  31. package/lib/types/control.d.ts +0 -1
  32. package/lib/types/control.js +19 -22
  33. package/lib/types/history.d.ts +2 -1
  34. package/lib/types/history.js +66 -37
  35. package/lib/types/index.d.ts +4 -4
  36. package/lib/types/index.js +15 -5
  37. package/lib/types/list.d.ts +2 -9
  38. package/lib/types/list.js +10 -124
  39. package/lib/types/media-references.d.ts +16 -0
  40. package/lib/types/media-references.js +77 -0
  41. package/lib/types/types.d.ts +85 -35
  42. package/package.json +71 -58
@@ -7,22 +7,22 @@
7
7
  * must stub); implementation-internal entry points (history staging, wire-frame
8
8
  * dispatch) stay on the class, invisible out here.
9
9
  */
10
- import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
10
+ import type { AttachmentIdType, FileAttachmentRef, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
11
11
  import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
12
12
  import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types';
13
13
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
14
14
  import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
15
15
  import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts';
16
- import type { PendingSubmissionImage, SessionSnapshot } from './snapshot.ts';
16
+ import type { PendingSubmissionAttachment, SessionSnapshot } from './snapshot.ts';
17
17
  /**
18
18
  * Why a local submission echo left the snapshot: `observed` when its durable
19
19
  * `user/message` event or host queue occurrence arrived (with the admitted
20
- * image references in prompt order), `failed` when the prompt was rejected,
20
+ * attachment references in prompt order), `failed` when the prompt was rejected,
21
21
  * threw, or was aborted before acceptance.
22
22
  */
23
23
  export type PendingSubmissionRetirement = {
24
24
  readonly reason: 'observed';
25
- readonly attachments: readonly ImageAttachmentRef[];
25
+ readonly attachments: readonly (ImageAttachmentRef | FileAttachmentRef)[];
26
26
  } | {
27
27
  readonly reason: 'failed';
28
28
  };
@@ -32,8 +32,8 @@ export interface BeginSubmissionInput {
32
32
  readonly mode: 'queue' | 'steer';
33
33
  /** Prompt text exactly as the upcoming prompt will send it. */
34
34
  readonly text: string;
35
- /** Ordered image previews matching the upcoming prompt's image parts. */
36
- readonly images: readonly PendingSubmissionImage[];
35
+ /** Ordered image previews and durable file metadata matching the upcoming prompt attachments. */
36
+ readonly attachments: readonly PendingSubmissionAttachment[];
37
37
  /** Settlement callback fired exactly once when the echo retires. */
38
38
  readonly onRetire?: (retirement: PendingSubmissionRetirement) => void;
39
39
  }
@@ -91,7 +91,7 @@ export interface ISession {
91
91
  data: Uint8Array;
92
92
  }>>;
93
93
  /**
94
- * Apply one edit, remove, or strict steer action to a still-pending queue occurrence.
94
+ * Apply one edit, remove, or Steer action to a still-pending queue occurrence.
95
95
  * @param itemId - agent-owned inbox occurrence identity.
96
96
  * @param action - requested queue operation.
97
97
  * @returns acceptance, or a business/transport error.
@@ -1,5 +1,6 @@
1
1
  /** Session-owned observable state excluding Conversation target data. */
2
2
  import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
3
+ import type { FileAttachmentRef } from '@deepseek-ai/dsh-attachment';
3
4
  import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
4
5
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
5
6
  import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client';
@@ -27,6 +28,18 @@ export interface PendingSubmissionImage {
27
28
  /** Intrinsic pixel height, when the submitter has probed it. */
28
29
  readonly height?: number;
29
30
  }
31
+ /** Image branch of a local submission echo attachment. */
32
+ export interface PendingSubmissionImageAttachment {
33
+ readonly type: 'image';
34
+ readonly value: PendingSubmissionImage;
35
+ }
36
+ /** File branch of a local submission echo attachment. */
37
+ export interface PendingSubmissionFileAttachment {
38
+ readonly type: 'file';
39
+ readonly value: FileAttachmentRef;
40
+ }
41
+ /** One attachment displayed by a local submission echo, in prompt order. */
42
+ export type PendingSubmissionAttachment = PendingSubmissionImageAttachment | PendingSubmissionFileAttachment;
30
43
  /** Client surface selected when a local submission begins. */
31
44
  export type PendingSubmissionPlacement = 'transcript' | 'queued' | 'steering';
32
45
  /**
@@ -44,8 +57,8 @@ export interface PendingSubmission {
44
57
  readonly time: number;
45
58
  /** Prompt text exactly as it will be sent (one text block). */
46
59
  readonly text: string;
47
- /** Ordered image previews matching the prompt's image parts. */
48
- readonly images: readonly PendingSubmissionImage[];
60
+ /** Ordered image previews and durable file metadata matching the prompt attachments. */
61
+ readonly attachments: readonly PendingSubmissionAttachment[];
49
62
  }
50
63
  /** History-open lifecycle of a Session event window. */
51
64
  export type OpenState = 'cold' | 'loading' | 'open' | 'error';
@@ -12,8 +12,8 @@ export type { ProjectionsBaseline, ProjectionValueStore, SessionProjectionMap, U
12
12
  export type { BeginSubmissionInput, ISession, PendingSubmissionRetirement, ProjectionsFace, SessionFace, SubmissionHandle, } from './contract/session.ts';
13
13
  export type { ISessions } from './contract/sessions.ts';
14
14
  export { MutableSessionEventSource } from './contract/events.ts';
15
- export type { SessionEventChange, SessionEventLike, SessionEventLikeEntry, SessionEventSource, SessionEventWindow, SessionLiveEventEntry, } from './contract/events.ts';
16
- export type { OpenState, PendingSubmission, PendingSubmissionImage, PendingSubmissionPlacement, PromptError, QueuedMessage, SessionSnapshot, } from './contract/snapshot.ts';
15
+ export type { AssistantLiveChunkEvent, SessionAssistantSettlementEntry, SessionEventChange, SessionEventLike, SessionEventLikeEntry, SessionEventSource, SessionEventWindow, SessionLiveEventEntry, SessionTransientEventEntry, } from './contract/events.ts';
16
+ export type { OpenState, PendingSubmission, PendingSubmissionAttachment, PendingSubmissionFileAttachment, PendingSubmissionImage, PendingSubmissionImageAttachment, PendingSubmissionPlacement, PromptError, QueuedMessage, SessionSnapshot, } from './contract/snapshot.ts';
17
17
  declare module '@deepseek-ai/cordis' {
18
18
  interface Context {
19
19
  /** Client Session object layer and Agent scope owner. */
@@ -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,11 @@
1
+ /** Event-local acceptance for raw Session journal responses; payloads remain owner-defined JSON. */
2
+ import type { SessionWireEvent } from '../types.ts';
3
+ /**
4
+ * Reject non-current event envelopes without stripping or normalizing wire fields.
5
+ * Range membership and source existence require the durable log and remain Host-owned.
6
+ * @param value - one event received in a follow frame or history page.
7
+ * @returns nothing after narrowing the accepted event envelope.
8
+ * @throws when the envelope or current event-local metadata is invalid.
9
+ */
10
+ export declare function assertSessionWireEvent(value: unknown): asserts value is SessionWireEvent;
11
+ //# sourceMappingURL=session-wire-event.d.ts.map
@@ -0,0 +1,43 @@
1
+ /** Event-local acceptance for raw Session journal responses; payloads remain owner-defined JSON. */
2
+ import { validateSessionEventData, validateSurfaceMetadata } from '@deepseek-ai/dsh-session/surface';
3
+ /**
4
+ * Reject non-current event envelopes without stripping or normalizing wire fields.
5
+ * Range membership and source existence require the durable log and remain Host-owned.
6
+ * @param value - one event received in a follow frame or history page.
7
+ * @returns nothing after narrowing the accepted event envelope.
8
+ * @throws when the envelope or current event-local metadata is invalid.
9
+ */
10
+ export function assertSessionWireEvent(value) {
11
+ const subject = 'session wire event';
12
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
13
+ throw new Error(`${subject} must be an object`);
14
+ }
15
+ const event = value;
16
+ for (const key of Object.keys(event)) {
17
+ switch (key) {
18
+ case 'type':
19
+ case 'seq':
20
+ case 'time':
21
+ case 'data':
22
+ case 'ignorable':
23
+ case 'surfaceOp':
24
+ case 'sourceEventSeqs':
25
+ break;
26
+ default:
27
+ throw new Error(`${subject} has unexpected field ${key}`);
28
+ }
29
+ }
30
+ const seq = event['seq'];
31
+ if (typeof event['type'] !== 'string'
32
+ || typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0)
33
+ || typeof event['time'] !== 'number' || !Number.isSafeInteger(event['time'])
34
+ || !Object.hasOwn(event, 'data') || event['data'] === undefined
35
+ || (Object.hasOwn(event, 'ignorable') && event['ignorable'] !== true)) {
36
+ throw new Error(`${subject} has an invalid envelope`);
37
+ }
38
+ // Event names and payloads are merge-extensible; only event-local owner rules run here.
39
+ const current = event;
40
+ validateSurfaceMetadata(current);
41
+ validateSessionEventData(current, subject);
42
+ }
43
+ //# sourceMappingURL=session-wire-event.js.map
@@ -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
  }