@deepseek-ai/dsh-api-session-controller 0.1.3-alpha.2 → 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.
@@ -99,6 +99,10 @@ const _deepseek_ai_dsh_api_session_controller_session_control_result$schema = z.
99
99
  'projections': z.record(z.intersection(z.string(), z.unknown()), z.object({
100
100
  'asOfSeq': z.number().readonly(),
101
101
  'values': z.intersection(z.object({
102
+ 'inbox': z.object({
103
+ 'next-turn': z.array(z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema4)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema4))])).readonly(),
104
+ 'next-step': z.array(z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema4)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema4))])).readonly(),
105
+ }).optional(),
102
106
  'agentPreset': z.union([z.literal(null), z.string()]).optional(),
103
107
  'title': z.union([z.literal(null), z.string()]).optional(),
104
108
  'todos': z.union([z.literal(null), z.array(z.object({
@@ -227,12 +231,8 @@ const _deepseek_ai_dsh_api_session_controller_session_follow_result$schema = z.u
227
231
  'time': z.number().readonly(),
228
232
  'data': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))]).readonly(),
229
233
  'ignorable': z.literal(true).readonly().optional(),
230
- 'sourceEventSeqs': z.array(z.number()).readonly().optional(),
231
- 'surfaceOp': z.union([z.literal("append"), z.object({
232
- 'op': z.literal("replace").readonly(),
233
- 'start': z.number().readonly(),
234
- 'end': z.number().readonly(),
235
- })]).readonly().optional(),
234
+ 'sourceEventSeqs': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))]).readonly().optional(),
235
+ 'surfaceOp': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))]).readonly().optional(),
236
236
  }).readonly(),
237
237
  }), z.object({
238
238
  'type': z.literal("snapshot").readonly(),
@@ -256,18 +256,18 @@ const _deepseek_ai_dsh_api_session_controller_session_follow_result$schema = z.u
256
256
  'time': z.number().readonly(),
257
257
  'data': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))]).readonly(),
258
258
  'ignorable': z.literal(true).readonly().optional(),
259
- 'sourceEventSeqs': z.array(z.number()).readonly().optional(),
260
- 'surfaceOp': z.union([z.literal("append"), z.object({
261
- 'op': z.literal("replace").readonly(),
262
- 'start': z.number().readonly(),
263
- 'end': z.number().readonly(),
264
- })]).readonly().optional(),
259
+ 'sourceEventSeqs': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))]).readonly().optional(),
260
+ 'surfaceOp': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))]).readonly().optional(),
265
261
  }).readonly(),
266
262
  })).readonly(),
267
263
  'hasMore': z.boolean().readonly(),
268
264
  'projections': z.object({
269
265
  'asOfSeq': z.number().readonly(),
270
266
  'values': z.intersection(z.object({
267
+ 'inbox': z.object({
268
+ 'next-turn': z.array(z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))])).readonly(),
269
+ 'next-step': z.array(z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema3)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema3))])).readonly(),
270
+ }).optional(),
271
271
  'agentPreset': z.union([z.literal(null), z.string()]).optional(),
272
272
  'title': z.union([z.literal(null), z.string()]).optional(),
273
273
  'todos': z.union([z.literal(null), z.array(z.object({
@@ -395,6 +395,10 @@ const _deepseek_ai_dsh_api_session_controller_session_list_result$schema = z.obj
395
395
  'projections': z.object({
396
396
  'asOfSeq': z.number().readonly(),
397
397
  'values': z.intersection(z.object({
398
+ 'inbox': z.object({
399
+ 'next-turn': z.array(z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema))])).readonly(),
400
+ 'next-step': z.array(z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema))])).readonly(),
401
+ }).optional(),
398
402
  'agentPreset': z.union([z.literal(null), z.string()]).optional(),
399
403
  'title': z.union([z.literal(null), z.string()]).optional(),
400
404
  'todos': z.union([z.literal(null), z.array(z.object({
@@ -520,12 +524,8 @@ const _deepseek_ai_dsh_api_session_controller_session_page_result$schema = z.obj
520
524
  'time': z.number().readonly(),
521
525
  'data': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema2)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema2))]).readonly(),
522
526
  'ignorable': z.literal(true).readonly().optional(),
523
- 'sourceEventSeqs': z.array(z.number()).readonly().optional(),
524
- 'surfaceOp': z.union([z.literal("append"), z.object({
525
- 'op': z.literal("replace").readonly(),
526
- 'start': z.number().readonly(),
527
- 'end': z.number().readonly(),
528
- })]).readonly().optional(),
527
+ 'sourceEventSeqs': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema2)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema2))]).readonly().optional(),
528
+ 'surfaceOp': z.union([z.literal(null), z.string(), z.number(), z.literal(false), z.literal(true), z.array(z.lazy(() => JsonValueRemoteCodec$schema2)), z.record(z.string(), z.lazy(() => JsonValueRemoteCodec$schema2))]).readonly().optional(),
529
529
  }).readonly(),
530
530
  })).readonly(),
531
531
  'hasMore': z.boolean().readonly(),
@@ -713,7 +713,7 @@ export const TYPERT_REMOTE = {
713
713
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionAttachmentValue',
714
714
  schema: _deepseek_ai_dsh_api_session_controller_session_attachment_result$schema,
715
715
  },
716
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":340,"column":3},
716
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":342,"column":3},
717
717
  },
718
718
  {
719
719
  id: '@deepseek-ai/dsh-api-session-controller#session/cancel',
@@ -738,7 +738,7 @@ export const TYPERT_REMOTE = {
738
738
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionCancelValue',
739
739
  schema: _deepseek_ai_dsh_api_session_controller_session_cancel_result$schema,
740
740
  },
741
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":360,"column":3},
741
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":362,"column":3},
742
742
  },
743
743
  {
744
744
  id: '@deepseek-ai/dsh-api-session-controller#session/canOpenWorkspacePath',
@@ -753,7 +753,7 @@ export const TYPERT_REMOTE = {
753
753
  typeSymbol: '@deepseek-ai/dsh-api-session-controller#session/canOpenWorkspacePath:result',
754
754
  schema: _deepseek_ai_dsh_api_session_controller_session_canOpenWorkspacePath_result$schema,
755
755
  },
756
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":265,"column":3},
756
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":267,"column":3},
757
757
  },
758
758
  {
759
759
  id: '@deepseek-ai/dsh-api-session-controller#session/control',
@@ -770,7 +770,7 @@ export const TYPERT_REMOTE = {
770
770
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionControlFrame',
771
771
  schema: _deepseek_ai_dsh_api_session_controller_session_control_result$schema,
772
772
  },
773
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":393,"column":3},
773
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":395,"column":3},
774
774
  },
775
775
  {
776
776
  id: '@deepseek-ai/dsh-api-session-controller#session/create',
@@ -795,7 +795,7 @@ export const TYPERT_REMOTE = {
795
795
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionCreateValue',
796
796
  schema: _deepseek_ai_dsh_api_session_controller_session_create_result$schema,
797
797
  },
798
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":237,"column":3},
798
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":239,"column":3},
799
799
  },
800
800
  {
801
801
  id: '@deepseek-ai/dsh-api-session-controller#session/follow',
@@ -822,7 +822,7 @@ export const TYPERT_REMOTE = {
822
822
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionFollowFrame',
823
823
  schema: _deepseek_ai_dsh_api_session_controller_session_follow_result$schema,
824
824
  },
825
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":383,"column":3},
825
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":385,"column":3},
826
826
  },
827
827
  {
828
828
  id: '@deepseek-ai/dsh-api-session-controller#session/fork',
@@ -847,7 +847,7 @@ export const TYPERT_REMOTE = {
847
847
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionForkValue',
848
848
  schema: _deepseek_ai_dsh_api_session_controller_session_fork_result$schema,
849
849
  },
850
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":318,"column":3},
850
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":320,"column":3},
851
851
  },
852
852
  {
853
853
  id: '@deepseek-ai/dsh-api-session-controller#session/list',
@@ -873,7 +873,7 @@ export const TYPERT_REMOTE = {
873
873
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionListValue',
874
874
  schema: _deepseek_ai_dsh_api_session_controller_session_list_result$schema,
875
875
  },
876
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":216,"column":9},
876
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":218,"column":9},
877
877
  },
878
878
  {
879
879
  id: '@deepseek-ai/dsh-api-session-controller#session/modelCatalog',
@@ -888,7 +888,7 @@ export const TYPERT_REMOTE = {
888
888
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#ModelCatalog',
889
889
  schema: _deepseek_ai_dsh_api_session_controller_session_modelCatalog_result$schema,
890
890
  },
891
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":256,"column":3},
891
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":258,"column":3},
892
892
  },
893
893
  {
894
894
  id: '@deepseek-ai/dsh-api-session-controller#session/openWorkspacePath',
@@ -914,7 +914,7 @@ export const TYPERT_REMOTE = {
914
914
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionOpenWorkspacePathValue',
915
915
  schema: _deepseek_ai_dsh_api_session_controller_session_openWorkspacePath_result$schema,
916
916
  },
917
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":277,"column":9},
917
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":279,"column":9},
918
918
  },
919
919
  {
920
920
  id: '@deepseek-ai/dsh-api-session-controller#session/page',
@@ -940,7 +940,7 @@ export const TYPERT_REMOTE = {
940
940
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionPage',
941
941
  schema: _deepseek_ai_dsh_api_session_controller_session_page_result$schema,
942
942
  },
943
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":371,"column":3},
943
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":373,"column":3},
944
944
  },
945
945
  {
946
946
  id: '@deepseek-ai/dsh-api-session-controller#session/prompt',
@@ -966,7 +966,7 @@ export const TYPERT_REMOTE = {
966
966
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionPromptValue',
967
967
  schema: _deepseek_ai_dsh_api_session_controller_session_prompt_result$schema,
968
968
  },
969
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":329,"column":3},
969
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":331,"column":3},
970
970
  },
971
971
  {
972
972
  id: '@deepseek-ai/dsh-api-session-controller#session/rename',
@@ -991,7 +991,7 @@ export const TYPERT_REMOTE = {
991
991
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionRenameValue',
992
992
  schema: _deepseek_ai_dsh_api_session_controller_session_rename_result$schema,
993
993
  },
994
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":308,"column":3},
994
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":310,"column":3},
995
995
  },
996
996
  {
997
997
  id: '@deepseek-ai/dsh-api-session-controller#session/search',
@@ -1017,7 +1017,7 @@ export const TYPERT_REMOTE = {
1017
1017
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionSearchValue',
1018
1018
  schema: _deepseek_ai_dsh_api_session_controller_session_search_result$schema,
1019
1019
  },
1020
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":227,"column":3},
1020
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":229,"column":3},
1021
1021
  },
1022
1022
  {
1023
1023
  id: '@deepseek-ai/dsh-api-session-controller#session/selectModel',
@@ -1042,7 +1042,7 @@ export const TYPERT_REMOTE = {
1042
1042
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionSelectModelValue',
1043
1043
  schema: _deepseek_ai_dsh_api_session_controller_session_selectModel_result$schema,
1044
1044
  },
1045
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":247,"column":3},
1045
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":249,"column":3},
1046
1046
  },
1047
1047
  {
1048
1048
  id: '@deepseek-ai/dsh-api-session-controller#session/updateQueue',
@@ -1067,7 +1067,7 @@ export const TYPERT_REMOTE = {
1067
1067
  typeSymbol: '@deepseek-ai/dsh-api-session-controller/types#SessionUpdateQueueValue',
1068
1068
  schema: _deepseek_ai_dsh_api_session_controller_session_updateQueue_result$schema,
1069
1069
  },
1070
- sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":350,"column":3},
1070
+ sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":352,"column":3},
1071
1071
  },
1072
1072
  {
1073
1073
  id: '@deepseek-ai/dsh-api-session-controller#skills/list',
@@ -380,13 +380,14 @@ export class ApiSessionAgentController {
380
380
  */
381
381
  async composeAgent(presetId) {
382
382
  const presets = this.ctx.get('agentPresets');
383
- if (presets === undefined)
384
- return { setup: (agentCtx) => { this.installSelection(agentCtx); } };
383
+ if (presets === undefined) {
384
+ return { setup: (_agentCtx, agent) => { this.installSelection(agent); } };
385
+ }
385
386
  const resolvedId = (await presets.resolve(presetId)).id;
386
387
  return {
387
388
  agentPreset: resolvedId,
388
- setup: async (agentCtx) => {
389
- this.installSelection(agentCtx);
389
+ setup: async (agentCtx, agent) => {
390
+ this.installSelection(agent);
390
391
  await presets.mount(agentCtx, resolvedId);
391
392
  },
392
393
  };
@@ -506,10 +507,7 @@ export class ApiSessionAgentController {
506
507
  const { provider, model } = this.ctx.agentDefaultModel.currentSelection();
507
508
  return { provider, model };
508
509
  }
509
- installSelection(agentCtx) {
510
- const agent = agentCtx.agent;
511
- if (agent === undefined)
512
- throw new Error('api-session: Agent setup has no scoped Agent');
510
+ installSelection(agent) {
513
511
  this.selectionFor(agent);
514
512
  }
515
513
  /**
@@ -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
@@ -2,6 +2,7 @@
2
2
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
3
3
  import { RemoteJournalStream, RemoteSnapshotStream, RemoteStreamCarrierError, } from '@deepseek-ai/dsh-api-gateway/client';
4
4
  import { historyEntries, historyRecordFirstSeq, historyRecordLastSeq, } from "./sessions/history-records.js";
5
+ import { assertSessionWireEvent } from "./session-wire-event.js";
5
6
  export { SESSION_SEARCH_RESULT_LIMIT, SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS, } from "../types.js";
6
7
  function toSessionJournalChange(change) {
7
8
  switch (change.type) {
@@ -78,6 +79,8 @@ export class SessionEventStream extends RemoteJournalStream {
78
79
  ...(request.maxMessages === undefined ? {} : { maxMessages: request.maxMessages }),
79
80
  }, signal)) {
80
81
  if (frame.type === 'snapshot') {
82
+ for (const record of frame.records)
83
+ assertSessionWireEvent(record.event);
81
84
  if (frame.assistantStream === undefined) {
82
85
  throw new RemoteError('gateway/internal', 'session assistant stream omitted its opted-in opening baseline', {});
83
86
  }
@@ -103,6 +106,7 @@ export class SessionEventStream extends RemoteJournalStream {
103
106
  yield { type: 'notification', notification: frame.frame };
104
107
  continue;
105
108
  }
109
+ assertSessionWireEvent(frame.event);
106
110
  yield { type: 'entry', entry: frame };
107
111
  }
108
112
  }
@@ -111,6 +115,8 @@ export class SessionEventStream extends RemoteJournalStream {
111
115
  const result = await this.remote.session.page({ address: this.address, throughSeq, ...request }, signal);
112
116
  if (!result.ok)
113
117
  throw result.error;
118
+ for (const record of result.value.records)
119
+ assertSessionWireEvent(record.event);
114
120
  return result.value;
115
121
  }
116
122
  /** @inheritdoc */
@@ -38,7 +38,7 @@ export declare class SessionCommandController {
38
38
  */
39
39
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
40
40
  /**
41
- * Admit one browser prompt after explicit Agent resume and image validation.
41
+ * Reject empty content, then admit one prompt after Agent and attachment validation.
42
42
  * @param request - Session identity, prompt content, source metadata, and delivery mode.
43
43
  * @returns acknowledgement that the Agent accepted the prompt.
44
44
  */
@@ -62,6 +62,9 @@ import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time';
62
62
  import { assertNever } from '@deepseek-ai/dsh-util-values';
63
63
  import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol';
64
64
  import { ApiSessionCwdConflict, ApiSessionNotFound, ApiSessionPresetConflict, ApiSessionSubagentOwnership, apiSessionSubagentOwnershipError, hasApiSessionSubagentOwner, inspectApiSession, } from "./agent.js";
65
+ function hasPromptContent(content) {
66
+ return content.some(part => part.type !== 'text' || part.text.trim().length > 0);
67
+ }
65
68
  /** Implements Session business commands delegated by the Session Controller Remote service. */
66
69
  export class SessionCommandController {
67
70
  ctx;
@@ -271,11 +274,14 @@ export class SessionCommandController {
271
274
  }
272
275
  }
273
276
  /**
274
- * Admit one browser prompt after explicit Agent resume and image validation.
277
+ * Reject empty content, then admit one prompt after Agent and attachment validation.
275
278
  * @param request - Session identity, prompt content, source metadata, and delivery mode.
276
279
  * @returns acknowledgement that the Agent accepted the prompt.
277
280
  */
278
281
  async prompt(request) {
282
+ if (!hasPromptContent(request.content)) {
283
+ throw new RemoteError('gateway/bad-request', 'prompt content must include non-whitespace text or an attachment', {});
284
+ }
279
285
  const clientTimeZone = request.clientTimeZone === undefined
280
286
  ? undefined
281
287
  : canonicalClientTimeZone(request.clientTimeZone);
@@ -379,9 +385,13 @@ export class SessionCommandController {
379
385
  * @returns acknowledgement that the queue mutation was applied.
380
386
  */
381
387
  updateQueue(request) {
382
- if (request.action.kind === 'edit'
383
- && request.action.content.some(block => block.type !== 'text')) {
384
- throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
388
+ if (request.action.kind === 'edit') {
389
+ if (request.action.content.some(block => block.type !== 'text')) {
390
+ throw new RemoteError('session/attachment-invalid', 'queue edits accept text content only', { reason: 'QUEUE_EDIT_NON_TEXT' });
391
+ }
392
+ if (!hasPromptContent(request.action.content)) {
393
+ throw new RemoteError('gateway/bad-request', 'queue edit content must include non-whitespace text', {});
394
+ }
385
395
  }
386
396
  const agent = this.ctx.agents.get(request.sessionId);
387
397
  if (agent === undefined) {
@@ -15,7 +15,6 @@ export declare class SessionControlController {
15
15
  control(signal: AbortSignal): AsyncIterable<SessionControlFrame>;
16
16
  private baseline;
17
17
  private projectionBaseline;
18
- private onSessionEvent;
19
18
  private onJobsChanged;
20
19
  private jobsFor;
21
20
  private broadcast;
@@ -7,7 +7,6 @@ export class SessionControlController {
7
7
  /** @param ctx - Host context carrying live Agent, projection, and jobs services. */
8
8
  constructor(ctx) {
9
9
  this.ctx = ctx;
10
- ctx.on('session/event', (session, event) => { this.onSessionEvent(session, event); });
11
10
  ctx.sessionProjections.onChanged((session, key, value, seq) => {
12
11
  this.broadcast({
13
12
  type: 'projection',
@@ -16,6 +15,16 @@ export class SessionControlController {
16
15
  value: value,
17
16
  seq,
18
17
  });
18
+ if (key !== 'inbox')
19
+ return;
20
+ const agent = this.ctx.agents.get(session.id);
21
+ if (agent?.session !== session)
22
+ return;
23
+ this.broadcast({
24
+ type: 'queue',
25
+ sessionId: session.id,
26
+ items: queueItemsFromInbox(value),
27
+ });
19
28
  });
20
29
  ctx.inject(['jobs'], (jobsCtx) => {
21
30
  jobsCtx.jobs.onJobsChanged((owner) => { this.onJobsChanged(owner); });
@@ -76,18 +85,6 @@ export class SessionControlController {
76
85
  }
77
86
  return blocks;
78
87
  }
79
- onSessionEvent(session, event) {
80
- if (event.type !== 'agent/inbox/spliced')
81
- return;
82
- const agent = this.ctx.agents.get(session.id);
83
- if (agent?.session !== session)
84
- return;
85
- this.broadcast({
86
- type: 'queue',
87
- sessionId: session.id,
88
- items: queueItems(agent, event.data),
89
- });
90
- }
91
88
  onJobsChanged(owner) {
92
89
  if (owner !== undefined) {
93
90
  this.broadcast({ type: 'jobs', sessionId: owner.id, jobs: this.jobsFor(owner) });
@@ -151,21 +148,21 @@ class ControlQueue {
151
148
  }
152
149
  }
153
150
  }
154
- function queueItems(agent, splice) {
155
- const project = (target) => {
156
- const messages = target === 'next-turn' ? agent.inbox.nextTurn : agent.inbox.nextStep;
157
- return splice?.target === target
158
- ? messages.toSpliced(splice.start, splice.removedCount ?? 0, ...splice.inserted)
159
- : messages;
160
- };
151
+ function queueItems(agent) {
152
+ return queueItemsFromInbox({
153
+ 'next-turn': agent.inbox.nextTurn,
154
+ 'next-step': agent.inbox.nextStep,
155
+ });
156
+ }
157
+ function queueItemsFromInbox(inbox) {
161
158
  return [
162
- ...project('next-turn').map(message => ({
159
+ ...inbox['next-turn'].map(message => ({
163
160
  id: message.id,
164
161
  placement: 'queued',
165
162
  ...promptRpcId(message),
166
163
  message: { id: message.id, content: message.content },
167
164
  })),
168
- ...project('next-step').map(message => ({
165
+ ...inbox['next-step'].map(message => ({
169
166
  id: message.id,
170
167
  placement: message.source.kind === 'user' ? 'steering' : 'context',
171
168
  ...promptRpcId(message),
@@ -98,6 +98,7 @@ import { ApiSessionList } from "./list.js";
98
98
  import { buildModelCatalog } from "./catalog.js";
99
99
  import { installModelSelectionProjection } from "./model-selection-projection.js";
100
100
  import { SessionSkillCatalog } from "./skill-catalog.js";
101
+ import { SessionMediaReferences } from "./media-references.js";
101
102
  export { ApiSessionNotFound } from "./agent.js";
102
103
  export { SessionFileReferences } from "./file-references.js";
103
104
  export { SessionSkillCatalog } from "./skill-catalog.js";
@@ -209,6 +210,7 @@ let SessionController = (() => {
209
210
  this.canOpenPath = internals.canOpenPath
210
211
  ?? (() => config.nativeOpen ?? (internals.openPath !== undefined || canOpenNativePath()));
211
212
  ctx.plugin(SessionFileReferences);
213
+ ctx.plugin(SessionMediaReferences);
212
214
  ctx.plugin(SessionSkillCatalog);
213
215
  ctx.on('session/created', (session) => {
214
216
  ctx.emit('api-session/added', this.listState.summaryFor(session));
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Authenticated GET/HEAD /api/file reads bounded file responses through
3
+ * the composed filesystem provider. Paths and MIME types do not restrict access;
4
+ * the connection service authenticates requests before this handler.
5
+ * @module @deepseek-ai/dsh-api-session-controller/media-references
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ /**
9
+ * File-display contribution. The connection service supplies authentication;
10
+ * `ctx.fs` supplies the execution world's paths, reads, and access policy.
11
+ */
12
+ export declare const SessionMediaReferences: {
13
+ inject: string[];
14
+ apply(ctx: Context): void;
15
+ };
16
+ //# sourceMappingURL=media-references.d.ts.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Authenticated GET/HEAD /api/file reads bounded file responses through
3
+ * the composed filesystem provider. Paths and MIME types do not restrict access;
4
+ * the connection service authenticates requests before this handler.
5
+ * @module @deepseek-ai/dsh-api-session-controller/media-references
6
+ */
7
+ import { isAbsolute } from 'node:path';
8
+ import { FsError } from '@deepseek-ai/dsh-fs';
9
+ import mime from 'mime-types';
10
+ const BASE_HEADERS = {
11
+ 'Cache-Control': 'private, no-store',
12
+ 'X-Content-Type-Options': 'nosniff',
13
+ // HTML and SVG files may be opened directly on the authenticated API origin.
14
+ 'Content-Security-Policy': "sandbox; default-src 'none'",
15
+ };
16
+ async function serveFile(request, fs, maxBytes) {
17
+ const fail = (status, text) => new Response(request.method === 'HEAD' ? null : text, { status, headers: BASE_HEADERS });
18
+ const path = new URL(request.url).searchParams.get('path');
19
+ if (path === null || path.length === 0)
20
+ return fail(400, 'missing path');
21
+ if (path.includes('\0') || !isAbsolute(path))
22
+ return fail(400, 'absolute path required');
23
+ try {
24
+ const target = await fs.resolve(path, { signal: request.signal });
25
+ const mediaType = mime.lookup(target.displayPath) || 'application/octet-stream';
26
+ const headers = {
27
+ ...BASE_HEADERS,
28
+ 'Content-Type': mediaType,
29
+ };
30
+ if (request.method === 'HEAD') {
31
+ const info = await fs.stat(target, request.signal);
32
+ if (info === undefined)
33
+ return fail(404, 'not found');
34
+ if (info.type !== 'file')
35
+ return fail(403, 'not a regular file');
36
+ if (info.size !== undefined) {
37
+ if (info.size > maxBytes)
38
+ return fail(413, 'file exceeds byte limit');
39
+ headers['Content-Length'] = String(info.size);
40
+ }
41
+ return new Response(null, { headers });
42
+ }
43
+ const bytes = await fs.readBytes(target, request.signal, maxBytes);
44
+ headers['Content-Length'] = String(bytes.byteLength);
45
+ return new Response(bytes.slice(), { headers });
46
+ }
47
+ catch (error) {
48
+ if (!(error instanceof FsError))
49
+ throw error;
50
+ const statuses = {
51
+ FS_NOT_FOUND: 404,
52
+ FS_NOT_REGULAR_FILE: 403,
53
+ FS_PERMISSION_DENIED: 403,
54
+ FS_SANDBOX_DENIED: 403,
55
+ FS_TOO_LARGE: 413,
56
+ FS_ABORTED: 499,
57
+ };
58
+ return fail(statuses[error.code] ?? 500, error.code);
59
+ }
60
+ }
61
+ /**
62
+ * File-display contribution. The connection service supplies authentication;
63
+ * `ctx.fs` supplies the execution world's paths, reads, and access policy.
64
+ */
65
+ export const SessionMediaReferences = {
66
+ inject: ['connection', 'fs', 'attachments'],
67
+ apply(ctx) {
68
+ const maxBytes = ctx.attachments.imageLimits.maxImageBytes;
69
+ ctx.effect(() => ctx.connection.fetch.register({
70
+ path: '/api/file',
71
+ methods: ['GET', 'HEAD'],
72
+ requestBody: 'buffered',
73
+ fetch: request => serveFile(request, ctx.fs, maxBytes),
74
+ }), 'session-controller: /api/file');
75
+ },
76
+ };
77
+ //# sourceMappingURL=media-references.js.map
@@ -134,6 +134,7 @@ export interface ModelCatalog {
134
134
  /** One client-requested mutation of a still-pending queue item. */
135
135
  export type QueueAction = {
136
136
  readonly kind: 'edit';
137
+ /** Non-empty text-only replacement content. */
137
138
  readonly content: readonly ContentBlock[];
138
139
  } | {
139
140
  readonly kind: 'remove';
@@ -293,6 +294,7 @@ export interface SessionPromptRequest {
293
294
  readonly requestId: SessionRequestId;
294
295
  readonly sessionId: SessionId;
295
296
  readonly mode: 'queue' | 'steer';
297
+ /** At least one non-whitespace text part or attachment. */
296
298
  readonly content: readonly PromptContentPart[];
297
299
  readonly clientTimeZone?: string;
298
300
  }
@@ -377,23 +379,29 @@ export interface SessionWireHeader {
377
379
  readonly delegationDepth?: number;
378
380
  readonly agentPreset?: string;
379
381
  }
380
- /** Browser wire form of one Session surface operation. */
382
+ /** Browser wire surface operation; replacement endpoints are earlier event seqs in surface order. */
381
383
  export type SessionWireSurfaceOp = 'append' | {
382
384
  readonly op: 'replace';
383
- readonly start: number;
384
- readonly end: number;
385
+ readonly startSeq: number;
386
+ readonly endSeq: number;
385
387
  };
386
- /** One history-page record. V2 embeds compact Assistant streams inside events. */
388
+ /** One history-page record with compact Assistant streams embedded inside events. */
387
389
  export type SessionHistoryRecord = SessionEventEntry;
388
- /** Session event wire form; durable readers own recognition of merge-extensible event names. */
390
+ /**
391
+ * Exact Session event envelope accepted by the Client journal adapter.
392
+ * Surface events require surfaceOp; only non-Assistant surface events may cite earlier sources.
393
+ * Durable readers own recognition of merge-extensible event names.
394
+ */
389
395
  export interface SessionWireEvent {
390
396
  readonly type: string;
391
397
  readonly seq: number;
392
398
  readonly time: number;
393
399
  readonly data: JsonValue;
394
400
  readonly ignorable?: true;
395
- readonly sourceEventSeqs?: number[];
396
- readonly surfaceOp?: SessionWireSurfaceOp;
401
+ /** Earlier sources on current surface events; opaque JSON on unknown ignorable events. */
402
+ readonly sourceEventSeqs?: JsonValue;
403
+ /** Canonical placement on current surface events; opaque JSON on unknown ignorable events. */
404
+ readonly surfaceOp?: JsonValue;
397
405
  }
398
406
  /** One message-aligned backwards-history request. */
399
407
  export interface SessionPageRequest {