dsh-remote-bridge 2.8.0 → 2.8.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.
package/README.en.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.8.0-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.8.1-087f8c">
13
13
  <img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
14
14
  <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
15
15
  <img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
@@ -402,7 +402,7 @@ inside the zero-change set, and 4 slots retained with 0 removed.
402
402
 
403
403
  ## Project
404
404
 
405
- - Version: `v2.8.0`
405
+ - Version: `v2.8.1`
406
406
  - **v2.7.1 is a defect-fix release**: the kernel's `sessionController.resolveAgent()` returns an
407
407
  `{ agent }` / `{ error }` **envelope**, not the Agent itself (`dsh-api-session-controller/lib/index.js:365-368`
408
408
  `liveAgent()` and `:221-246` `resolve()` both have that shape). The bridge used to hand the envelope
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.8.0-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.8.1-087f8c">
13
13
  <img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
14
14
  <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
15
15
  <img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
@@ -433,7 +433,7 @@ DSH **0.2.1-rc.3** 隔离实例上**实跑验过**,不是静态推断:插件
433
433
 
434
434
  ## 项目信息
435
435
 
436
- - 当前版本:`v2.8.0`
436
+ - 当前版本:`v2.8.1`
437
437
  - **v2.7.1 是缺陷修复版**:内核 `sessionController.resolveAgent()` 返回的是 `{ agent }` / `{ error }`
438
438
  **包装对象**而非 Agent 本体(`dsh-api-session-controller/lib/index.js:365-368` 的 `liveAgent()` 与
439
439
  `:221-246` 的 `resolve()` 都是这个形状),桥此前把包装对象直接当 Agent 传给 `fileUploads.upload()`,
@@ -1,7 +1,7 @@
1
1
  asyncapi: 3.0.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile Events
4
- version: 2.8.0
4
+ version: 2.8.1
5
5
  description: |
6
6
  Live file and authorized DSH chat events. Reconnect clients must refresh
7
7
  REST state; unbounded replay is not provided.
@@ -21,6 +21,13 @@ info:
21
21
  (message too big).
22
22
  - A server ping is sent every 30 seconds; a connection that misses the
23
23
  pong for two consecutive intervals is terminated.
24
+ - `/ws/mobile` gives the same three guarantees with its own counters: a
25
+ 32-connection limit (checked **before** the upgrade, so the 33rd gets
26
+ HTTP `429` with `Retry-After: 5` rather than a WebSocket close code), a
27
+ 30-second ping with two-interval tolerance, and a 512-frame send queue
28
+ whose overflow is reported as `connection-resync-required` (the
29
+ `/ws/mobile` spelling of the `connection.resyncRequired` signal; same
30
+ payload, see `ConnectionResyncRequired`).
24
31
  - Each connection has a 512-frame send queue. Under backpressure the
25
32
  server first drops rebuildable `chat.message.delta` frames, then other
26
33
  rebuildable frames queued ahead, and as a last resort the oldest state
@@ -35,18 +42,190 @@ servers:
35
42
  description: Default listener; the bind IP and port are configurable from the local DSH WebUI
36
43
  protocol: ws
37
44
  pathname: /api/v1/events
45
+ mobile:
46
+ host: 127.0.0.1:3090
47
+ description: |
48
+ Same listener as `local`, different path. `/ws/mobile` is a **bidirectional**
49
+ channel (request/response frames plus server push), unlike the one-way
50
+ `/api/v1/events` broadcast. Port default is 3090; the bind IP/port are
51
+ configurable from the local DSH WebUI.
52
+ protocol: ws
53
+ pathname: /ws/mobile
38
54
  channels:
39
55
  events:
40
56
  address: /api/v1/events
41
57
  messages:
42
58
  workspaceEvent:
43
59
  $ref: '#/components/messages/WorkspaceEvent'
60
+ mobile:
61
+ address: /ws/mobile
62
+ title: Mobile gateway (bidirectional frames)
63
+ description: |
64
+ Mobile-facing gateway added in v2.8: the client sends `{"type": ...}` frames,
65
+ the server answers `{"kind": ...}` frames and may push unsolicited frames
66
+ (`event`, `question-requested`, `approval-requested`, `speech-state`, ...).
67
+
68
+ **WS first, REST fallback.** Every business operation here has a REST
69
+ equivalent under `/api/v1` (same device token, same scope/root/session
70
+ authorization). A client should fall back to REST when the socket cannot be
71
+ established, or when a frame answers `error` with code `NOT_IMPLEMENTED`
72
+ (capability temporarily missing) — not treat it as "the feature does not
73
+ exist". `UNKNOWN_TYPE` means the client sent a frame this server does not
74
+ know at all (a client bug, not a capability gap).
75
+
76
+ ## Handshake and authentication
77
+
78
+ - Path: `ws://HOST:PORT/ws/mobile` (upgrade on the main HTTP server).
79
+ - The first requested subprotocol **must** be `dsh-mobile-v1`. A connection
80
+ whose negotiated subprotocol differs is closed with code `4001`.
81
+ - Three credential carriers, all resolved through the same `principal()`
82
+ predicate as `/api/v1` (device token / scopes / roots / revocation):
83
+ 1. **First pairing** — subprotocols `dsh-mobile-v1, dsh-pair.<code>` plus
84
+ header `X-DSH-Device-ID` (required; installation-stable id, validated
85
+ only, never persisted) and optional `X-DSH-Device-Name`
86
+ (`Base64(UTF-8)` display name; missing or invalid base64 falls back to
87
+ the device id). On success the server sends `paired`
88
+ (`{token, device}`) **before** `hello`; the pairing code is one-shot.
89
+ 2. **Later connections** — subprotocols `dsh-mobile-v1, dsh-auth.<token>`.
90
+ 3. **Later connections (header form)** — `Authorization: Bearer <token>`.
91
+ - Credentials in the URL query string are **rejected** (`4001`): a token in
92
+ a URL ends up in access logs.
93
+ - Connection cap is 32 authenticated sockets (configurable). Authentication
94
+ happens **before** counting; the 33rd authenticated upgrade is refused at
95
+ the HTTP Upgrade stage with `429` + `Retry-After: 5`.
96
+ - Every accepted connection receives `hello` as its first frame (after
97
+ `paired` on the pairing flow). `protocol` is currently `3`;
98
+ `capabilities` shares its source with `connection.ready`.
99
+
100
+ ## Frame shapes
101
+
102
+ Client → server frames carry `type` (the `MobileClientFrame` schema
103
+ enumerates the full set); server → client frames carry `kind` (the
104
+ `MobileServerFrame` schema enumerates the full set). Unknown `type` answers
105
+ `error` with code `UNKNOWN_TYPE`.
106
+
107
+ ## requestId correlation (request/response)
108
+
109
+ `requestId` is an **opaque client-chosen string**. Correlation is
110
+ **uniform**: every frame that answers a client request carries the
111
+ request's `requestId` back verbatim on `data.requestId`.
112
+
113
+ The echo is implemented once, centrally, in
114
+ `MobileGatewayWsHandler.sendFrame` — handlers do not copy the id
115
+ themselves any more (a few still do; the central injection never
116
+ overwrites a value already present in `data`). The id travels through
117
+ `AsyncLocalStorage`, not through instance state, because frame dispatch is
118
+ **concurrent**: `webSocket.on('message')` calls `void
119
+ this.handleMessage(...)` without awaiting, so several requests can be in
120
+ flight on one connection. A shared slot would cross-talk; a per-async-chain
121
+ context cannot.
122
+
123
+ **Correlated (response) frames** — every `kind` that answers a request,
124
+ including `error` and `sent`:
125
+
126
+ `sessions`, `history`, `workspaces`, `workspace-create`, `attachment`,
127
+ `message` (answers with `sent`), `models`, `providers`, `commands`,
128
+ `command-options`, `command-executed`, `command-selected`, `permission`,
129
+ `permission-options`, `agent-presets`, `host`, `default-model`,
130
+ `session-created`, `session-renamed`, `session-archived`,
131
+ `session-cancelled`, `session-stats`, `context-usage`, `tasks`, `goal*`,
132
+ `agent-team`, `subagents`, `schedule-*`, `speech-*` (except the pushes
133
+ below), `file-list`, `directory-create`, `file-resolve`,
134
+ `file-download-open`/`-cancelled`, `file-upload-open`/`-completed`/`-cancelled`,
135
+ `unsubscribe` (answers with `sent`), and **`error`**.
136
+
137
+ **Uncorrelated (unsolicited / streaming) frames** — these are never an
138
+ answer to a specific request, so they never carry `requestId`. The set is
139
+ declared in code as `REQUEST_ID_ECHO_EXCLUDED_KINDS`
140
+ (`src/host/ws-protocol.ts`):
141
+
142
+ - handshake & heartbeat: `hello`, `paired`, `pong`
143
+ - subscription & event stream: `subscribed`, `event`
144
+ - HITL pushes: `question-requested`, `approval-requested`,
145
+ `approval-resolved`, `question-resolved`
146
+ - speech pushes: `speech-state`, `speech-transcript`, `speech-preparing`
147
+ - projection / queue pushes: `tasks-updated`, `goal-updated`,
148
+ `session-title-changed`, `queue-item-updated`
149
+ - chunked transfer pushes: `file-download-chunk`,
150
+ `file-upload-chunk-accepted`
151
+
152
+ > Adding a **push** kind is a two-step change: append it to
153
+ > `REQUEST_ID_ECHO_EXCLUDED_KINDS`, and add a line here. A push kind that
154
+ > is missing from the set will start echoing a stale `requestId` from
155
+ > whatever request happened to be in flight when it fired.
156
+
157
+ Rules:
158
+
159
+ - Absent or empty string ⇒ **no** `requestId` key in the response at all
160
+ (never `null`, never `""`), so a client must not match on falsy values.
161
+ - A non-object `data` (omitted, `null`, or an array) never gets the key.
162
+ - `file-resolve`, `file-download-open`, `file-upload-open` **require**
163
+ `requestId` and answer `error` code `bad-request` without it.
164
+ - Correlation is per-connection; two connections may reuse the same id.
165
+ - Clients must time out unmatched requests themselves — the server does not
166
+ send a "request expired" frame.
167
+ - `error` frames carry **both** `data.requestId` and
168
+ `data.requestType`. `requestId` pins the failure to one in-flight
169
+ request; `requestType` says which request kind it was. Concurrent
170
+ requests of the same kind are distinguishable only by `requestId`.
171
+
172
+ ## Backpressure and close codes
173
+
174
+ Identical parameters to `/api/v1/events` (see `MobileConnectionLimits`):
175
+ a 1 MiB per-frame cap, a 512-frame per-connection send queue that drops the
176
+ oldest frame when full, and a heartbeat every 30 s (two missed intervals ⇒
177
+ terminate). Device revocation or an authorization change closes the socket
178
+ with `4001`. `connection.resyncRequired` (delivered as an `event` frame)
179
+ tells the client to re-fetch its baseline over REST; dropped frames are
180
+ never replayed.
181
+
182
+ ## Declared but not backed by this kernel build
183
+
184
+ `search`, `directories`, `defaults`, `set-default`, `queue-update`,
185
+ `plugin-catalog`, `plugin-inspect`, `plugin-install`, `plugin-settings`
186
+ answer `error` with code `NOT_IMPLEMENTED` and a human-readable `message`
187
+ pointing at the REST fallback (`requestType` echoes the frame type). They
188
+ are part of the vocabulary precisely so a client can tell "capability
189
+ missing" from "unknown frame".
190
+ messages:
191
+ clientFrame:
192
+ $ref: '#/components/messages/MobileClientFrame'
193
+ serverFrame:
194
+ $ref: '#/components/messages/MobileServerFrame'
44
195
  operations:
45
196
  receiveEvents:
46
197
  action: receive
47
198
  channel:
48
199
  $ref: '#/channels/events'
49
200
  summary: Receive authorized file and chat events after Bearer authentication during upgrade.
201
+ sendMobileClientFrame:
202
+ action: send
203
+ channel:
204
+ $ref: '#/channels/mobile'
205
+ summary: "Send a `{type: ...}` request frame; supply a `requestId` if you need to correlate the response."
206
+ description: |
207
+ The client owns this direction. `subscribe`/`unsubscribe` narrow the pushed
208
+ `event` stream to a session whitelist; `ping` is answered with `pong`;
209
+ everything else maps to a REST-equivalent operation (see `docs/project/API.md`).
210
+
211
+ Every answer frame echoes a non-empty `requestId` verbatim on
212
+ `data.requestId` — see "requestId correlation" on the `mobile` channel.
213
+ receiveMobileServerFrame:
214
+ action: receive
215
+ channel:
216
+ $ref: '#/channels/mobile'
217
+ summary: "Receive `{kind: ...}` response frames plus unsolicited pushes (event, HITL, speech state)."
218
+ description: |
219
+ Every response frame echoes the request's `requestId` on
220
+ `data.requestId` when the request supplied a non-empty one; `error` frames
221
+ do too, so a failure is always attributable to a single in-flight request.
222
+ Unsolicited frames (`hello`, `paired`, `pong`, `subscribed`, `event`,
223
+ `question-requested`, `approval-requested`, `approval-resolved`,
224
+ `question-resolved`, `speech-state`, `speech-transcript`,
225
+ `speech-preparing`, `tasks-updated`, `goal-updated`,
226
+ `session-title-changed`, `queue-item-updated`, `file-download-chunk`,
227
+ `file-upload-chunk-accepted`, `connection-resync-required`) may arrive at
228
+ any time, never carry `requestId`, and must not be mistaken for responses.
50
229
  components:
51
230
  messages:
52
231
  WorkspaceEvent:
@@ -73,6 +252,16 @@ components:
73
252
  - $ref: '#/components/schemas/ChatDeliverablesPresented'
74
253
  - type: object
75
254
  additionalProperties: true
255
+ MobileClientFrame:
256
+ name: MobileClientFrame
257
+ title: "Client → server frame (`{type: ...}`)"
258
+ payload:
259
+ $ref: '#/components/schemas/MobileClientFrame'
260
+ MobileServerFrame:
261
+ name: MobileServerFrame
262
+ title: "Server → client frame (`{kind: ...}`)"
263
+ payload:
264
+ $ref: '#/components/schemas/MobileServerFrame'
76
265
  schemas:
77
266
  ChatMessageDelta:
78
267
  type: object
@@ -271,6 +460,11 @@ components:
271
460
  do not recognize this event type ignore it safely. Example:
272
461
  `{"id":"<uuid>","type":"connection.resyncRequired","time":1761100000000,
273
462
  "data":{"dropped":12,"reason":"backpressure"}}`.
463
+ This payload is shared by both channels: `/api/v1/events` spells the
464
+ frame type `connection.resyncRequired` (dotted, matching its other event
465
+ types), while `/ws/mobile` spells the kind `connection-resync-required`
466
+ (kebab, matching every other kind there). Both are unsolicited and never
467
+ carry `requestId`.
274
468
  required: [dropped, reason]
275
469
  properties:
276
470
  dropped:
@@ -280,3 +474,399 @@ components:
280
474
  reason:
281
475
  type: string
282
476
  const: backpressure
477
+
478
+ # ───────────────────────── /ws/mobile ─────────────────────────
479
+ #
480
+ # 帧形状的事实来源:src/host/ws-protocol.ts(ClientFrameType / ServerFrameKind)、
481
+ # src/host/ws-handler.ts(基础帧 + 域处理器委派)。改一处必须同步另一处,
482
+ # tests/mobile-frame-contract.spec.ts 会做双向比对(联合成员 ⊆ 本文件 enum)。
483
+
484
+ MobileClientFrame:
485
+ type: object
486
+ title: Client → server frame
487
+ description: |
488
+ One text frame from the client, JSON encoded. `type` selects the handler;
489
+ every other field is handler-specific (see `docs/project/API.md` §
490
+ 「客户端→服务端帧」 for the per-type field table).
491
+
492
+ `requestId` is optional on most frames and **required** on `file-resolve`,
493
+ `file-download-open` and `file-upload-open`. When present and non-empty it
494
+ is echoed verbatim on `data.requestId` of **whatever frame answers this
495
+ request** — including `error` frames. See "requestId correlation" on the
496
+ `mobile` channel for the full rule set.
497
+ required: [type]
498
+ properties:
499
+ type:
500
+ type: string
501
+ description: |
502
+ The frame type. The enum is the **complete** vocabulary this server
503
+ recognises: members that the current kernel cannot back are answered
504
+ with `error` code `NOT_IMPLEMENTED` (see the channel description),
505
+ and anything outside the enum answers `error` code `UNKNOWN_TYPE`.
506
+ enum:
507
+ # 基础帧(ws-handler.ts 直接处理)
508
+ - ping
509
+ - subscribe
510
+ - unsubscribe
511
+ - message
512
+ - workspaces
513
+ - sessions
514
+ - history
515
+ - attachment
516
+ # 会话生命周期(ws-sessions.ts)
517
+ - session-create
518
+ - session-cancel
519
+ - session-archive
520
+ - session-rename
521
+ - session-agent-preset
522
+ - fork
523
+ - workspace-create
524
+ # 文件浏览 / 目录(ws-files.ts)
525
+ - file-list
526
+ - directory-create
527
+ # 分块传输(ws-file-transfer.ts)
528
+ - file-resolve
529
+ - file-download-open
530
+ - file-download-read
531
+ - file-download-cancel
532
+ - file-upload-open
533
+ - file-upload-chunk
534
+ - file-upload-finish
535
+ - file-upload-cancel
536
+ # 模型 / 设置(ws-models.ts, ws-settings.ts)
537
+ - models
538
+ - select-model
539
+ - default-model
540
+ - save-default-model
541
+ - providers
542
+ - permission-options
543
+ - permission
544
+ # 命令与二级菜单(ws-commands.ts)
545
+ - commands
546
+ - command-execute
547
+ - command-options
548
+ - command-select
549
+ # 预设(ws-presets.ts)
550
+ - agent-presets
551
+ - select-agent-preset
552
+ # 会话投影(ws-goals.ts, ws-team.ts)
553
+ - context-usage
554
+ - session-stats
555
+ - tasks
556
+ - goal
557
+ - goal-edit
558
+ - goal-pause
559
+ - goal-resume
560
+ - goal-clear
561
+ - agent-team
562
+ - subagents
563
+ # HITL(ws-hitl.ts)
564
+ - question-answer
565
+ - question-cancel
566
+ - approval-response
567
+ # 主机快照(ws-host.ts)
568
+ - host
569
+ # 语音(ws-speech.ts)
570
+ - speech-catalog
571
+ - speech-subscribe
572
+ - speech-unsubscribe
573
+ - speech-configure
574
+ - speech-prepare
575
+ - speech-cancel-preparation
576
+ - speech-transcribe
577
+ - speech-cancel
578
+ # 定时任务(ws-schedule.ts)
579
+ - schedule-catalog
580
+ - schedule-list
581
+ - schedule-history
582
+ - schedule-update
583
+ - schedule-delete
584
+ # 声明但本内核无对应 RPC → NOT_IMPLEMENTED + REST 兜底
585
+ - search
586
+ - directories
587
+ - defaults
588
+ - set-default
589
+ - queue-update
590
+ - plugin-catalog
591
+ - plugin-inspect
592
+ - plugin-install
593
+ - plugin-settings
594
+ requestId:
595
+ type: string
596
+ minLength: 1
597
+ description: |
598
+ Opaque client-chosen correlation id. Echoed verbatim on the answer
599
+ frame's `data.requestId` when non-empty; absent (or `""`) ⇒ the answer
600
+ has no `requestId` key at all.
601
+ sessionId:
602
+ type: string
603
+ description: Target session for session-scoped frames; `subscribe` requires it non-empty.
604
+ additionalProperties: true
605
+
606
+ MobileServerFrame:
607
+ type: object
608
+ title: Server → client frame
609
+ description: |
610
+ One text frame from the server, JSON encoded. `kind` selects the shape of
611
+ `data`. Every answer to a client request carries `data.requestId` when
612
+ the request supplied a non-empty one — `error` frames included. Frames
613
+ that are not an answer to a specific request never carry it: `hello`,
614
+ `paired`, `pong`, `subscribed`, `event`, the HITL pushes
615
+ (`question-requested`, `approval-requested`, `approval-resolved`,
616
+ `question-resolved`), the speech pushes (`speech-state`,
617
+ `speech-transcript`, `speech-preparing`), the projection/queue pushes
618
+ (`tasks-updated`, `goal-updated`, `session-title-changed`,
619
+ `queue-item-updated`) and the chunked-transfer pushes
620
+ (`file-download-chunk`, `file-upload-chunk-accepted`). They may arrive at
621
+ any time. The exclusion set lives in `REQUEST_ID_ECHO_EXCLUDED_KINDS`
622
+ (`src/host/ws-protocol.ts`) — see "requestId correlation" on the `mobile`
623
+ channel.
624
+
625
+ Note: `id`/`time` envelope fields are **not** stamped on `/ws/mobile`
626
+ frames (that envelope belongs to `/api/v1/events`); a client must key on
627
+ `kind` plus its own `requestId`.
628
+ required: [kind]
629
+ properties:
630
+ kind:
631
+ type: string
632
+ description: |
633
+ The frame kind. The enum is the complete set this server can emit.
634
+ `hello` is always the first frame on an authenticated connection
635
+ (`paired` precedes it on the pairing flow).
636
+ enum:
637
+ # 握手
638
+ - hello
639
+ - pong
640
+ - paired
641
+ # 基础帧应答
642
+ - subscribed
643
+ - sent
644
+ - error
645
+ - event
646
+ # 查询应答
647
+ - workspaces
648
+ - sessions
649
+ - history
650
+ - search
651
+ - attachment
652
+ # 文件
653
+ - file-list
654
+ - directory-create
655
+ - file-resolved
656
+ - file-download-opened
657
+ - file-download-chunk
658
+ - file-download-cancelled
659
+ - file-upload-opened
660
+ - file-upload-chunk-accepted
661
+ - file-upload-completed
662
+ - file-upload-cancelled
663
+ # 会话生命周期
664
+ - session-created
665
+ - session-cancelled
666
+ - session-archived
667
+ - session-renamed
668
+ - session-archives
669
+ - session-title-changed
670
+ - fork
671
+ - workspace-create
672
+ - queue-item-updated
673
+ - session-queues
674
+ - session-queue
675
+ # 模型 / 设置
676
+ - models
677
+ - default-model
678
+ - providers
679
+ - permission
680
+ - permission-options
681
+ - agent-presets
682
+ # 命令
683
+ - commands
684
+ - command-options
685
+ - command-executed
686
+ - command-selected
687
+ # 投影
688
+ - tasks
689
+ - goal
690
+ - goal-edit
691
+ - goal-pause
692
+ - goal-resume
693
+ - goal-clear
694
+ - tasks-updated
695
+ - goal-updated
696
+ - session-stats
697
+ - context-usage
698
+ - agent-team
699
+ - subagents
700
+ - host
701
+ # HITL
702
+ - question-requested
703
+ - question-response
704
+ - question-resolved
705
+ - approval-requested
706
+ - approval-response
707
+ - approval-resolved
708
+ # 语音
709
+ - speech-catalog
710
+ - speech-subscribed
711
+ - speech-unsubscribed
712
+ - speech-state
713
+ - speech-configured
714
+ - speech-preparing
715
+ - speech-preparation-cancelled
716
+ - speech-transcript
717
+ - speech-cancelled
718
+ # 定时任务
719
+ - schedule-catalog
720
+ - schedule-list
721
+ - schedule-history
722
+ - schedule-update
723
+ - schedule-delete
724
+ # 连接与反压
725
+ - connection-resync-required
726
+ data:
727
+ description: |
728
+ Kind-specific payload. `hello` uses `MobileHello`, `paired` uses
729
+ `MobilePaired`, `error` uses `MobileError`, `event` reuses
730
+ `WorkspaceEvent`. Other kinds are documented field-by-field in
731
+ `docs/project/API.md` and typed in the Kotlin SDK.
732
+ additionalProperties: true
733
+
734
+ MobileHello:
735
+ type: object
736
+ title: hello (first frame on every authenticated connection)
737
+ description: |
738
+ Sent immediately after the upgrade (and after `paired` on the pairing
739
+ flow). `capabilities` shares its source with `connection.ready`
740
+ (`src/host/capabilities.ts`), names are `<feature>-v<major>` and are never
741
+ silently removed — use them for feature detection instead of comparing
742
+ versions. `port` is the local port this connection arrived on; `clients`
743
+ is the current authenticated connection count. `device` is omitted for
744
+ loopback-admin principals (they are not devices).
745
+ required: [protocol, capabilities, authenticated]
746
+ properties:
747
+ protocol:
748
+ type: integer
749
+ const: 3
750
+ description: Mobile gateway protocol revision.
751
+ capabilities:
752
+ type: array
753
+ items: { type: string }
754
+ authenticated:
755
+ type: boolean
756
+ const: true
757
+ device:
758
+ type: object
759
+ required: [id, name, scopes, rootIds]
760
+ properties:
761
+ id: { type: string }
762
+ name: { type: string }
763
+ scopes:
764
+ type: array
765
+ items:
766
+ type: string
767
+ enum: [chat.read, chat.write, files.read, files.write, files.delete, settings.read, settings.write]
768
+ rootIds:
769
+ type: array
770
+ items: { type: string }
771
+ port: { type: integer }
772
+ clients: { type: integer, minimum: 1 }
773
+
774
+ MobilePaired:
775
+ type: object
776
+ title: paired (pairing flow only, before hello)
777
+ description: |
778
+ Carries the freshly minted device token. It is shown **once** — the
779
+ pairing code itself is one-shot, and reusing it closes the next
780
+ connection with `4001`.
781
+ required: [token, device]
782
+ properties:
783
+ token: { type: string }
784
+ device:
785
+ type: object
786
+ required: [id, name]
787
+ properties:
788
+ id: { type: string }
789
+ name: { type: string }
790
+
791
+ MobileError:
792
+ type: object
793
+ title: error
794
+ description: |
795
+ `code` distinguishes three very different situations: `NOT_IMPLEMENTED`
796
+ (this kernel build cannot back the frame — fall back to REST),
797
+ `UNKNOWN_TYPE` (the client sent a frame outside the vocabulary — a client
798
+ bug), and everything else (`MISSING_PARAMS`, `FORBIDDEN_SCOPE`,
799
+ `bad-request`, `PATH_OUTSIDE_ROOTS`, `INTERNAL_ERROR`, upstream codes…).
800
+ `message` never contains absolute host paths.
801
+ required: [code, message]
802
+ properties:
803
+ code: { type: string }
804
+ message: { type: string }
805
+ requestType:
806
+ type: string
807
+ description: The offending frame's `type` (or `kind` for a rejected response).
808
+ requestId:
809
+ type: string
810
+ description: |
811
+ The offending request's `requestId`, echoed so the failure can be
812
+ pinned to one in-flight request. Absent when the request carried none.
813
+ statusCode:
814
+ type: integer
815
+ description: Present when the failure originated from an HTTP-status-shaped upstream error.
816
+
817
+ MobileConnectionLimits:
818
+ type: object
819
+ title: Backpressure and connection limits
820
+ description: |
821
+ Constants shared with `/api/v1/events` (source of truth
822
+ `src/host/ws-protocol.ts` / `src/host/server.ts`). Clients should treat
823
+ these as documented defaults, not hard-coded contracts: they are
824
+ server-configurable, and the observable behaviour (`1013` close,
825
+ `resyncRequired`) is the real contract.
826
+ properties:
827
+ maxFrameBytes:
828
+ type: integer
829
+ const: 1048576
830
+ description: 1 MiB per frame; larger frames are refused with close `1009`.
831
+ maxWebSocketPayloadBytes:
832
+ type: integer
833
+ const: 150994944
834
+ description: 144 MiB total buffered payload per connection (`DEFAULT_MAX_WS_PAYLOAD_BYTES`).
835
+ sendQueueFrames:
836
+ type: integer
837
+ const: 512
838
+ description: Per-connection send queue; on overflow the oldest frame is dropped and a `resyncRequired` is queued.
839
+ heartbeatIntervalMs:
840
+ type: integer
841
+ const: 30000
842
+ description: Server ping cadence; two missed pongs terminate the connection.
843
+ maxConnections:
844
+ type: integer
845
+ const: 32
846
+ description: "Authenticated connections; the next upgrade gets HTTP 429 + `Retry-After: 5`."
847
+ pairingTtlMs:
848
+ type: integer
849
+ const: 600000
850
+ description: Pairing code lifetime (10 minutes, one-shot).
851
+ closeDeviceRevoked:
852
+ type: integer
853
+ const: 4001
854
+ description: Close code for revoked devices, failed authentication, or a wrong subprotocol.
855
+ fileDownload:
856
+ type: object
857
+ properties:
858
+ maxBytes: { type: integer, const: 536870912 }
859
+ chunkBytes: { type: integer, const: 524288 }
860
+ idleMs: { type: integer, const: 120000 }
861
+ maxTransfers: { type: integer, const: 4 }
862
+ fileUpload:
863
+ type: object
864
+ properties:
865
+ maxBytes: { type: integer, const: 536870912 }
866
+ chunkBytes: { type: integer, const: 262144 }
867
+ idleMs: { type: integer, const: 120000 }
868
+ maxTransfers: { type: integer, const: 4 }
869
+ interactionProtocolRevision:
870
+ type: string
871
+ const: question-approval-v2
872
+ description: HITL (question/approval) frame revision.