dsh-plugin-mobile-gateway 0.7.3 → 0.7.5

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.
@@ -13,88 +13,52 @@ function requireArray(value, endpoint) {
13
13
  return value
14
14
  }
15
15
 
16
- function chunkRowLength(event) {
17
- if (event.type === 'chunkrow/tool-call-chunks') return requireArray(event.data?.args, event.type).length
18
- return requireArray(event.data?.texts, event.type).length
16
+ export const DSH_VERSION = '0.1.5-rc.2'
17
+ export const SESSION_FORMAT_VERSION = 3
18
+
19
+ export function readHistoryRecords(records) {
20
+ return requireArray(records, 'session history').map(record => {
21
+ const value = requireRecord(record, 'session history record')
22
+ if (value.type !== 'event') throw new Error('DSH 0.1.5-rc.2 history requires event records')
23
+ const event = requireRecord(value.event, 'session history event')
24
+ if (!Number.isSafeInteger(event.seq) || event.seq < 0) throw new Error('invalid history sequence')
25
+ return event
26
+ })
19
27
  }
20
28
 
21
- // DSH 0.1.2 compresses consecutive assistant deltas inside history pages.
22
- // dsh-mobile-v1 predates that storage shape, so restore the exact logical
23
- // SessionEvent sequence at this boundary instead of teaching every client a
24
- // Host-specific encoding detail.
25
- function expandChunkRow(event) {
26
- const data = requireRecord(event.data, event.type)
27
- const values = event.type === 'chunkrow/tool-call-chunks'
28
- ? requireArray(data.args, event.type)
29
- : requireArray(data.texts, event.type)
30
- const gaps = requireArray(data.dt, event.type)
31
- if (gaps.length !== Math.max(0, values.length - 1)) {
32
- throw new Error(`${event.type} returned invalid timing data`)
33
- }
34
- const events = []
35
- let time = event.time
36
- for (let index = 0; index < values.length; index += 1) {
37
- if (index > 0) time += gaps[index - 1]
38
- let chunk
39
- if (event.type === 'chunkrow/text-chunks') {
40
- chunk = { type: 'text-delta', index: data.index, text: values[index] }
41
- } else if (event.type === 'chunkrow/reasoning-chunks') {
42
- chunk = { type: 'reasoning-delta', index: data.index, text: values[index] }
43
- } else {
44
- chunk = {
45
- type: 'tool-call-delta',
46
- index: data.index,
47
- id: data.id,
48
- ...(Object.hasOwn(data, 'name') ? { name: data.name } : {}),
49
- argumentsDelta: values[index],
50
- }
51
- }
52
- events.push({
53
- type: 'assistant/chunk',
54
- seq: event.seq + index,
55
- time,
56
- data: { turn: data.turn, step: data.step, chunk },
29
+ export function readSessionSnapshot(frame, sessionId) {
30
+ requireRecord(frame, 'session/follow')
31
+ if (frame.type !== 'snapshot' || !Number.isSafeInteger(frame.cursor) || frame.cursor < -1
32
+ || frame.header?.id !== sessionId) throw new Error('session/follow returned an invalid snapshot')
33
+ if (frame.header.version !== SESSION_FORMAT_VERSION) {
34
+ throw Object.assign(new Error('mobile-gateway requires DSH 0.1.5-rc.2 Session format 3'), {
35
+ code: 'unsupported-session-format',
57
36
  })
58
37
  }
59
- return events
60
- }
61
-
62
- export function expandHistoryRecords(records) {
63
- const events = []
64
- for (const record of requireArray(records, 'session history')) {
65
- const value = requireRecord(record, 'session history record')
66
- const event = requireRecord(value.event, 'session history event')
67
- if (value.type === 'event') {
68
- events.push(event)
69
- continue
70
- }
71
- if (value.type !== 'chunks' || ![
72
- 'chunkrow/text-chunks',
73
- 'chunkrow/reasoning-chunks',
74
- 'chunkrow/tool-call-chunks',
75
- ].includes(event.type)) {
76
- throw new Error('session history returned an unsupported record')
77
- }
78
- if (chunkRowLength(event) === 0) throw new Error(`${event.type} returned an empty run`)
79
- events.push(...expandChunkRow(event))
38
+ return {
39
+ events: readHistoryRecords(frame.records).map(event => ({ event })),
40
+ hasMore: frame.hasMore === true,
41
+ projections: requireRecord(frame.projections, 'session/follow projections'),
42
+ historyFormatVersion: frame.header.version,
43
+ cursor: frame.cursor,
80
44
  }
81
- return events
82
45
  }
83
46
 
84
47
  async function firstStreamFrame(gateway, namespace, method, args, signal) {
85
- const ownedAbort = signal === undefined ? new AbortController() : null
86
- const streamSignal = signal ?? ownedAbort.signal
87
- const iterable = await gateway.stream({ namespace, method, args, signal: streamSignal })
88
- const iteratorFactory = iterable?.[Symbol.asyncIterator] ?? iterable?.[Symbol.iterator]
89
- if (typeof iteratorFactory !== 'function') throw new Error(`${namespace}/${method} returned an invalid stream`)
90
- const iterator = iteratorFactory.call(iterable)
48
+ const ownedAbort = new AbortController()
49
+ const streamSignal = signal ? AbortSignal.any([signal, ownedAbort.signal]) : ownedAbort.signal
50
+ let iterator
91
51
  try {
52
+ const iterable = await gateway.stream({ namespace, method, args, signal: streamSignal })
53
+ const iteratorFactory = iterable?.[Symbol.asyncIterator] ?? iterable?.[Symbol.iterator]
54
+ if (typeof iteratorFactory !== 'function') throw new Error(`${namespace}/${method} returned an invalid stream`)
55
+ iterator = iteratorFactory.call(iterable)
92
56
  const first = await iterator.next()
93
57
  if (first.done) throw new Error(`${namespace}/${method} ended before its opening frame`)
94
58
  return first.value
95
59
  } finally {
96
- ownedAbort?.abort()
97
- if (typeof iterator.return === 'function') await iterator.return()
60
+ ownedAbort.abort()
61
+ if (typeof iterator?.return === 'function') await iterator.return()
98
62
  }
99
63
  }
100
64
 
@@ -102,7 +66,7 @@ function requestArgs(request) {
102
66
  return { request }
103
67
  }
104
68
 
105
- // The RC.1 SessionController intentionally names its reserved list argument
69
+ // The 0.1.5-rc.2 SessionController names its reserved list argument
106
70
  // `_request`. Typert descriptors preserve that source parameter name exactly,
107
71
  // so this endpoint cannot share the normal `{ request }` wrapper.
108
72
  function sessionListArgs(request) {
@@ -134,20 +98,19 @@ export function createDshHostAdapter(typertGateway) {
134
98
  requestArgs(request),
135
99
  signal,
136
100
  ), 'session/follow')
137
- if (frame.type !== 'snapshot' || !Number.isSafeInteger(frame.cursor)) {
138
- throw new Error('session/follow returned an invalid opening snapshot')
139
- }
140
- return frame
101
+ return readSessionSnapshot(frame, request.address.sessionId)
141
102
  }
142
103
 
143
104
  const history = async (payload, signal) => {
144
105
  const request = {
145
106
  address: { kind: 'session', sessionId: payload.sessionId },
146
- ...(payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
107
+ // Older-page requests need only the opening cursor/projections, not another large latest page.
108
+ ...(payload.beforeSeq !== undefined ? { maxMessages: 1 }
109
+ : payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
147
110
  }
148
111
  const snapshot = await sessionSnapshot(request, signal)
149
- let records = snapshot.records
150
- let hasMore = snapshot.hasMore === true
112
+ let events = snapshot.events
113
+ let hasMore = snapshot.hasMore
151
114
  if (payload.beforeSeq !== undefined) {
152
115
  const page = requireRecord(await invoke('session', 'page', requestArgs({
153
116
  address: request.address,
@@ -155,13 +118,13 @@ export function createDshHostAdapter(typertGateway) {
155
118
  beforeSeq: payload.beforeSeq,
156
119
  ...(payload.maxMessages === undefined ? {} : { maxMessages: payload.maxMessages }),
157
120
  }), signal), 'session/page')
158
- records = page.records
121
+ events = readHistoryRecords(page.records).map(event => ({ event }))
159
122
  hasMore = page.hasMore === true
160
123
  }
161
124
  return {
162
- events: expandHistoryRecords(records).map(event => ({ event })),
125
+ ...snapshot,
126
+ events,
163
127
  hasMore,
164
- projections: requireRecord(snapshot.projections, 'session/follow projections'),
165
128
  }
166
129
  }
167
130
 
@@ -172,10 +135,13 @@ export function createDshHostAdapter(typertGateway) {
172
135
 
173
136
  const commands = {
174
137
  list: (sessionId, signal) => invoke('commands', 'list', { agentId: sessionId }, signal),
175
- execute: (sessionId, line, images, signal) => invoke(
138
+ // DSH 0.1.5-rc.2 accepts `submittedAttachments`. The parsed
139
+ // wire images already carry the { type: 'image', ... } shape it expects, so
140
+ // only the field name changes.
141
+ execute: (sessionId, line, attachments, signal) => invoke(
176
142
  'commands',
177
143
  'execute',
178
- { agentId: sessionId, line, images },
144
+ { agentId: sessionId, line, submittedAttachments: attachments },
179
145
  signal,
180
146
  ),
181
147
  }
@@ -196,6 +162,8 @@ export function createDshHostAdapter(typertGateway) {
196
162
  ])
197
163
  return {
198
164
  version: 'remote-gateway',
165
+ dshVersion: DSH_VERSION,
166
+ historyFormatVersion: SESSION_FORMAT_VERSION,
199
167
  cwd: os.homedir(),
200
168
  attachedSessions: Array.isArray(sessions?.items) ? sessions.items.length : 0,
201
169
  canOpenPath: canOpenPath === true,
@@ -301,6 +269,14 @@ export function createDshHostAdapter(typertGateway) {
301
269
  describe: (_payload = {}, signal) => describeHost(signal),
302
270
  },
303
271
  describeHost,
272
+ openSessionStream(sessionId, signal) {
273
+ return typertGateway.stream({
274
+ namespace: 'session', method: 'follow',
275
+ // Bound the opening window at the Host; older records remain available via session.page.
276
+ args: requestArgs({ address: { kind: 'session', sessionId }, maxMessages: 12, assistantStream: true }),
277
+ signal,
278
+ })
279
+ },
304
280
  openControlStream(signal) {
305
281
  return typertGateway.stream({ namespace: 'session', method: 'control', args: {}, signal })
306
282
  },