dsh-zen-remote 2.4.1 → 2.4.3
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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/remote-access.en.md +1 -1
- package/docs/remote-access.md +1 -1
- package/lib/client.js +12 -2
- package/lib/intercept.js +20 -5
- package/lib/intercept.js.map +1 -1
- package/lib/invoke-binary.js +5 -3
- package/lib/invoke-binary.js.map +1 -1
- package/lib/relay-access.js +32 -3
- package/lib/relay-access.js.map +1 -1
- package/lib/relay-client.js +15 -2
- package/lib/relay-client.js.map +1 -1
- package/lib/types/intercept.d.ts +1 -1
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/invoke-binary.d.ts +4 -3
- package/lib/types/invoke-binary.d.ts.map +1 -1
- package/lib/types/relay-access.d.ts +10 -3
- package/lib/types/relay-access.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/settings/SettingsSection.tsx +16 -6
- package/src/intercept.ts +20 -5
- package/src/invoke-binary.ts +5 -3
- package/src/relay-access.ts +32 -3
- package/src/relay-client.ts +13 -1
|
@@ -386,6 +386,16 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
386
386
|
// MS-7: the paired-server pool, in pairing order (empty on a legacy body),
|
|
387
387
|
// and the cap the counters and the red alert read.
|
|
388
388
|
const serverRows: readonly ClientServerView[] = clientView?.servers ?? []
|
|
389
|
+
// #56: the 立即重连 entries key off the REAL offline state, never the
|
|
390
|
+
// presence of a scheduled retry — the ladder can legitimately sit
|
|
391
|
+
// offline with no timer armed (a handshake a retry joined just died),
|
|
392
|
+
// and gating on nextRetryAt disabled every manual entrance in exactly
|
|
393
|
+
// that state. View 'unreachable' is the wire 'offline' (T55 alias);
|
|
394
|
+
// online / connecting / unpaired / revoked / incompatible stay closed,
|
|
395
|
+
// matching the route's offline-only contract.
|
|
396
|
+
const entryReconnectable = (server: ClientServerView): boolean => server.state === 'unreachable'
|
|
397
|
+
const anyReconnectable = clientView !== undefined
|
|
398
|
+
&& (serverRows.length > 0 ? serverRows.some(entryReconnectable) : clientView.state === 'unreachable')
|
|
389
399
|
const maxServers = clientView?.maxServers ?? MAX_PAIRED_SERVERS
|
|
390
400
|
// RD-3: the remote drop guard's browser-side counters, read at render
|
|
391
401
|
// time — no snapshot state of its own: the initial mount, the 15s
|
|
@@ -1386,7 +1396,7 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1386
1396
|
<Button
|
|
1387
1397
|
variant="ghost"
|
|
1388
1398
|
size="sm"
|
|
1389
|
-
disabled={entryBusy !== null || server
|
|
1399
|
+
disabled={entryBusy !== null || !entryReconnectable(server)}
|
|
1390
1400
|
onClick={() => { void runEntryReconnect(server.entryId) }}
|
|
1391
1401
|
>
|
|
1392
1402
|
{entryBusy === server.entryId ? t('settings.client.reconnectBusy') : t('settings.client.reconnect')}
|
|
@@ -1415,14 +1425,14 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1415
1425
|
<p className="zr-settings-status-line" style={{ flex: 1 }} data-down={clientView !== undefined && (clientView.state === 'unreachable' || clientView.state === 'unexpected' || clientView.state === 'revoked' || clientView.state === 'invalid-url' || clientView.state === 'incompatible') ? 'true' : undefined}>
|
|
1416
1426
|
{clientView === undefined ? '' : clientStatusText(clientView, now, t)}
|
|
1417
1427
|
</p>
|
|
1418
|
-
{/* T43
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1428
|
+
{/* T43 立即重连(#56 重订资格):按钮看真实 offline 状态
|
|
1429
|
+
('unreachable' = wire 'offline'),不再要求 nextRetryAt ——
|
|
1430
|
+
梯子可能恰好没有计时器在排(刚加入的握手死掉),那正是
|
|
1431
|
+
最需要手动入口的时刻;路由对非 offline 本就 409。 */}
|
|
1422
1432
|
<Button
|
|
1423
1433
|
variant="outline"
|
|
1424
1434
|
size="sm"
|
|
1425
|
-
disabled={clientView === undefined ||
|
|
1435
|
+
disabled={clientView === undefined || !anyReconnectable || reconnectBusy}
|
|
1426
1436
|
onClick={() => { void runReconnect() }}
|
|
1427
1437
|
>
|
|
1428
1438
|
{reconnectBusy ? t('settings.client.reconnectBusy') : t('settings.client.reconnect')}
|
package/src/intercept.ts
CHANGED
|
@@ -155,6 +155,9 @@ export const CLIENT_METHOD_FIELDS: Readonly<Record<string, readonly SessionField
|
|
|
155
155
|
// it is restored like the parent before forwarding
|
|
156
156
|
'subagents/prompt': ['request.parentSessionId', 'request.childSessionId'],
|
|
157
157
|
'subagents/interruptByParent': ['parentSessionId', 'childSessionId'],
|
|
158
|
+
// 0.2.2-rc.2 rename: only the child id travels (no parent, no mode).
|
|
159
|
+
// interruptByParent stays for 0.2.2-rc.1 (npm latest).
|
|
160
|
+
'subagents/interruptByUser': ['childSessionId'],
|
|
158
161
|
// attachments and @ references (T31) — the top-level agentId IS the session
|
|
159
162
|
// id (the gateway's agent lookup resolves it through the session-keyed
|
|
160
163
|
// agent registry).
|
|
@@ -186,6 +189,8 @@ export const CLIENT_METHOD_FIELDS: Readonly<Record<string, readonly SessionField
|
|
|
186
189
|
'workspaceFiles/read': ['workspaceFileScopeId'],
|
|
187
190
|
'workspaceFiles/readBytes': ['workspaceFileScopeId'],
|
|
188
191
|
'workspaceFiles/stat': ['workspaceFileScopeId'],
|
|
192
|
+
// 0.2.2-rc.2: official sidebar-files search bar. Same scope id.
|
|
193
|
+
'workspaceFiles/search': ['workspaceFileScopeId'],
|
|
189
194
|
// Office document preview (2.2.6) — client side of relay-access.ts's entry:
|
|
190
195
|
// `render` is located by the SAME `workspaceFileScopeId` lookup (the session
|
|
191
196
|
// id). It MUST stay identical to the server table
|
|
@@ -194,6 +199,9 @@ export const CLIENT_METHOD_FIELDS: Readonly<Record<string, readonly SessionField
|
|
|
194
199
|
// the relay is ever asked. `generation` stays unregistered: no virtual id
|
|
195
200
|
// rides it, so it answers locally (see the server table's note).
|
|
196
201
|
'officeToPdf/render': ['workspaceFileScopeId'],
|
|
202
|
+
// 0.2.2-rc.2 successor of officeToPdf/render (same scope id, same
|
|
203
|
+
// PDF-bytes result). officeToPdf/render stays for 0.2.2-rc.1.
|
|
204
|
+
'officeKit/render': ['workspaceFileScopeId'],
|
|
197
205
|
// terminal (T41a) — opens SERVER-side by explicit user request (SPEC story
|
|
198
206
|
// 45); the boundary is the standing one (shared session + paired desktop
|
|
199
207
|
// client), no extra switch. The terminal and attachment ids in these calls
|
|
@@ -214,6 +222,8 @@ export const CLIENT_METHOD_FIELDS: Readonly<Record<string, readonly SessionField
|
|
|
214
222
|
// the retain/list half locates by a plain top-level json sessionId
|
|
215
223
|
'terminal/list': ['sessionId'],
|
|
216
224
|
'terminal/retain': ['sessionId'],
|
|
225
|
+
// 0.2.2-rc.2 live Agent-terminal roster stream (same sessionId as list).
|
|
226
|
+
'terminal/watch': ['sessionId'],
|
|
217
227
|
// session/* — the global control stream and the unscoped list: merged with
|
|
218
228
|
// the relay's filtered answer by the T23b-2 routes below (never refused on
|
|
219
229
|
// arguments — a stray id in them is unowned data).
|
|
@@ -288,7 +298,7 @@ export type DispatchEnvelope =
|
|
|
288
298
|
* Everything else — `session/prompt|cancel|rename|selectModel|updateQueue`
|
|
289
299
|
* (send, cancel, rename, model switch, inbox mutation), `session/create` /
|
|
290
300
|
* `session/fork` (T31: new sessions on the server), `subagents/prompt` /
|
|
291
|
-
* `subagents/interruptByParent` (T31: prompt/interrupt a remote subagent),
|
|
301
|
+
* `subagents/interruptByParent` / `subagents/interruptByUser` (T31: prompt/interrupt a remote subagent),
|
|
292
302
|
* `fileUploads/upload` (T31: attachments into a remote session), `job/kill`,
|
|
293
303
|
* `messageFeedback/put|delete`, the workspace session-list mutations, and
|
|
294
304
|
* the T41a mutations (`goals/edit|pause|resume|clear`, `commands/execute`,
|
|
@@ -335,15 +345,20 @@ export const REMOTE_READ_METHODS: ReadonlySet<string> = new Set([
|
|
|
335
345
|
'workspaceFiles/read',
|
|
336
346
|
'workspaceFiles/readBytes',
|
|
337
347
|
'workspaceFiles/stat',
|
|
348
|
+
// 0.2.2-rc.2: official sidebar-files search — a pure Host-side walk.
|
|
349
|
+
'workspaceFiles/search',
|
|
338
350
|
// officeToPdf (2.2.6): a pure read — it converts one already-readable file
|
|
339
351
|
// into PDF bytes on the server (writing only its own scratch space, like
|
|
340
352
|
// any preview). Offline it answers `remote-offline`, never the write
|
|
341
|
-
// message.
|
|
353
|
+
// message. officeKit/render is the 0.2.2-rc.2 successor (same shape).
|
|
342
354
|
'officeToPdf/render',
|
|
355
|
+
'officeKit/render',
|
|
343
356
|
'terminal/environment',
|
|
344
357
|
'terminal/shells',
|
|
345
358
|
'terminal/list',
|
|
346
359
|
'terminal/retain',
|
|
360
|
+
// 0.2.2-rc.2: live Agent-terminal roster (WebTerminalInfo[]); read-only.
|
|
361
|
+
'terminal/watch',
|
|
347
362
|
])
|
|
348
363
|
|
|
349
364
|
/** Whether `endpoint` (a client-table method) is a remote WRITE: anything
|
|
@@ -1012,13 +1027,13 @@ function restoreRegisteredFields(fields: readonly SessionField[], args: unknown)
|
|
|
1012
1027
|
* `rewriteFrame` untouched.
|
|
1013
1028
|
* - everything else in the table (`session/prompt|cancel|rename|updateQueue|
|
|
1014
1029
|
* attachment`, `job/kill`, `skills/list`, `schedule/list`, `subagents/prompt`
|
|
1015
|
-
* (`{messageId}`), `subagents/interruptByParent` (`{accepted}`),
|
|
1030
|
+
* (`{messageId}`), `subagents/interruptByParent` / `subagents/interruptByUser` (`{accepted}`),
|
|
1016
1031
|
* `fileUploads/upload` (receipt + attachment ids, not session ids),
|
|
1017
1032
|
* `fileReferences/list` (`{path,kind}` rows), `goals/get|edit|pause|resume|
|
|
1018
1033
|
* clear` (GoalView/GoalRef ids are GOAL ids), `commands/list|execute`
|
|
1019
1034
|
* (`commandId` is a command id), `agentPresets/select` (a preset id
|
|
1020
|
-
* string), `workspaceFiles/list|read|readBytes|stat` (paths, versions,
|
|
1021
|
-
* text/bytes), `terminal/*` (terminal ids, attachment ids, controller ids
|
|
1035
|
+
* string), `workspaceFiles/list|read|readBytes|stat|search` (paths, versions,
|
|
1036
|
+
* text/bytes, search matches), `terminal/*` (terminal ids, attachment ids, controller ids
|
|
1022
1037
|
* — client-side identity, never session ids)) has NO
|
|
1023
1038
|
* session id in its result — verified. THREE of them still get a
|
|
1024
1039
|
* T52 rewrite of the model-catalog PROVIDER ids their results carry (never
|
package/src/invoke-binary.ts
CHANGED
|
@@ -18,12 +18,14 @@ export const BINARY_MARKER = '$zr-u8'
|
|
|
18
18
|
|
|
19
19
|
/** 结果里带二进制的方法(`namespace/method`)。请求侧参数不带二进制
|
|
20
20
|
* (readBytes 的参数是 scope/paths),故只有结果方向需要标记。
|
|
21
|
-
* `officeToPdf/render`(2.2.6
|
|
22
|
-
* JSON 通道会把 typed array
|
|
23
|
-
*
|
|
21
|
+
* `officeToPdf/render`(2.2.6)与其 0.2.2-rc.2 后继 `officeKit/render`
|
|
22
|
+
* 返回的 `data` 是整份 PDF 字节:同一个 JSON 通道会把 typed array 压扁,
|
|
23
|
+
* 客户端严格编解码当场拒绝——登记进来后服务端按同一标记写出、客户端按
|
|
24
|
+
* 同一标记还原。 */
|
|
24
25
|
export const INVOKE_BINARY_RESULT_METHODS: ReadonlySet<string> = new Set([
|
|
25
26
|
'workspaceFiles/readBytes',
|
|
26
27
|
'officeToPdf/render',
|
|
28
|
+
'officeKit/render',
|
|
27
29
|
])
|
|
28
30
|
|
|
29
31
|
/** 深度上限:与其余 walker(share-store 祖先链、会话引用扫描)一致。 */
|
package/src/relay-access.ts
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* half — every one takes `agent` as its first parameter, wire `agentId`,
|
|
61
61
|
* source `lookup: 'agent'`, wireTypeSymbol `SessionId` (the same shape
|
|
62
62
|
* T31 verified for fileUploads/fileReferences).
|
|
63
|
-
* - `workspaceFiles/list|changes|read|readBytes|stat` carry a TOP-LEVEL
|
|
63
|
+
* - `workspaceFiles/list|changes|read|readBytes|stat|search` carry a TOP-LEVEL
|
|
64
64
|
* `workspaceFileScopeId`: the host registers the `workspaceFileScope`
|
|
65
65
|
* lookup with wireTypeSymbol `SessionId` and resolves it by session —
|
|
66
66
|
* `sessions.get(sessionId).header.cwd` is only the BASE for relative
|
|
@@ -68,8 +68,9 @@
|
|
|
68
68
|
* not a containment: absolute paths anywhere the server process can read
|
|
69
69
|
* are served (see the registry note below). The field itself is
|
|
70
70
|
* share-checked like any session id.
|
|
71
|
-
* - `terminal/list
|
|
72
|
-
* `sessionId` (source `'json'`, not a
|
|
71
|
+
* - `terminal/list`, `terminal/retain` and (0.2.2-rc.2) `terminal/watch`
|
|
72
|
+
* take a plain TOP-LEVEL json `sessionId` (source `'json'`, not a
|
|
73
|
+
* lookup) — same check.
|
|
73
74
|
* - `sessionFeedback/record` carries `request.sessionId` — a plain B-type.
|
|
74
75
|
* - `sessionReferenceResolver/candidates` (@ mentions) answers with EVERY
|
|
75
76
|
* server session — title, cwd, and a ready-made `dsh-session:` mention
|
|
@@ -84,6 +85,12 @@
|
|
|
84
85
|
* does not descend from the claimed parent fails its own check, so the
|
|
85
86
|
* "shared parent + foreign child" decoy refuses here before DSH's own
|
|
86
87
|
* lineage validation is ever reached.
|
|
88
|
+
* - `subagents/interruptByUser` (0.2.2-rc.2, the rename of
|
|
89
|
+
* `interruptByParent`) takes only the top-level `childSessionId` — the
|
|
90
|
+
* user addresses the target Session directly, with no parent or mode.
|
|
91
|
+
* The child is still share-checked through the same `parentOf`
|
|
92
|
+
* inheritance. `interruptByParent` stays registered so a 0.2.2-rc.1 host
|
|
93
|
+
* (npm `latest`) keeps working.
|
|
87
94
|
*
|
|
88
95
|
* PROMPT TEXT REFERENCES ARE CHECKED TOO: a prompt whose text carries a
|
|
89
96
|
* canonical `dsh-session:<base64url(id)>` address makes DSH inject that
|
|
@@ -230,6 +237,10 @@ const RELAY_METHODS: Record<string, RelayMethod> = {
|
|
|
230
237
|
// refuses before DSH is ever asked
|
|
231
238
|
'subagents/prompt': { fields: ['request.parentSessionId', 'request.childSessionId'] },
|
|
232
239
|
'subagents/interruptByParent': { fields: ['parentSessionId', 'childSessionId'] },
|
|
240
|
+
// 0.2.2-rc.2 renamed interruptByParent → interruptByUser and dropped
|
|
241
|
+
// parentSessionId / mode. The child still borrows its ancestor's share
|
|
242
|
+
// through parentOf. interruptByParent stays for 0.2.2-rc.1 (npm latest).
|
|
243
|
+
'subagents/interruptByUser': { fields: ['childSessionId'] },
|
|
233
244
|
// attachments and @ references (T31) — the top-level agentId resolves
|
|
234
245
|
// through the agent registry keyed by session id, so it IS the session id
|
|
235
246
|
'fileUploads/upload': { fields: ['agentId'] },
|
|
@@ -265,6 +276,12 @@ const RELAY_METHODS: Record<string, RelayMethod> = {
|
|
|
265
276
|
'workspaceFiles/read': { fields: ['workspaceFileScopeId'] },
|
|
266
277
|
'workspaceFiles/readBytes': { fields: ['workspaceFileScopeId'] },
|
|
267
278
|
'workspaceFiles/stat': { fields: ['workspaceFileScopeId'] },
|
|
279
|
+
// 0.2.2-rc.2: official sidebar-files search bar
|
|
280
|
+
// (`remote.workspaceFiles.search(sessionId, request)`). Same
|
|
281
|
+
// workspaceFileScopeId ownership as list/stat; the Host walks the
|
|
282
|
+
// workspace and returns matches. A remote session without this entry
|
|
283
|
+
// gets `forbidden-method` on every search.
|
|
284
|
+
'workspaceFiles/search': { fields: ['workspaceFileScopeId'] },
|
|
268
285
|
// Office document preview (2.2.6, remote Word/Excel/PPT). The document
|
|
269
286
|
// preview's Office renderer does NOT convert on the client: it asks the
|
|
270
287
|
// HOST's LibreOffice to turn doc/docx/xls/xlsx/ppt/pptx into PDF
|
|
@@ -288,6 +305,13 @@ const RELAY_METHODS: Record<string, RelayMethod> = {
|
|
|
288
305
|
// INVOKE_BINARY_RESULT_METHODS — without that marker the JSON channel
|
|
289
306
|
// flattens the typed array and the client's strict codec rejects it.
|
|
290
307
|
'officeToPdf/render': { fields: ['workspaceFileScopeId'] },
|
|
308
|
+
// 0.2.2-rc.2 replaces officeToPdf with officeKit (same
|
|
309
|
+
// workspaceFileScopeId, same RenderedDocumentBytes result, same
|
|
310
|
+
// generation sibling left unregistered — RT dsh-office-kit
|
|
311
|
+
// lib/typert.remote-client.d.ts). The official document-preview plugin
|
|
312
|
+
// now calls `scope.remote.officeKit.render`. officeToPdf/render stays
|
|
313
|
+
// for 0.2.2-rc.1 (npm latest). Also in INVOKE_BINARY_RESULT_METHODS.
|
|
314
|
+
'officeKit/render': { fields: ['workspaceFileScopeId'] },
|
|
291
315
|
// terminal (T41a) — opens SERVER-side by explicit user request (SPEC story
|
|
292
316
|
// 45). What the client gets is a shell running AS THE SERVER USER, WITHOUT
|
|
293
317
|
// the agent sandbox or approval restrictions (RT dsh-api-terminal-controller
|
|
@@ -313,6 +337,11 @@ const RELAY_METHODS: Record<string, RelayMethod> = {
|
|
|
313
337
|
// the retain/list half locates by a plain top-level json sessionId
|
|
314
338
|
'terminal/list': { fields: ['sessionId'] },
|
|
315
339
|
'terminal/retain': { fields: ['sessionId'], stream: true },
|
|
340
|
+
// 0.2.2-rc.2: live roster stream the official sidebar uses to open and
|
|
341
|
+
// close Agent terminal tabs (`ctx.webTerminals.watch` →
|
|
342
|
+
// `remote.watch(sessionId)`). Same top-level sessionId as list/retain;
|
|
343
|
+
// frames are WebTerminalInfo[] (read-only).
|
|
344
|
+
'terminal/watch': { fields: ['sessionId'], stream: true },
|
|
316
345
|
// session/* — the global control stream and the unscoped list
|
|
317
346
|
'session/control': { fields: [], stream: true, streamFilter: 'control' },
|
|
318
347
|
'session/list': { fields: [], resultFilter: 'session-list' },
|
package/src/relay-client.ts
CHANGED
|
@@ -1338,7 +1338,19 @@ export function createRelayClient(options: CreateRelayClientOptions): RelayClien
|
|
|
1338
1338
|
})()
|
|
1339
1339
|
connectInFlight = attempt
|
|
1340
1340
|
const clear = (): void => {
|
|
1341
|
-
if (connectInFlight
|
|
1341
|
+
if (connectInFlight !== attempt) return
|
|
1342
|
+
connectInFlight = undefined
|
|
1343
|
+
// #56: a fired retry (or a manual / network-restored reconnect) that
|
|
1344
|
+
// JOINED this attempt started no attempt of its own — when this one
|
|
1345
|
+
// dies and a concurrent transport failure already moved the state to
|
|
1346
|
+
// 'offline' mid-flight, the catch's 'connecting' restore never runs,
|
|
1347
|
+
// so nobody re-arms the ladder. The settle is the one place every
|
|
1348
|
+
// joiner's failure funnels through: re-arm here or the client sits
|
|
1349
|
+
// offline forever with no timer and no UI way back (the settings page
|
|
1350
|
+
// used to key its 立即重连 buttons on nextRetryAt). scheduleRetry's
|
|
1351
|
+
// own guards (stopped, one timer at a time) keep this a no-op on every
|
|
1352
|
+
// path that already armed the next wait.
|
|
1353
|
+
if (state === 'offline') scheduleRetry()
|
|
1342
1354
|
}
|
|
1343
1355
|
attempt.then(clear, clear)
|
|
1344
1356
|
return attempt
|