ds-harness-remote 0.4.1 → 0.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.
Files changed (70) hide show
  1. package/README.md +35 -11
  2. package/cordis.patch.yml +3 -0
  3. package/dist/client-runtime.d.ts +28 -0
  4. package/dist/client-runtime.d.ts.map +1 -1
  5. package/dist/client-runtime.js +327 -5
  6. package/dist/client-runtime.js.map +1 -1
  7. package/dist/client-secure-transport.d.ts.map +1 -1
  8. package/dist/client-secure-transport.js +9 -2
  9. package/dist/client-secure-transport.js.map +1 -1
  10. package/dist/client.github.js +167 -48
  11. package/dist/client.js +167 -48
  12. package/dist/client.js.map +1 -1
  13. package/dist/codex/app-server.d.ts +71 -0
  14. package/dist/codex/app-server.d.ts.map +1 -0
  15. package/dist/codex/app-server.js +268 -0
  16. package/dist/codex/app-server.js.map +1 -0
  17. package/dist/codex/domain.d.ts +82 -0
  18. package/dist/codex/domain.d.ts.map +1 -0
  19. package/dist/codex/domain.js +893 -0
  20. package/dist/codex/domain.js.map +1 -0
  21. package/dist/codex/method-policy.d.ts +297 -0
  22. package/dist/codex/method-policy.d.ts.map +1 -0
  23. package/dist/codex/method-policy.js +103 -0
  24. package/dist/codex/method-policy.js.map +1 -0
  25. package/dist/codex/peer-bridge.d.ts +57 -0
  26. package/dist/codex/peer-bridge.d.ts.map +1 -0
  27. package/dist/codex/peer-bridge.js +300 -0
  28. package/dist/codex/peer-bridge.js.map +1 -0
  29. package/dist/codex/virtual-harness.d.ts +178 -0
  30. package/dist/codex/virtual-harness.d.ts.map +1 -0
  31. package/dist/codex/virtual-harness.js +2515 -0
  32. package/dist/codex/virtual-harness.js.map +1 -0
  33. package/dist/config.d.ts +10 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +12 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/control-runtime.d.ts +1 -0
  38. package/dist/control-runtime.d.ts.map +1 -1
  39. package/dist/control-runtime.js +22 -0
  40. package/dist/control-runtime.js.map +1 -1
  41. package/dist/harness-remote-bridge.d.ts +2 -2
  42. package/dist/harness-remote-bridge.d.ts.map +1 -1
  43. package/dist/harness-remote-bridge.js +2 -2
  44. package/dist/harness-remote-bridge.js.map +1 -1
  45. package/dist/index.d.ts +11 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8550 -3884
  48. package/dist/index.js.map +4 -4
  49. package/dist/remote-typert-gateway.d.ts +1 -1
  50. package/dist/remote-typert-gateway.d.ts.map +1 -1
  51. package/dist/remote-typert-gateway.js +9 -2
  52. package/dist/remote-typert-gateway.js.map +1 -1
  53. package/dist/rpc-router.d.ts +5 -2
  54. package/dist/rpc-router.d.ts.map +1 -1
  55. package/dist/rpc-router.js +30 -1
  56. package/dist/rpc-router.js.map +1 -1
  57. package/dist/server-connection.d.ts +1 -0
  58. package/dist/server-connection.d.ts.map +1 -1
  59. package/dist/server-connection.js +69 -22
  60. package/dist/server-connection.js.map +1 -1
  61. package/dist/service.d.ts +20 -0
  62. package/dist/service.d.ts.map +1 -1
  63. package/dist/service.js +58 -2
  64. package/dist/service.js.map +1 -1
  65. package/dist/typert-gateway-contract.d.ts +2 -2
  66. package/dist/typert-gateway-contract.d.ts.map +1 -1
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +19 -23
  70. package/public.d.ts +4 -0
@@ -1,4 +1,4 @@
1
- import { RemoteClientCore } from '@dsh-remote/client-core';
1
+ import { CodexRemoteClient, RemoteClientCore } from '@dsh-remote/client-core';
2
2
  import { AdaptiveTransport, stunOnlyIceServers, } from '@dsh-remote/webrtc';
3
3
  import { ApiProxySwitch } from './api-proxy-switch.js';
4
4
  import { ClientSecureTransport } from './client-secure-transport.js';
@@ -6,6 +6,7 @@ import { CONTROL_RPC_PREFIX } from './control-route.js';
6
6
  import { uuidV7 } from './ids.js';
7
7
  import { RemoteHarnessApiProxy } from './remote-api-proxy.js';
8
8
  import { RemoteTypertGateway } from './remote-typert-gateway.js';
9
+ import { CodexVirtualHarness, discoverCodexVirtualWorkspaces, } from './codex/virtual-harness.js';
9
10
  import { ServerApiError, } from './server-api.js';
10
11
  import { TypertGatewaySwitch } from './typert-gateway-switch.js';
11
12
  import { loadNodeRtcFactory } from './werift-rtc.js';
@@ -22,8 +23,10 @@ export class ClientModeRuntime {
22
23
  identity;
23
24
  connected;
24
25
  pendingWorkspaceSelection;
26
+ codexVirtual;
25
27
  proxySwitch;
26
28
  gatewaySwitch;
29
+ codexStreams = new Map();
27
30
  closed = false;
28
31
  constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
29
32
  this.config = config;
@@ -70,6 +73,7 @@ export class ClientModeRuntime {
70
73
  ...(this.pendingWorkspaceSelection === undefined
71
74
  ? {}
72
75
  : { workspaceSelection: { ...this.pendingWorkspaceSelection } }),
76
+ backend: this.codexVirtual === undefined ? 'harness' : 'codex',
73
77
  hostAuthorizationAvailable: this.host !== undefined,
74
78
  ...(this.host === undefined ? {} : { host: this.host.hostStatus() }),
75
79
  };
@@ -141,8 +145,10 @@ export class ClientModeRuntime {
141
145
  const previous = this.connected;
142
146
  this.connected = undefined;
143
147
  this.pendingWorkspaceSelection = undefined;
148
+ await this.closeCodexVirtual();
144
149
  this.proxySwitch?.selectLocal();
145
150
  this.gatewaySwitch.selectLocal();
151
+ await this.closeCodexStreams(previous?.client);
146
152
  await previous?.client.close().catch(() => undefined);
147
153
  await this.server.revokeCurrentDevice();
148
154
  this.identity = await this.identities.reset(this.config.deviceName);
@@ -161,11 +167,13 @@ export class ClientModeRuntime {
161
167
  }
162
168
  async setMode(mode, targetDeviceId, signal) {
163
169
  if (mode === 'local') {
170
+ await this.closeCodexVirtual();
164
171
  this.proxySwitch?.selectLocal();
165
172
  this.gatewaySwitch.selectLocal();
166
173
  const previous = this.connected;
167
174
  this.connected = undefined;
168
175
  this.pendingWorkspaceSelection = undefined;
176
+ await this.closeCodexStreams(previous?.client);
169
177
  await previous?.client.close().catch(() => undefined);
170
178
  this.logger.info('Harness target switched', { mode: 'local' });
171
179
  return this.status();
@@ -184,7 +192,9 @@ export class ClientModeRuntime {
184
192
  const previous = this.connected;
185
193
  this.connected = next;
186
194
  this.pendingWorkspaceSelection = undefined;
195
+ await this.closeCodexVirtual();
187
196
  this.selectRemoteTarget(next);
197
+ await this.closeCodexStreams(previous?.client);
188
198
  await previous?.client.close().catch(() => undefined);
189
199
  this.logger.info('Harness target switched', { mode: 'remote', targetDeviceId: shortId(next.target.deviceId) });
190
200
  return this.status();
@@ -242,15 +252,58 @@ export class ClientModeRuntime {
242
252
  });
243
253
  workspace = unwrapNativeResult(response);
244
254
  }
255
+ await this.closeCodexVirtual();
245
256
  this.selectRemoteTarget(remote);
246
257
  const workspaceId = workspaceRecordId(workspace.workspace);
247
258
  this.pendingWorkspaceSelection = { targetDeviceId: remote.target.deviceId, workspaceId };
248
259
  this.logger.info('Remote workspace opened', { targetDeviceId: shortId(remote.target.deviceId) });
249
260
  return { ...this.status(), workspace };
250
261
  }
262
+ async listCodexWorkspaces(targetDeviceId, signal) {
263
+ const remote = await this.ensureConnected(targetDeviceId, signal);
264
+ remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
265
+ if (!remote.features.codex) {
266
+ throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'The selected Host does not provide CodeX workspaces.');
267
+ }
268
+ return discoverCodexVirtualWorkspaces(new CodexRemoteClient(remote.client), signal);
269
+ }
270
+ async openCodexWorkspace(targetDeviceId, workspaceId, signal) {
271
+ const remote = await this.ensureConnected(targetDeviceId, signal);
272
+ remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
273
+ if (!remote.features.codex) {
274
+ throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'The selected Host does not provide CodeX workspaces.');
275
+ }
276
+ this.assertRemoteCompatible(remote);
277
+ const virtual = CodexVirtualHarness.remote(remote.client, {
278
+ deviceId: remote.target.deviceId,
279
+ name: remote.target.name,
280
+ });
281
+ let workspace;
282
+ try {
283
+ workspace = await virtual.selectWorkspace(workspaceId, signal);
284
+ }
285
+ catch {
286
+ await virtual.close();
287
+ throw new ClientModeError('WORKSPACE_NOT_FOUND', 'The selected CodeX workspace is no longer available.');
288
+ }
289
+ await this.closeCodexVirtual();
290
+ this.codexVirtual = virtual;
291
+ this.selectCodexTarget(virtual, remote);
292
+ const preferredSessionId = await virtual.preferredSessionId(signal);
293
+ this.pendingWorkspaceSelection = {
294
+ targetDeviceId: remote.target.deviceId,
295
+ workspaceId,
296
+ backend: 'codex',
297
+ ...(preferredSessionId === undefined ? {} : { sessionId: preferredSessionId }),
298
+ };
299
+ this.logger.info('CodeX virtual workspace opened', { targetDeviceId: shortId(remote.target.deviceId) });
300
+ return { ...this.status(), workspace };
301
+ }
251
302
  consumeWorkspaceSelection(selection) {
252
303
  const pending = this.pendingWorkspaceSelection;
253
- if (pending?.targetDeviceId === selection.targetDeviceId && pending.workspaceId === selection.workspaceId) {
304
+ if (pending?.targetDeviceId === selection.targetDeviceId
305
+ && pending.workspaceId === selection.workspaceId
306
+ && (pending.backend ?? 'harness') === (selection.backend ?? 'harness')) {
254
307
  this.pendingWorkspaceSelection = undefined;
255
308
  }
256
309
  return this.status();
@@ -262,6 +315,8 @@ export class ClientModeRuntime {
262
315
  this.proxySwitch?.selectLocal();
263
316
  this.gatewaySwitch.selectLocal();
264
317
  this.pendingWorkspaceSelection = undefined;
318
+ await this.closeCodexVirtual();
319
+ await this.closeCodexStreams(this.connected?.client);
265
320
  await this.connected?.client.close().catch(() => undefined);
266
321
  this.connected = undefined;
267
322
  this.proxySwitch?.restore();
@@ -277,6 +332,157 @@ export class ClientModeRuntime {
277
332
  }
278
333
  return remote.client.rpc('fileviewer.call', { endpoint, payload }, signal);
279
334
  }
335
+ activeRemote() {
336
+ return this.connected;
337
+ }
338
+ activeCodexRemote() {
339
+ const remote = this.activeRemote();
340
+ if (remote === undefined)
341
+ return undefined;
342
+ if (!remote.features.codex) {
343
+ return undefined;
344
+ }
345
+ return remote;
346
+ }
347
+ async openCodexStream(payload, signal) {
348
+ const remote = this.activeCodexRemote();
349
+ const value = record(payload);
350
+ if (typeof value.streamId !== 'string' || value.streamId.length === 0 || value.streamId.length > 128
351
+ || typeof value.threadId !== 'string' || value.threadId.length === 0) {
352
+ throw new ClientModeError('INVALID_MESSAGE', 'A Codex stream and thread are required.');
353
+ }
354
+ if (this.codexStreams.has(value.streamId))
355
+ throw new ClientModeError('REQUEST_CONFLICT', 'The Codex stream is already open.');
356
+ let wake = () => undefined;
357
+ const stream = {
358
+ target: remote === undefined ? { kind: 'local' } : { kind: 'remote', client: remote.client },
359
+ frames: [],
360
+ unsubscribe: () => undefined,
361
+ close: async () => {
362
+ if (remote === undefined) {
363
+ await this.host?.codexCloseStream?.({ streamId: value.streamId }).catch(() => undefined);
364
+ return;
365
+ }
366
+ await remote.client.rpc('codex.app.stream.close', { streamId: value.streamId }).catch(() => undefined);
367
+ },
368
+ wake: () => wake(),
369
+ };
370
+ if (remote === undefined) {
371
+ const host = this.requireLocalCodex();
372
+ this.codexStreams.set(value.streamId, stream);
373
+ try {
374
+ const result = await host.codexOpenStream({ streamId: value.streamId, threadId: value.threadId }, this.publishLocalCodexFrame, signal);
375
+ return result;
376
+ }
377
+ catch (error) {
378
+ this.codexStreams.delete(value.streamId);
379
+ stream.wake();
380
+ throw error;
381
+ }
382
+ }
383
+ stream.unsubscribe = remote.client.onEvent(event => {
384
+ if (event.event === 'codex.app.frame' && isRecord(event.data) && event.data.streamId === value.streamId) {
385
+ this.appendCodexFrame(stream, event.data);
386
+ }
387
+ if (event.event === 'codex.app.stream.closed' && isRecord(event.data) && event.data.streamId === value.streamId) {
388
+ stream.closed = typeof event.data.reason === 'string' ? event.data.reason : 'closed';
389
+ stream.wake();
390
+ }
391
+ });
392
+ try {
393
+ // Subscribe before opening the Host stream so the first App Server
394
+ // notification cannot race past the loopback listener.
395
+ await remote.client.rpc('codex.app.stream.open', { streamId: value.streamId, threadId: value.threadId }, signal);
396
+ }
397
+ catch (error) {
398
+ stream.unsubscribe();
399
+ throw error;
400
+ }
401
+ this.codexStreams.set(value.streamId, stream);
402
+ return { opened: true, streamId: value.streamId, threadId: value.threadId };
403
+ }
404
+ publishLocalCodexFrame = async (event, data) => {
405
+ const streamId = data.streamId;
406
+ const stream = this.codexStreams.get(streamId);
407
+ if (stream === undefined || stream.target.kind !== 'local')
408
+ return;
409
+ if (event === 'codex.app.frame') {
410
+ this.appendCodexFrame(stream, data);
411
+ return;
412
+ }
413
+ const closed = data;
414
+ stream.closed = typeof closed.reason === 'string' ? closed.reason : 'closed';
415
+ stream.wake();
416
+ };
417
+ appendCodexFrame(stream, data) {
418
+ if (!isRecord(data) || !isRecord(data.frame) || typeof data.frame.method !== 'string')
419
+ return;
420
+ if (stream.frames.length >= 256) {
421
+ stream.closed = 'overflow';
422
+ }
423
+ else {
424
+ stream.frames.push({ method: data.frame.method, params: data.frame.params });
425
+ }
426
+ stream.wake();
427
+ }
428
+ localCodexAvailable() {
429
+ return this.host?.codexStatus?.().available === true;
430
+ }
431
+ requireLocalCodex() {
432
+ if (!this.localCodexAvailable()
433
+ || this.host?.codexCall === undefined
434
+ || this.host.codexRespond === undefined
435
+ || this.host.codexOpenStream === undefined
436
+ || this.host.codexCloseStream === undefined) {
437
+ throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'Local CodeX is disabled or unavailable on this Host.');
438
+ }
439
+ return {
440
+ codexCall: this.host.codexCall.bind(this.host),
441
+ codexRespond: this.host.codexRespond.bind(this.host),
442
+ codexOpenStream: this.host.codexOpenStream.bind(this.host),
443
+ codexCloseStream: this.host.codexCloseStream.bind(this.host),
444
+ };
445
+ }
446
+ async nextCodexFrames(payload, signal) {
447
+ const value = record(payload);
448
+ if (typeof value.streamId !== 'string')
449
+ throw new ClientModeError('INVALID_MESSAGE', 'A Codex stream is required.');
450
+ const stream = this.codexStreams.get(value.streamId);
451
+ if (stream === undefined)
452
+ throw new ClientModeError('STREAM_NOT_FOUND', 'The Codex stream is not open.');
453
+ if (stream.frames.length === 0 && stream.closed === undefined)
454
+ await waitForCodexFrames(stream, signal);
455
+ const frames = stream.frames.splice(0, 100);
456
+ return {
457
+ streamId: value.streamId,
458
+ frames,
459
+ closed: stream.closed !== undefined,
460
+ ...(stream.closed === undefined ? {} : { reason: stream.closed }),
461
+ };
462
+ }
463
+ async closeCodexStream(payload) {
464
+ const value = record(payload);
465
+ if (typeof value.streamId !== 'string')
466
+ throw new ClientModeError('INVALID_MESSAGE', 'A Codex stream is required.');
467
+ const stream = this.codexStreams.get(value.streamId);
468
+ if (stream === undefined)
469
+ return { closed: false, streamId: value.streamId };
470
+ this.codexStreams.delete(value.streamId);
471
+ stream.unsubscribe();
472
+ stream.wake();
473
+ await stream.close();
474
+ return { closed: true, streamId: value.streamId };
475
+ }
476
+ async closeCodexStreams(client) {
477
+ const targets = [...this.codexStreams.entries()].filter(([, stream]) => (client === undefined || (stream.target.kind === 'remote' && stream.target.client === client)));
478
+ await Promise.all(targets.map(async ([streamId, stream]) => {
479
+ this.codexStreams.delete(streamId);
480
+ stream.unsubscribe();
481
+ stream.closed = 'peer-disconnected';
482
+ stream.wake();
483
+ await stream.close();
484
+ }));
485
+ }
280
486
  selectRemoteTarget(remote) {
281
487
  const target = { deviceId: remote.target.deviceId, name: remote.target.name };
282
488
  if (this.gatewaySwitch.supportsCarrier()) {
@@ -289,6 +495,20 @@ export class ClientModeRuntime {
289
495
  list: remote.features.commandList,
290
496
  }, target);
291
497
  }
498
+ selectCodexTarget(virtual, remote) {
499
+ const target = { deviceId: remote.target.deviceId, name: remote.target.name };
500
+ if (this.gatewaySwitch.supportsCarrier()) {
501
+ this.gatewaySwitch.selectRemote(virtual, undefined, target);
502
+ return;
503
+ }
504
+ this.proxySwitch.selectRemote(virtual.api, target);
505
+ this.gatewaySwitch.selectRemote(request => virtual.invoke(request), { execute: true, list: true }, target);
506
+ }
507
+ async closeCodexVirtual() {
508
+ const virtual = this.codexVirtual;
509
+ this.codexVirtual = undefined;
510
+ await virtual?.close();
511
+ }
292
512
  assertRemoteCompatible(remote) {
293
513
  const localRemoteGateway = this.gatewaySwitch.supportsCarrier();
294
514
  if ((localRemoteGateway && remote.features.remoteGateway)
@@ -337,8 +557,14 @@ export class ClientModeRuntime {
337
557
  targetDeviceId: shortId(target.deviceId),
338
558
  attempt,
339
559
  reason: diagnosticReason(error),
340
- ...webrtcDiagnosticsLogFields(diagnostics),
341
560
  });
561
+ if (diagnostics !== undefined) {
562
+ this.logger.debug('remote Harness WebRTC fallback diagnostics', {
563
+ targetDeviceId: shortId(target.deviceId),
564
+ attempt,
565
+ ...webrtcDiagnosticsLogFields(diagnostics),
566
+ });
567
+ }
342
568
  },
343
569
  });
344
570
  const attempts = this.config.forceRelay || rtcFactory === undefined
@@ -374,6 +600,7 @@ export class ClientModeRuntime {
374
600
  return;
375
601
  this.connected = undefined;
376
602
  this.pendingWorkspaceSelection = undefined;
603
+ void this.closeCodexVirtual();
377
604
  this.proxySwitch?.selectLocal();
378
605
  this.gatewaySwitch.selectLocal();
379
606
  void connectedClient.close().catch(() => undefined);
@@ -390,10 +617,21 @@ export class ClientModeRuntime {
390
617
  negotiatedCapabilities: connectionDetails.negotiatedCapabilities,
391
618
  webRtcEnabled: connectionDetails.webRtcEnabled,
392
619
  }),
393
- ...webrtcDiagnosticsLogFields(connectionDetails?.webRtc?.diagnostics),
394
620
  });
621
+ if (connectionDetails?.webRtc?.diagnostics !== undefined) {
622
+ this.logger.debug('remote Harness transport diagnostics', {
623
+ targetDeviceId: shortId(target.deviceId),
624
+ ...webrtcDiagnosticsLogFields(connectionDetails.webRtc.diagnostics),
625
+ });
626
+ }
395
627
  const features = await probeRemoteHostFeatures(connectedClient, serverDevice.clientVersion);
396
- return { client: connectedClient, target, transport: connectedTransport, features };
628
+ return {
629
+ client: connectedClient,
630
+ target,
631
+ transport: connectedTransport,
632
+ features,
633
+ ...(serverDevice.clientVersion === undefined ? {} : { clientVersion: serverDevice.clientVersion }),
634
+ };
397
635
  }
398
636
  catch (error) {
399
637
  await client?.close().catch(() => undefined);
@@ -449,6 +687,12 @@ export class ClientModeRuntime {
449
687
  throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
450
688
  return ok(await this.listRemoteWorkspaces(value.targetDeviceId, signal));
451
689
  }
690
+ if (endpoint === 'codex.workspaces.list') {
691
+ const value = record(payload);
692
+ if (typeof value.targetDeviceId !== 'string')
693
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
694
+ return ok(await this.listCodexWorkspaces(value.targetDeviceId, signal));
695
+ }
452
696
  if (endpoint === 'workspace.open') {
453
697
  const value = record(payload);
454
698
  if (typeof value.targetDeviceId !== 'string' || typeof value.path !== 'string') {
@@ -456,6 +700,13 @@ export class ClientModeRuntime {
456
700
  }
457
701
  return ok(await this.openRemoteWorkspace(value.targetDeviceId, value.path, signal));
458
702
  }
703
+ if (endpoint === 'codex.workspace.open') {
704
+ const value = record(payload);
705
+ if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
706
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host and CodeX Workspace are required.');
707
+ }
708
+ return ok(await this.openCodexWorkspace(value.targetDeviceId, value.workspaceId, signal));
709
+ }
459
710
  if (endpoint === 'workspace.selection.consume') {
460
711
  const value = record(payload);
461
712
  if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
@@ -464,6 +715,8 @@ export class ClientModeRuntime {
464
715
  return ok(this.consumeWorkspaceSelection({
465
716
  targetDeviceId: value.targetDeviceId,
466
717
  workspaceId: value.workspaceId,
718
+ ...(value.backend === 'codex' ? { backend: 'codex' } : {}),
719
+ ...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}),
467
720
  }));
468
721
  }
469
722
  if (endpoint === 'fileviewer.stat' || endpoint === 'fileviewer.readRange' || endpoint === 'fileviewer.list') {
@@ -472,6 +725,47 @@ export class ClientModeRuntime {
472
725
  : endpoint === 'fileviewer.readRange' ? 'readRange' : 'list';
473
726
  return ok(await this.callRemoteFileViewer(method, payload, signal));
474
727
  }
728
+ if (endpoint === 'codex.call') {
729
+ const value = record(payload);
730
+ if (typeof value.method !== 'string' || !('params' in value)) {
731
+ throw new ClientModeError('INVALID_MESSAGE', 'A Codex method and params are required.');
732
+ }
733
+ const remote = this.activeCodexRemote();
734
+ if (remote !== undefined)
735
+ return ok(await new CodexRemoteClient(remote.client).request(value.method, value.params, signal));
736
+ const host = this.requireLocalCodex();
737
+ return ok(await host.codexCall(value, signal));
738
+ }
739
+ if (endpoint === 'codex.probe') {
740
+ const local = this.localCodexAvailable();
741
+ let remoteSupported = false;
742
+ const remote = this.activeRemote();
743
+ if (remote !== undefined) {
744
+ try {
745
+ remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
746
+ remoteSupported = remote.features.codex;
747
+ }
748
+ catch (error) {
749
+ if (!local)
750
+ throw error;
751
+ }
752
+ }
753
+ return ok({ supported: local || remoteSupported, local, remote: remoteSupported });
754
+ }
755
+ if (endpoint === 'codex.respond') {
756
+ const value = record(payload);
757
+ const remote = this.activeCodexRemote();
758
+ if (remote !== undefined)
759
+ return ok(await remote.client.rpc('codex.app.respond', value, signal));
760
+ const host = this.requireLocalCodex();
761
+ return ok(await host.codexRespond(value, signal));
762
+ }
763
+ if (endpoint === 'codex.stream.open')
764
+ return ok(await this.openCodexStream(payload, signal));
765
+ if (endpoint === 'codex.stream.next')
766
+ return ok(await this.nextCodexFrames(payload, signal));
767
+ if (endpoint === 'codex.stream.close')
768
+ return ok(await this.closeCodexStream(payload));
475
769
  if (endpoint === 'host.account.login') {
476
770
  if (this.host === undefined)
477
771
  throw new ClientModeError('METHOD_NOT_ALLOWED', 'This plugin is not running as a Host.');
@@ -666,6 +960,7 @@ export function remoteHostFeatures(clientVersion) {
666
960
  fileViewer: isVersionAtLeast(clientVersion, REMOTE_FILE_VIEWER_MIN_VERSION),
667
961
  apiProxy: true,
668
962
  remoteGateway: false,
963
+ codex: false,
669
964
  };
670
965
  }
671
966
  export async function probeRemoteHostFeatures(client, clientVersion) {
@@ -694,8 +989,35 @@ export async function probeRemoteHostFeatures(client, clientVersion) {
694
989
  fileViewer: capabilities.has('fileviewer.read.v1'),
695
990
  apiProxy,
696
991
  remoteGateway,
992
+ codex: capabilities.has('codex.appserver.v1'),
697
993
  };
698
994
  }
995
+ async function waitForCodexFrames(stream, signal) {
996
+ if (signal?.aborted)
997
+ throw new ClientModeError('RPC_ABORTED', 'The Codex event poll was cancelled.');
998
+ await new Promise((resolve, reject) => {
999
+ const previousWake = stream.wake;
1000
+ const timer = setTimeout(done, 25_000);
1001
+ const onAbort = () => {
1002
+ cleanup();
1003
+ reject(new ClientModeError('RPC_ABORTED', 'The Codex event poll was cancelled.'));
1004
+ };
1005
+ function cleanup() {
1006
+ clearTimeout(timer);
1007
+ stream.wake = previousWake;
1008
+ signal?.removeEventListener('abort', onAbort);
1009
+ }
1010
+ function done() {
1011
+ cleanup();
1012
+ resolve();
1013
+ }
1014
+ stream.wake = () => {
1015
+ previousWake();
1016
+ done();
1017
+ };
1018
+ signal?.addEventListener('abort', onAbort, { once: true });
1019
+ });
1020
+ }
699
1021
  function isVersionAtLeast(value, minimum) {
700
1022
  const match = value?.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
701
1023
  if (match === undefined || match === null)