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.
@@ -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.nextRetryAt === undefined}
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 立即重连: the response carrying a nextRetryAt is the one
1419
- honest "the relay is offline with a retry armed" signal — the
1420
- probe word underneath may be unreachable or unexpected; the
1421
- route 409s anything else anyway (T43-fix). */}
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 || clientView.nextRetryAt === undefined || reconnectBusy}
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
@@ -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)返回的 `data` 是整份 PDF 字节:同一个
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 祖先链、会话引用扫描)一致。 */
@@ -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` and `terminal/retain` take a plain TOP-LEVEL json
72
- * `sessionId` (source `'json'`, not a lookup) — same check.
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' },
@@ -1338,7 +1338,19 @@ export function createRelayClient(options: CreateRelayClientOptions): RelayClien
1338
1338
  })()
1339
1339
  connectInFlight = attempt
1340
1340
  const clear = (): void => {
1341
- if (connectInFlight === attempt) connectInFlight = undefined
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