dsh-codex-subscription 2.5.8 → 3.0.0-beta.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/lib/index.js CHANGED
@@ -9,13 +9,14 @@ import { promisify } from "node:util";
9
9
  import { HttpsProxyAgent } from "https-proxy-agent";
10
10
  import WebSocket from "ws";
11
11
  import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
12
- import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
13
- import { constants, readFileSync } from "node:fs";
14
- import { dirname, join, resolve } from "node:path";
12
+ import { lstat, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
13
+ import { constants, readFileSync, statSync } from "node:fs";
14
+ import { dirname, isAbsolute, join, resolve } from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
17
  import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
18
18
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
19
+ import { hostname } from "node:os";
19
20
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
20
21
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
21
22
  import z from "@deepseek-ai/schemastery";
@@ -429,6 +430,11 @@ const PREFERENCE_FIELDS = Object.freeze({
429
430
  default: "session",
430
431
  error: "Invalid review model"
431
432
  },
433
+ remoteControl: {
434
+ choices: ["off", "on"],
435
+ default: "off",
436
+ error: "Invalid Remote Control setting"
437
+ },
432
438
  subagentBackend: {
433
439
  choices: ["dsh", "codex"],
434
440
  default: "dsh",
@@ -1110,6 +1116,10 @@ const RPC_ENDPOINTS = Object.freeze([
1110
1116
  "runtime/cancel",
1111
1117
  "storage/status",
1112
1118
  "storage/clear-forecast",
1119
+ "remote/status",
1120
+ "remote/enable",
1121
+ "remote/disable",
1122
+ "remote/pair",
1113
1123
  "reset-credit/inspect",
1114
1124
  "reset-credit/prepare",
1115
1125
  "reset-credit/consume",
@@ -2049,7 +2059,7 @@ function inheritedOriginalImageRef(session, assetId) {
2049
2059
  }
2050
2060
  //#endregion
2051
2061
  //#region src/version.js
2052
- const PACKAGE_VERSION = "2.5.8";
2062
+ const PACKAGE_VERSION = "3.0.0-beta.1";
2053
2063
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2054
2064
  //#endregion
2055
2065
  //#region src/codex-images.js
@@ -2677,6 +2687,1541 @@ function createSketchAgentTool(bridge, attachments, getSessionMessages = () => [
2677
2687
  });
2678
2688
  }
2679
2689
  //#endregion
2690
+ //#region src/remote-control-host.js
2691
+ const METHOD_NOT_FOUND = -32601;
2692
+ const INVALID_PARAMS = -32602;
2693
+ const INTERNAL_ERROR = -32603;
2694
+ const MAX_SEEN = 128;
2695
+ const QUIET = /* @__PURE__ */ new Set([
2696
+ "command/exec",
2697
+ "thread/attachment/list",
2698
+ "thread/list",
2699
+ "thread/queue/list",
2700
+ "threadSection/list",
2701
+ "config/read",
2702
+ "configRequirements/read"
2703
+ ]);
2704
+ var RpcError = class extends Error {
2705
+ constructor(code, message) {
2706
+ super(message);
2707
+ this.code = code;
2708
+ }
2709
+ };
2710
+ const invalidParams = (message) => new RpcError(INVALID_PARAMS, message);
2711
+ /**
2712
+ * One host per relay; each remote client gets its own connection.
2713
+ * @param {{ methods: Record<string, (params: any, context: { client: object, signal: AbortSignal, notify: (method: string, params?: object) => Promise<void> }) => any>, notifications?: Record<string, Function>, onConnection?: (client: object) => void, onClose?: (client: object) => void }} options
2714
+ */
2715
+ function createRemoteControlHost({ methods, notifications = {}, onConnection, onActive, onClose, debug = false }) {
2716
+ const seen = /* @__PURE__ */ new Map();
2717
+ const trace = [];
2718
+ const record = (entry) => {
2719
+ if (!debug) return;
2720
+ trace.push({
2721
+ at: Date.now(),
2722
+ ...entry
2723
+ });
2724
+ if (trace.length > 60) trace.shift();
2725
+ };
2726
+ const note = (kind, method) => {
2727
+ const key = `${kind}:${method}`;
2728
+ if (!seen.has(key) && seen.size >= MAX_SEEN) return;
2729
+ seen.set(key, (seen.get(key) ?? 0) + 1);
2730
+ };
2731
+ return Object.freeze({
2732
+ /** Methods remote clients called, for diagnostics; never params or content. */
2733
+ seen: () => Object.fromEntries(seen),
2734
+ /** Add a development trace entry from the bridge; ignored unless debug is on. */
2735
+ note: (entry) => record(entry),
2736
+ /** Development trace of recent calls; only filled when debug is on. */
2737
+ trace: () => [...trace],
2738
+ serve(client) {
2739
+ const aborts = /* @__PURE__ */ new Set();
2740
+ const pending = /* @__PURE__ */ new Map();
2741
+ let initialized = false;
2742
+ let active = false;
2743
+ const notify = async (method, params) => {
2744
+ await client.send(params === void 0 ? { method } : {
2745
+ method,
2746
+ params
2747
+ });
2748
+ };
2749
+ /** Ask the phone something (approvals); settles with its result or rejects. */
2750
+ const ask = (method, params, signal) => new Promise((resolve, reject) => {
2751
+ const id = `s${randomUUID()}`;
2752
+ const done = () => {
2753
+ pending.delete(id);
2754
+ signal?.removeEventListener("abort", onAbort);
2755
+ };
2756
+ const onAbort = () => {
2757
+ done();
2758
+ reject(new RpcError(-32800, "Request cancelled"));
2759
+ };
2760
+ if (signal?.aborted) return onAbort();
2761
+ signal?.addEventListener("abort", onAbort, { once: true });
2762
+ record({
2763
+ method: `ask:${method}`,
2764
+ params: JSON.stringify(params ?? null).slice(0, 400)
2765
+ });
2766
+ pending.set(id, {
2767
+ resolve: (value) => {
2768
+ record({
2769
+ method: `answer:${method}`,
2770
+ params: JSON.stringify(value ?? null).slice(0, 400)
2771
+ });
2772
+ done();
2773
+ resolve(value);
2774
+ },
2775
+ reject: (error) => {
2776
+ record({
2777
+ method: `answer:${method}`,
2778
+ error: String(error?.message ?? error).slice(0, 200)
2779
+ });
2780
+ done();
2781
+ reject(error);
2782
+ }
2783
+ });
2784
+ client.send({
2785
+ id,
2786
+ method,
2787
+ params
2788
+ }).catch((error) => {
2789
+ done();
2790
+ reject(error);
2791
+ });
2792
+ });
2793
+ notify.ask = ask;
2794
+ const context = {
2795
+ client,
2796
+ notify,
2797
+ ask
2798
+ };
2799
+ onConnection?.(client);
2800
+ const reply = async (id, result) => {
2801
+ await client.send({
2802
+ id,
2803
+ result
2804
+ });
2805
+ };
2806
+ const fail = async (id, error) => {
2807
+ const known = error instanceof RpcError;
2808
+ await client.send({
2809
+ id,
2810
+ error: {
2811
+ code: known ? error.code : INTERNAL_ERROR,
2812
+ message: known ? error.message : "Request failed"
2813
+ }
2814
+ });
2815
+ };
2816
+ return {
2817
+ receive(message) {
2818
+ if (message !== null && typeof message === "object" && typeof message.method !== "string" && message.id !== void 0) {
2819
+ const waiting = pending.get(message.id);
2820
+ if (waiting) if (message.error) waiting.reject(new RpcError(message.error.code ?? INTERNAL_ERROR, String(message.error.message ?? "Request failed")));
2821
+ else waiting.resolve(message.result);
2822
+ return;
2823
+ }
2824
+ if (message === null || typeof message !== "object" || typeof message.method !== "string") return;
2825
+ const { id, method, params } = message;
2826
+ if (id === void 0) {
2827
+ note("notify", method);
2828
+ if (method === "initialized") initialized = true;
2829
+ notifications[method]?.(params, {
2830
+ ...context,
2831
+ signal: new AbortController().signal
2832
+ });
2833
+ return;
2834
+ }
2835
+ note("request", method);
2836
+ if (!active && method !== "initialize") {
2837
+ active = true;
2838
+ onActive?.(client);
2839
+ }
2840
+ if (debug && !QUIET.has(method)) record({
2841
+ method: `request:${method}`,
2842
+ params: JSON.stringify(params ?? null).slice(0, 700)
2843
+ });
2844
+ const handler = methods[method];
2845
+ if (!handler) {
2846
+ record({
2847
+ method,
2848
+ missing: true,
2849
+ params: JSON.stringify(params ?? null).slice(0, 600)
2850
+ });
2851
+ fail(id, new RpcError(METHOD_NOT_FOUND, `Method not found: ${method}`)).catch(() => {});
2852
+ return;
2853
+ }
2854
+ if (method !== "initialize" && !initialized && method !== "ping") initialized = true;
2855
+ const controller = new AbortController();
2856
+ aborts.add(controller);
2857
+ Promise.resolve().then(() => handler(params, {
2858
+ ...context,
2859
+ signal: controller.signal
2860
+ })).then((result) => reply(id, result ?? {}), (error) => {
2861
+ if (!QUIET.has(method)) record({
2862
+ method,
2863
+ error: String(error?.message ?? error).slice(0, 300),
2864
+ params: JSON.stringify(params ?? null).slice(0, 600)
2865
+ });
2866
+ return fail(id, error);
2867
+ }).catch(() => {}).finally(() => aborts.delete(controller));
2868
+ },
2869
+ close() {
2870
+ for (const waiting of [...pending.values()]) waiting.reject(new RpcError(-32800, "Client disconnected"));
2871
+ for (const controller of aborts) controller.abort();
2872
+ aborts.clear();
2873
+ onClose?.(client, notify);
2874
+ }
2875
+ };
2876
+ }
2877
+ });
2878
+ }
2879
+ //#endregion
2880
+ //#region src/remote-control-relay.js
2881
+ const REMOTE_CONTROL_BASE_URL = "https://chatgpt.com/backend-api/";
2882
+ const PROTOCOL_VERSION = "3";
2883
+ const MAX_SEGMENT_BYTES = 150 * 1024;
2884
+ const TARGET_SEGMENT_BYTES = 100 * 1024;
2885
+ const MAX_MESSAGE_BYTES = 100 * 1024 * 1024;
2886
+ const MAX_CHUNKS = 1024;
2887
+ const INITIAL_RECONNECT_MS = 1e3;
2888
+ const CONFLICT_RENEW_MS = 15 * 6e4;
2889
+ const MAX_RECONNECT_MS = 3e4;
2890
+ const REQUEST_TIMEOUT_MS = 3e4;
2891
+ const REFRESH_MARGIN_MS = 5 * 6e4;
2892
+ var RemoteControlError = class extends Error {
2893
+ constructor(code, message) {
2894
+ super(message);
2895
+ this.name = "RemoteControlError";
2896
+ this.code = code;
2897
+ }
2898
+ };
2899
+ function remoteControlEndpoints(base = REMOTE_CONTROL_BASE_URL) {
2900
+ const root = new URL(base);
2901
+ const local = [
2902
+ "localhost",
2903
+ "127.0.0.1",
2904
+ "[::1]"
2905
+ ].includes(root.hostname);
2906
+ if (!(root.protocol === "https:" && (root.hostname === "chatgpt.com" || root.hostname.endsWith(".chatgpt.com"))) && !(local && root.protocol === "http:")) throw new RemoteControlError("unsupported-host", "Remote Control only talks to chatgpt.com");
2907
+ if (!root.pathname.endsWith("/")) root.pathname += "/";
2908
+ const at = (suffix) => new URL(suffix, root);
2909
+ const websocket = at("wham/remote/control/server");
2910
+ websocket.protocol = root.protocol === "https:" ? "wss:" : "ws:";
2911
+ return Object.freeze({
2912
+ enroll: at("wham/remote/control/server/enroll"),
2913
+ refresh: at("wham/remote/control/server/refresh"),
2914
+ pair: at("wham/remote/control/server/pair"),
2915
+ websocket
2916
+ });
2917
+ }
2918
+ const text = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
2919
+ function enrollment(value) {
2920
+ const environmentId = text(value?.environment_id);
2921
+ const serverId = text(value?.server_id);
2922
+ const token = text(value?.remote_control_token);
2923
+ const expiresAt = Date.parse(value?.expires_at);
2924
+ if (!environmentId || !serverId || !token || !Number.isFinite(expiresAt)) throw new RemoteControlError("bad-response", "Remote Control returned an unexpected enrollment");
2925
+ return {
2926
+ environmentId,
2927
+ serverId,
2928
+ token,
2929
+ expiresAt
2930
+ };
2931
+ }
2932
+ /**
2933
+ * @param {{
2934
+ * credentials: (signal?: AbortSignal) => Promise<{ access?: string, accountId?: string }>,
2935
+ * fetch?: typeof fetch,
2936
+ * WebSocket: new (url: string, options: object) => any,
2937
+ * installationId: string,
2938
+ * hostName: string,
2939
+ * userAgent: string,
2940
+ * serve: (client: { send: (message: object) => Promise<void>, close: () => void, id: string }) => { receive: (message: object) => void, close: () => void },
2941
+ * base?: string,
2942
+ * now?: () => number,
2943
+ * wait?: (ms: number, signal: AbortSignal) => Promise<void>,
2944
+ * }} options
2945
+ */
2946
+ function createRemoteControlRelay(options) {
2947
+ let installation = options.installationId;
2948
+ const endpoints = remoteControlEndpoints(options.base);
2949
+ const doFetch = options.fetch ?? globalThis.fetch;
2950
+ const now = options.now ?? Date.now;
2951
+ const wait = options.wait ?? ((ms, signal) => new Promise((resolve) => {
2952
+ const timer = setTimeout(resolve, ms);
2953
+ signal.addEventListener("abort", () => {
2954
+ clearTimeout(timer);
2955
+ resolve();
2956
+ }, { once: true });
2957
+ }));
2958
+ let enrolled;
2959
+ let stop;
2960
+ let running;
2961
+ let socket;
2962
+ const clients = /* @__PURE__ */ new Map();
2963
+ const assemblies = /* @__PURE__ */ new Map();
2964
+ const sequences = /* @__PURE__ */ new Map();
2965
+ const state = {
2966
+ status: "stopped",
2967
+ connectedAt: void 0,
2968
+ lastError: void 0,
2969
+ reconnects: 0
2970
+ };
2971
+ const request = async (url, init, signal) => {
2972
+ const response = await doFetch(url, {
2973
+ ...init,
2974
+ redirect: "error",
2975
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS)
2976
+ });
2977
+ if (!response.ok) throw new RemoteControlError(response.status === 401 || response.status === 403 ? "unauthorized" : "http-error", `Remote Control request failed (HTTP ${response.status})`);
2978
+ return response.json();
2979
+ };
2980
+ const ensureEnrollment = async (signal) => {
2981
+ if (enrolled && enrolled.expiresAt > now() + REFRESH_MARGIN_MS) return enrolled;
2982
+ const { access, accountId } = await options.credentials(signal);
2983
+ if (!access || !accountId) throw new RemoteControlError("not-signed-in", "ChatGPT subscription is not signed in");
2984
+ const headers = {
2985
+ authorization: `Bearer ${access}`,
2986
+ "chatgpt-account-id": accountId,
2987
+ "content-type": "application/json",
2988
+ "user-agent": options.userAgent,
2989
+ "x-codex-installation-id": installation
2990
+ };
2991
+ const refresh = enrolled !== void 0;
2992
+ const body = refresh ? {
2993
+ installation_id: installation,
2994
+ server_id: enrolled.serverId
2995
+ } : {
2996
+ app_server_version: options.userAgent.split("/").at(-1),
2997
+ arch: process.arch,
2998
+ installation_id: installation,
2999
+ name: options.hostName,
3000
+ os: process.platform
3001
+ };
3002
+ const next = enrollment(await request(refresh ? endpoints.refresh : endpoints.enroll, {
3003
+ method: "POST",
3004
+ headers,
3005
+ body: JSON.stringify(body)
3006
+ }, signal));
3007
+ if (refresh && next.serverId !== enrolled.serverId) throw new RemoteControlError("bad-response", "Remote Control returned a mismatched enrollment");
3008
+ enrolled = next;
3009
+ return enrolled;
3010
+ };
3011
+ /** Start a pairing: the code goes into a QR; the manual code can be typed in the app. */
3012
+ const pair = async (signal) => {
3013
+ const current = await ensureEnrollment(signal);
3014
+ const value = await request(endpoints.pair, {
3015
+ method: "POST",
3016
+ headers: {
3017
+ authorization: `Bearer ${current.token}`,
3018
+ "content-type": "application/json",
3019
+ "user-agent": options.userAgent
3020
+ },
3021
+ body: JSON.stringify({ manual_code: true })
3022
+ }, signal);
3023
+ const pairingCode = text(value?.pairing_code);
3024
+ const expiresAt = Date.parse(value?.expires_at);
3025
+ if (!pairingCode || value?.server_id !== current.serverId || !Number.isFinite(expiresAt)) throw new RemoteControlError("bad-response", "Remote Control returned an unexpected pairing");
3026
+ return {
3027
+ pairingCode,
3028
+ manualCode: text(value?.manual_pairing_code) ?? null,
3029
+ expiresAt
3030
+ };
3031
+ };
3032
+ const sendEnvelope = async (envelope) => {
3033
+ if (socket?.readyState !== 1) throw new RemoteControlError("not-connected", "Remote Control is not connected");
3034
+ await new Promise((resolve, reject) => socket.send(JSON.stringify(envelope), (error) => error ? reject(error) : resolve()));
3035
+ };
3036
+ const sendServerMessage = async (clientId, streamId, message) => {
3037
+ const key = `${clientId}\u0000${streamId}`;
3038
+ const sequence = sequences.get(key) ?? 1;
3039
+ sequences.set(key, sequence + 1);
3040
+ const envelope = {
3041
+ client_id: clientId,
3042
+ message,
3043
+ seq_id: sequence,
3044
+ stream_id: streamId,
3045
+ type: "server_message"
3046
+ };
3047
+ const encoded = JSON.stringify(envelope);
3048
+ if (Buffer.byteLength(encoded) <= MAX_SEGMENT_BYTES) return sendEnvelope(envelope);
3049
+ const bytes = Buffer.from(JSON.stringify(message));
3050
+ if (bytes.byteLength > MAX_MESSAGE_BYTES) throw new RemoteControlError("too-large", "Remote Control message is too large");
3051
+ const chunks = [];
3052
+ for (let offset = 0; offset < bytes.byteLength; offset += TARGET_SEGMENT_BYTES) chunks.push(bytes.subarray(offset, offset + TARGET_SEGMENT_BYTES).toString("base64"));
3053
+ await Promise.all(chunks.map((chunk, segmentId) => sendEnvelope({
3054
+ client_id: clientId,
3055
+ message_chunk_base64: chunk,
3056
+ message_size_bytes: bytes.byteLength,
3057
+ segment_count: chunks.length,
3058
+ segment_id: segmentId,
3059
+ seq_id: sequence,
3060
+ stream_id: streamId,
3061
+ type: "server_message_chunk"
3062
+ })));
3063
+ };
3064
+ const closeClient = (clientId, streamId) => {
3065
+ for (const [key, client] of clients) if (key.startsWith(`${clientId}\u0000`) && (streamId === void 0 || key === `${clientId}\u0000${streamId}`)) {
3066
+ clients.delete(key);
3067
+ sequences.delete(key);
3068
+ client.close();
3069
+ }
3070
+ };
3071
+ const deliver = async (envelope) => {
3072
+ const streamId = envelope.stream_id ?? randomUUID();
3073
+ const key = `${envelope.client_id}\u0000${streamId}`;
3074
+ let client = clients.get(key);
3075
+ if (!client) {
3076
+ let closed = false;
3077
+ const connection = options.serve({
3078
+ id: `${envelope.client_id}/${streamId}`,
3079
+ send: async (message) => {
3080
+ if (!closed) await sendServerMessage(envelope.client_id, streamId, message);
3081
+ },
3082
+ close: () => {
3083
+ closed = true;
3084
+ }
3085
+ });
3086
+ client = {
3087
+ receive: connection.receive,
3088
+ close: () => {
3089
+ closed = true;
3090
+ connection.close();
3091
+ }
3092
+ };
3093
+ clients.set(key, client);
3094
+ }
3095
+ client.receive(envelope.message);
3096
+ if (Number.isInteger(envelope.seq_id)) await sendEnvelope({
3097
+ client_id: envelope.client_id,
3098
+ seq_id: envelope.seq_id,
3099
+ stream_id: streamId,
3100
+ type: "ack"
3101
+ });
3102
+ };
3103
+ const receiveChunk = (envelope) => {
3104
+ const streamId = envelope.stream_id ?? randomUUID();
3105
+ const key = `${envelope.client_id}\u0000${streamId}\u0000${envelope.seq_id ?? 0}`;
3106
+ const count = envelope.segment_count;
3107
+ if (!Number.isInteger(count) || count < 1 || count > MAX_CHUNKS || !Number.isInteger(envelope.segment_id) || envelope.segment_id < 0 || envelope.segment_id >= count || !Number.isInteger(envelope.message_size_bytes) || envelope.message_size_bytes < 1 || envelope.message_size_bytes > MAX_MESSAGE_BYTES) {
3108
+ assemblies.delete(key);
3109
+ return;
3110
+ }
3111
+ const assembly = assemblies.get(key) ?? {
3112
+ chunks: new Array(count),
3113
+ size: envelope.message_size_bytes
3114
+ };
3115
+ if (assembly.chunks.length !== count || assembly.size !== envelope.message_size_bytes) {
3116
+ assemblies.delete(key);
3117
+ return;
3118
+ }
3119
+ assembly.chunks[envelope.segment_id] = envelope.message_chunk_base64;
3120
+ assemblies.set(key, assembly);
3121
+ if (assembly.chunks.some((chunk) => typeof chunk !== "string")) return void 0;
3122
+ assemblies.delete(key);
3123
+ const bytes = Buffer.concat(assembly.chunks.map((chunk) => Buffer.from(chunk, "base64")));
3124
+ if (bytes.byteLength !== assembly.size) return void 0;
3125
+ return {
3126
+ client_id: envelope.client_id,
3127
+ message: JSON.parse(bytes.toString("utf8")),
3128
+ seq_id: envelope.seq_id,
3129
+ stream_id: streamId,
3130
+ type: "client_message"
3131
+ };
3132
+ };
3133
+ const receive = async (raw) => {
3134
+ const envelope = JSON.parse(raw);
3135
+ if (typeof envelope?.client_id !== "string" || envelope.client_id.length === 0) return;
3136
+ if (envelope.type === "client_message") await deliver(envelope);
3137
+ else if (envelope.type === "client_message_chunk") {
3138
+ const message = receiveChunk(envelope);
3139
+ if (message) await deliver(message);
3140
+ } else if (envelope.type === "client_closed") closeClient(envelope.client_id, envelope.stream_id);
3141
+ else if (envelope.type === "ping") await sendEnvelope({
3142
+ client_id: envelope.client_id,
3143
+ seq_id: envelope.seq_id ?? 0,
3144
+ status: "active",
3145
+ stream_id: envelope.stream_id ?? randomUUID(),
3146
+ type: "pong"
3147
+ });
3148
+ };
3149
+ const connectOnce = async (signal) => {
3150
+ const current = await ensureEnrollment(signal);
3151
+ await new Promise((resolve, reject) => {
3152
+ const ws = new options.WebSocket(endpoints.websocket.toString(), { headers: {
3153
+ authorization: `Bearer ${current.token}`,
3154
+ "user-agent": options.userAgent,
3155
+ "x-codex-installation-id": installation,
3156
+ "x-codex-name": Buffer.from(options.hostName).toString("base64"),
3157
+ "x-codex-protocol-version": PROTOCOL_VERSION,
3158
+ "x-codex-server-id": current.serverId
3159
+ } });
3160
+ socket = ws;
3161
+ let queue = Promise.resolve();
3162
+ const onAbort = () => ws.close();
3163
+ signal.addEventListener("abort", onAbort, { once: true });
3164
+ ws.on("open", () => {
3165
+ state.status = "connected";
3166
+ state.connectedAt = now();
3167
+ state.lastError = void 0;
3168
+ });
3169
+ ws.on("message", (data) => {
3170
+ queue = queue.then(() => receive(String(data))).catch((error) => {
3171
+ state.lastError = error?.code ?? "protocol";
3172
+ });
3173
+ });
3174
+ ws.on("error", (error) => reject(new RemoteControlError("socket", error?.message ?? "socket error")));
3175
+ ws.on("close", () => {
3176
+ signal.removeEventListener("abort", onAbort);
3177
+ resolve();
3178
+ });
3179
+ });
3180
+ for (const [, client] of clients) client.close();
3181
+ clients.clear();
3182
+ sequences.clear();
3183
+ assemblies.clear();
3184
+ socket = void 0;
3185
+ };
3186
+ const loop = async (signal) => {
3187
+ let delay = INITIAL_RECONNECT_MS;
3188
+ let conflicts = 0;
3189
+ let conflictSince = 0;
3190
+ while (!signal.aborted) {
3191
+ state.status = "connecting";
3192
+ try {
3193
+ await connectOnce(signal);
3194
+ delay = INITIAL_RECONNECT_MS;
3195
+ } catch (error) {
3196
+ state.lastError = error?.code ?? "network";
3197
+ if (options.debug) state.detail = String(error?.message ?? error).slice(0, 160);
3198
+ state.status = "error";
3199
+ conflicts = /409/u.test(String(error?.message)) ? conflicts + 1 : 0;
3200
+ if (conflicts === 1) conflictSince = now();
3201
+ if (error?.code === "unauthorized" || conflicts >= 3) enrolled = void 0;
3202
+ if (conflicts >= 3 && now() - conflictSince >= (options.conflictRenewMs ?? CONFLICT_RENEW_MS) && options.renewInstallation) {
3203
+ installation = await options.renewInstallation();
3204
+ enrolled = void 0;
3205
+ conflicts = 0;
3206
+ }
3207
+ if (error?.code === "not-signed-in") {
3208
+ state.status = "stopped";
3209
+ return;
3210
+ }
3211
+ }
3212
+ if (signal.aborted) break;
3213
+ state.reconnects += 1;
3214
+ await wait(delay, signal);
3215
+ delay = Math.min(delay * 2, MAX_RECONNECT_MS);
3216
+ }
3217
+ };
3218
+ return Object.freeze({
3219
+ pair,
3220
+ status: () => ({
3221
+ ...state,
3222
+ enrolled: enrolled !== void 0,
3223
+ server: enrolled?.serverId.slice(-6),
3224
+ clients: clients.size
3225
+ }),
3226
+ start() {
3227
+ if (running) return running;
3228
+ stop = new AbortController();
3229
+ state.status = "connecting";
3230
+ running = loop(stop.signal).finally(() => {
3231
+ running = void 0;
3232
+ if (state.status !== "error") state.status = "stopped";
3233
+ });
3234
+ return running;
3235
+ },
3236
+ async stop() {
3237
+ stop?.abort();
3238
+ socket?.close();
3239
+ await running;
3240
+ state.status = "stopped";
3241
+ }
3242
+ });
3243
+ }
3244
+ //#endregion
3245
+ //#region src/remote-control.js
3246
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
3247
+ /** The installation id only tells ChatGPT "this is the same machine"; it is not a secret. */
3248
+ async function installationId(filename, fresh = false) {
3249
+ try {
3250
+ if (fresh) throw Object.assign(/* @__PURE__ */ new Error("rotate"), { code: "ENOENT" });
3251
+ const value = JSON.parse(await readFile(filename, "utf8"))?.installationId;
3252
+ if (typeof value === "string" && UUID.test(value)) return value;
3253
+ } catch (error) {
3254
+ if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) throw error;
3255
+ }
3256
+ const created = randomUUID();
3257
+ await mkdir(dirname(filename), { recursive: true });
3258
+ const temporary = `${filename}.${process.pid}.tmp`;
3259
+ await writeFile(temporary, `${JSON.stringify({ installationId: created })}\n`, { mode: 384 });
3260
+ await rename(temporary, filename);
3261
+ return created;
3262
+ }
3263
+ /**
3264
+ * Optional ChatGPT Remote Control host. Nothing is contacted until enable().
3265
+ * @param {{ credentials: Function, WebSocket: Function, fetch?: Function, stateFile: string, hostName: string, userAgent: string, methods: object, notifications?: object, onConnection?: Function, onClose?: Function, base?: string }} options
3266
+ */
3267
+ function createRemoteControl(options) {
3268
+ const phones = /* @__PURE__ */ new Set();
3269
+ const host = createRemoteControlHost({
3270
+ methods: options.methods,
3271
+ notifications: options.notifications,
3272
+ onConnection: options.onConnection,
3273
+ onActive: (client) => {
3274
+ phones.add(client.id);
3275
+ },
3276
+ onClose: (client, notify) => {
3277
+ phones.delete(client.id);
3278
+ options.onClose?.(client, notify);
3279
+ },
3280
+ debug: process.env.DSH_CODEX_REMOTE_DEBUG === "1"
3281
+ });
3282
+ let relay;
3283
+ let starting;
3284
+ const ensureRelay = async () => {
3285
+ relay ??= createRemoteControlRelay({
3286
+ debug: process.env.DSH_CODEX_REMOTE_DEBUG === "1",
3287
+ credentials: options.credentials,
3288
+ fetch: options.fetch,
3289
+ WebSocket: options.WebSocket,
3290
+ installationId: await installationId(options.stateFile),
3291
+ renewInstallation: () => installationId(options.stateFile, true),
3292
+ hostName: options.hostName,
3293
+ userAgent: options.userAgent,
3294
+ base: options.base,
3295
+ serve: (client) => host.serve(client)
3296
+ });
3297
+ return relay;
3298
+ };
3299
+ return Object.freeze({
3300
+ host,
3301
+ async enable() {
3302
+ starting ??= ensureRelay().then((value) => {
3303
+ value.start();
3304
+ return value;
3305
+ }).finally(() => {
3306
+ starting = void 0;
3307
+ });
3308
+ await starting;
3309
+ return this.status();
3310
+ },
3311
+ async disable() {
3312
+ if (relay) await relay.stop();
3313
+ return this.status();
3314
+ },
3315
+ async pair(signal) {
3316
+ return (await ensureRelay()).pair(signal);
3317
+ },
3318
+ status() {
3319
+ return {
3320
+ ...relay?.status() ?? {
3321
+ status: "stopped",
3322
+ enrolled: false,
3323
+ clients: 0,
3324
+ reconnects: 0
3325
+ },
3326
+ phones: phones.size,
3327
+ methods: host.seen(),
3328
+ trace: host.trace()
3329
+ };
3330
+ }
3331
+ });
3332
+ }
3333
+ //#endregion
3334
+ //#region src/remote-control-dsh.js
3335
+ /** A folder the phone named, only when it is an existing absolute folder on this machine. */
3336
+ function localFolder(value) {
3337
+ if (typeof value !== "string" || value === "" || !isAbsolute(value)) return void 0;
3338
+ if (process.platform === "win32" && !/^[A-Za-z]:[\\/]/u.test(value)) return void 0;
3339
+ try {
3340
+ return statSync(value).isDirectory() ? value : void 0;
3341
+ } catch {
3342
+ return;
3343
+ }
3344
+ }
3345
+ const seconds = (value) => Math.floor((Number.isFinite(value) ? value : Date.now()) / 1e3);
3346
+ const emptyTurn = () => ({
3347
+ id: randomUUID(),
3348
+ items: [],
3349
+ itemsView: "full",
3350
+ status: "inProgress",
3351
+ error: null,
3352
+ startedAt: seconds(Date.now()),
3353
+ completedAt: null,
3354
+ durationMs: null
3355
+ });
3356
+ const textOf = (content) => (content ?? []).map((block) => block?.type === "text" ? block.text : "").filter(Boolean).join("\n");
3357
+ const inputText = (input) => (Array.isArray(input) ? input : []).map((part) => part?.type === "text" ? part.text : "").filter(Boolean).join("\n");
3358
+ const notFound = (id) => new RpcError(-32602, `Thread not found: ${String(id).slice(0, 80)}`);
3359
+ const modelKey = (selection) => `${encodeURIComponent(selection.provider)}/${encodeURIComponent(selection.model)}`;
3360
+ function parseModelKey(value) {
3361
+ const at = typeof value === "string" ? value.indexOf("/") : -1;
3362
+ if (at <= 0) return void 0;
3363
+ try {
3364
+ return {
3365
+ provider: decodeURIComponent(value.slice(0, at)),
3366
+ model: decodeURIComponent(value.slice(at + 1))
3367
+ };
3368
+ } catch {
3369
+ return;
3370
+ }
3371
+ }
3372
+ function parseArguments(value) {
3373
+ if (typeof value !== "string") return value ?? null;
3374
+ try {
3375
+ return JSON.parse(value);
3376
+ } catch {
3377
+ return { raw: value };
3378
+ }
3379
+ }
3380
+ /** What the person typed: DSH appends its own system reminders (skills, context) to user messages. */
3381
+ const visibleText = (content) => textOf(content).replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gu, "").trim();
3382
+ /** Text of a user-role message the person wrote; DSH's own injected ones (runtime context, notices) carry another source kind. */
3383
+ const typedText = (message) => message.role === "user" && (message.source === void 0 || message.source?.kind === "user") ? visibleText(message.content) : "";
3384
+ /** One user message starts a turn; assistant blocks and tool results fill it. */
3385
+ function projectTurns(messages, at = Date.now()) {
3386
+ const turns = [];
3387
+ let turn;
3388
+ const calls = /* @__PURE__ */ new Map();
3389
+ const close = () => {
3390
+ if (turn && turn.items.length > 0) {
3391
+ turn.status = "completed";
3392
+ turn.completedAt = turn.startedAt;
3393
+ turns.push(turn);
3394
+ }
3395
+ turn = void 0;
3396
+ };
3397
+ for (const message of messages) if (message.role === "user") {
3398
+ const text = typedText(message);
3399
+ if (text === "") continue;
3400
+ close();
3401
+ turn = {
3402
+ ...emptyTurn(),
3403
+ id: message.id,
3404
+ startedAt: seconds(at)
3405
+ };
3406
+ turn.items.push({
3407
+ type: "userMessage",
3408
+ id: message.id,
3409
+ clientId: null,
3410
+ content: [{
3411
+ type: "text",
3412
+ text,
3413
+ text_elements: []
3414
+ }]
3415
+ });
3416
+ } else if (message.role === "assistant") {
3417
+ turn ??= {
3418
+ ...emptyTurn(),
3419
+ startedAt: seconds(at)
3420
+ };
3421
+ for (const [index, block] of message.content.entries()) {
3422
+ const id = `${message.id}:${index}`;
3423
+ if (block.type === "text") turn.items.push({
3424
+ type: "agentMessage",
3425
+ id,
3426
+ text: block.text,
3427
+ phase: null,
3428
+ memoryCitation: null,
3429
+ delivery: null
3430
+ });
3431
+ else if (block.type === "reasoning") turn.items.push({
3432
+ type: "reasoning",
3433
+ id,
3434
+ summary: [],
3435
+ content: [block.text]
3436
+ });
3437
+ else if (block.type === "tool-call") {
3438
+ const item = {
3439
+ type: "dynamicToolCall",
3440
+ id: block.id,
3441
+ namespace: null,
3442
+ tool: block.name,
3443
+ arguments: parseArguments(block.arguments),
3444
+ status: "inProgress",
3445
+ contentItems: null,
3446
+ success: null,
3447
+ durationMs: null
3448
+ };
3449
+ calls.set(block.id, item);
3450
+ turn.items.push(item);
3451
+ }
3452
+ }
3453
+ } else if (message.role === "tool") {
3454
+ const item = calls.get(message.toolCallId);
3455
+ if (item) {
3456
+ item.contentItems = [{
3457
+ type: "inputText",
3458
+ text: textOf(message.content)
3459
+ }];
3460
+ item.status = message.isError ? "failed" : "completed";
3461
+ item.success = !message.isError;
3462
+ }
3463
+ }
3464
+ close();
3465
+ return turns;
3466
+ }
3467
+ function thread({ id, title, preview, cwd, createdAt, updatedAt, running, turns = [] }) {
3468
+ return {
3469
+ id,
3470
+ sessionId: id,
3471
+ forkedFromId: null,
3472
+ parentThreadId: null,
3473
+ preview: preview ?? title ?? "",
3474
+ ephemeral: false,
3475
+ modelProvider: "dsh",
3476
+ createdAt: seconds(createdAt),
3477
+ updatedAt: seconds(updatedAt ?? createdAt),
3478
+ recencyAt: seconds(updatedAt ?? createdAt),
3479
+ status: running ? {
3480
+ type: "active",
3481
+ activeFlags: []
3482
+ } : { type: "idle" },
3483
+ path: null,
3484
+ cwd: cwd ?? "",
3485
+ cliVersion: "dsh",
3486
+ source: { custom: "dsh" },
3487
+ threadSource: "dsh",
3488
+ agentNickname: null,
3489
+ agentRole: null,
3490
+ gitInfo: null,
3491
+ name: title ?? null,
3492
+ turns,
3493
+ historyMode: "legacy",
3494
+ section: null,
3495
+ sectionEnteredAt: null,
3496
+ projectId: null,
3497
+ canAcceptDirectInput: true,
3498
+ extra: null
3499
+ };
3500
+ }
3501
+ /**
3502
+ * @param {{ controller: () => any, agents: () => any, permissions?: () => any, userAgent: string }} options
3503
+ */
3504
+ const PHONE_RETURN_MS = 12e4;
3505
+ function createDshRemoteControl({ controller, agents, permissions = () => void 0, workspaces = () => void 0, trace = () => {}, phoneReturnMs = PHONE_RETURN_MS, userAgent }) {
3506
+ const subscribers = /* @__PURE__ */ new Map();
3507
+ const active = /* @__PURE__ */ new Map();
3508
+ const queues = /* @__PURE__ */ new Map();
3509
+ let catalog;
3510
+ const service = () => {
3511
+ const value = controller();
3512
+ if (!value) throw new RpcError(-32603, "DSH session service is unavailable in this profile");
3513
+ return value;
3514
+ };
3515
+ const arrivals = /* @__PURE__ */ new Map();
3516
+ const subscribe = (threadId, notify) => {
3517
+ const set = subscribers.get(threadId) ?? /* @__PURE__ */ new Set();
3518
+ set.add(notify);
3519
+ subscribers.set(threadId, set);
3520
+ for (const wake of arrivals.get(threadId) ?? []) wake();
3521
+ };
3522
+ /** Wait until a phone opens the thread again, the signal aborts, or the time runs out. */
3523
+ const phoneReturns = (threadId, ms, signal) => new Promise((resolve) => {
3524
+ const set = arrivals.get(threadId) ?? /* @__PURE__ */ new Set();
3525
+ arrivals.set(threadId, set);
3526
+ const done = (value) => {
3527
+ clearTimeout(timer);
3528
+ set.delete(wake);
3529
+ signal?.removeEventListener("abort", stop);
3530
+ if (set.size === 0) arrivals.delete(threadId);
3531
+ resolve(value);
3532
+ };
3533
+ const wake = () => done(true);
3534
+ const stop = () => done(false);
3535
+ const timer = setTimeout(stop, ms);
3536
+ set.add(wake);
3537
+ signal?.addEventListener("abort", stop, { once: true });
3538
+ });
3539
+ const emit = (threadId, method, params) => {
3540
+ for (const notify of subscribers.get(threadId) ?? []) notify(method, params).catch(() => {});
3541
+ };
3542
+ const agentFor = async (id) => {
3543
+ const result = await service().resolveAgent(id);
3544
+ if (!result?.agent) throw notFound(id);
3545
+ return result.agent;
3546
+ };
3547
+ const loadCatalog = async () => {
3548
+ catalog = await service().modelCatalog();
3549
+ return catalog;
3550
+ };
3551
+ const selection = (agent) => agent?.options?.provider && agent?.options?.model ? {
3552
+ provider: agent.options.provider,
3553
+ model: agent.options.model,
3554
+ reasoningEffort: agent.options.reasoningEffort
3555
+ } : catalog?.default;
3556
+ const settings = (agent, cwd) => {
3557
+ const current = selection(agent);
3558
+ const preset = agent?.session ? presetOf(agent.session) : void 0;
3559
+ return {
3560
+ activePermissionProfile: null,
3561
+ approvalPolicy: preset === "danger-full-access" ? "never" : "on-request",
3562
+ approvalsReviewer: "user",
3563
+ cwd: cwd ?? "",
3564
+ initialTurnsPage: null,
3565
+ instructionSources: [],
3566
+ itemsBackwardsCursor: null,
3567
+ model: current ? modelKey(current) : "dsh/default",
3568
+ modelProvider: current?.provider ?? "dsh",
3569
+ multiAgentMode: "explicitRequestOnly",
3570
+ reasoningEffort: current?.reasoningEffort ?? null,
3571
+ runtimeWorkspaceRoots: cwd ? [cwd] : [],
3572
+ sandbox: sandboxFor(preset, cwd),
3573
+ serviceTier: null,
3574
+ turnsBackwardsCursor: null
3575
+ };
3576
+ };
3577
+ const openThread = async (id, notify) => {
3578
+ const agent = await agentFor(id);
3579
+ if (notify) subscribe(id, notify);
3580
+ await (catalog ? void 0 : loadCatalog().catch(() => void 0));
3581
+ const session = agent.session;
3582
+ const messages = session.deriveMessages();
3583
+ const first = messages.find((message) => typedText(message) !== "");
3584
+ return {
3585
+ agent,
3586
+ thread: thread({
3587
+ id,
3588
+ cwd: session.meta?.cwd,
3589
+ createdAt: session.meta?.createdAt,
3590
+ running: active.has(id),
3591
+ preview: first ? visibleText(first.content).slice(0, 200) : "",
3592
+ turns: projectTurns(messages, session.meta?.createdAt)
3593
+ })
3594
+ };
3595
+ };
3596
+ /** Apply a model or effort the phone picked; DSH validates it against its own catalog. */
3597
+ const applySelection = async (agent, sessionId, params) => {
3598
+ const picked = parseModelKey(params?.model);
3599
+ const effort = typeof params?.effort === "string" ? params.effort : void 0;
3600
+ if (!picked && effort === void 0) return;
3601
+ const current = selection(agent) ?? {};
3602
+ const next = {
3603
+ provider: picked?.provider ?? current.provider,
3604
+ model: picked?.model ?? current.model,
3605
+ ...effort ?? current.reasoningEffort ? { reasoningEffort: effort ?? current.reasoningEffort } : {}
3606
+ };
3607
+ if (!next.provider || !next.model) return;
3608
+ if (next.provider === current.provider && next.model === current.model && next.reasoningEffort === current.reasoningEffort) return;
3609
+ await service().selectModel({
3610
+ sessionId,
3611
+ ...next
3612
+ });
3613
+ };
3614
+ const presetOf = (session) => {
3615
+ try {
3616
+ return permissions()?.current?.(session);
3617
+ } catch {
3618
+ return;
3619
+ }
3620
+ };
3621
+ const sandboxFor = (preset, cwd) => preset === "danger-full-access" || preset === "auto" ? { type: "dangerFullAccess" } : {
3622
+ type: "workspaceWrite",
3623
+ writableRoots: cwd ? [cwd] : [],
3624
+ networkAccess: false,
3625
+ excludeTmpdirEnvVar: false,
3626
+ excludeSlashTmp: false
3627
+ };
3628
+ const applyPermissions = (agent, params) => {
3629
+ const type = params?.sandboxPolicy?.type ?? (typeof params?.sandbox === "string" ? params.sandbox : void 0);
3630
+ const preset = type === "dangerFullAccess" || type === "danger-full-access" ? "danger-full-access" : type === "workspaceWrite" || type === "workspace-write" ? "workspace-write" : void 0;
3631
+ if (!preset || !agent?.session || presetOf(agent.session) === preset) return;
3632
+ permissions()?.set?.(agent.session, preset);
3633
+ };
3634
+ const titles = /* @__PURE__ */ new Map();
3635
+ const lastPrompt = { text: "" };
3636
+ const titleThread = (params) => {
3637
+ const id = `ephemeral-${randomUUID()}`;
3638
+ titles.set(id, Date.now());
3639
+ for (const [key, at] of titles) if (Date.now() - at > 6e5) titles.delete(key);
3640
+ return {
3641
+ ...settings(void 0, ""),
3642
+ thread: {
3643
+ ...thread({
3644
+ id,
3645
+ cwd: "",
3646
+ createdAt: Date.now()
3647
+ }),
3648
+ ephemeral: true,
3649
+ modelProvider: "dsh"
3650
+ }
3651
+ };
3652
+ };
3653
+ const titleTurn = async (params, notify) => {
3654
+ const title = (lastPrompt.text || inputText(params?.input).split(/\n\s*\n/u).map((part) => part.trim()).filter(Boolean).at(-1) || "DSH").split("\n")[0].trim().slice(0, 36);
3655
+ const turn = {
3656
+ ...emptyTurn(),
3657
+ status: "completed",
3658
+ completedAt: seconds(Date.now())
3659
+ };
3660
+ const item = {
3661
+ type: "agentMessage",
3662
+ id: randomUUID(),
3663
+ text: title,
3664
+ phase: null,
3665
+ memoryCitation: null,
3666
+ delivery: null
3667
+ };
3668
+ turn.items.push(item);
3669
+ const threadId = params.threadId;
3670
+ titles.delete(threadId);
3671
+ queueMicrotask(() => {
3672
+ notify("turn/started", {
3673
+ threadId,
3674
+ turn: {
3675
+ ...turn,
3676
+ items: [],
3677
+ status: "inProgress",
3678
+ completedAt: null
3679
+ }
3680
+ }).catch(() => {});
3681
+ notify("item/completed", {
3682
+ threadId,
3683
+ turnId: turn.id,
3684
+ item,
3685
+ completedAtMs: Date.now()
3686
+ }).catch(() => {});
3687
+ notify("turn/completed", {
3688
+ threadId,
3689
+ turn
3690
+ }).catch(() => {});
3691
+ });
3692
+ return { turn: {
3693
+ ...turn,
3694
+ items: [],
3695
+ status: "inProgress",
3696
+ completedAt: null
3697
+ } };
3698
+ };
3699
+ async function startTurn(params, notify) {
3700
+ const text = inputText(params?.input);
3701
+ if (text === "") throw invalidParams("Only text input is supported");
3702
+ lastPrompt.text = text;
3703
+ const id = params?.threadId;
3704
+ const agent = await agentFor(id);
3705
+ if (active.has(id)) throw new RpcError(-32602, "A turn is already running in this conversation");
3706
+ subscribe(id, notify);
3707
+ await applySelection(agent, id, params);
3708
+ applyPermissions(agent, params);
3709
+ const turn = emptyTurn();
3710
+ active.set(id, {
3711
+ turn,
3712
+ ended: void 0
3713
+ });
3714
+ const userItem = {
3715
+ type: "userMessage",
3716
+ id: randomUUID(),
3717
+ clientId: params.clientUserMessageId ?? null,
3718
+ content: [{
3719
+ type: "text",
3720
+ text,
3721
+ text_elements: []
3722
+ }]
3723
+ };
3724
+ emit(id, "turn/started", {
3725
+ threadId: id,
3726
+ turn
3727
+ });
3728
+ emit(id, "item/started", {
3729
+ threadId: id,
3730
+ turnId: turn.id,
3731
+ item: userItem,
3732
+ startedAtMs: Date.now()
3733
+ });
3734
+ emit(id, "item/completed", {
3735
+ threadId: id,
3736
+ turnId: turn.id,
3737
+ item: userItem,
3738
+ completedAtMs: Date.now()
3739
+ });
3740
+ try {
3741
+ await service().prompt({
3742
+ requestId: randomUUID(),
3743
+ sessionId: id,
3744
+ mode: "queue",
3745
+ content: [{
3746
+ type: "text",
3747
+ text
3748
+ }]
3749
+ }, AbortSignal.timeout(3e4));
3750
+ } catch (error) {
3751
+ active.delete(id);
3752
+ throw new RpcError(-32603, error?.message ? `DSH did not accept the message: ${String(error.message).slice(0, 200)}` : "DSH did not accept the message");
3753
+ }
3754
+ Promise.resolve(agent.whenIdle?.()).catch(() => {}).then(() => {
3755
+ const reason = active.get(id)?.ended;
3756
+ turn.status = reason?.kind === "error" ? "failed" : reason?.kind === "aborted" || reason?.kind === "interrupted" ? "interrupted" : "completed";
3757
+ if (reason?.kind === "error") {
3758
+ const message = String(reason.error?.message ?? reason.error?.code ?? "The model request failed").slice(0, 500);
3759
+ turn.error = {
3760
+ message,
3761
+ codexErrorInfo: null,
3762
+ additionalDetails: null
3763
+ };
3764
+ emit(id, "error", {
3765
+ threadId: id,
3766
+ turnId: turn.id,
3767
+ willRetry: false,
3768
+ error: turn.error
3769
+ });
3770
+ }
3771
+ turn.completedAt = seconds(Date.now());
3772
+ active.delete(id);
3773
+ emit(id, "turn/completed", {
3774
+ threadId: id,
3775
+ turn
3776
+ });
3777
+ const [next, ...rest] = queues.get(id) ?? [];
3778
+ if (next) {
3779
+ queues.set(id, rest);
3780
+ emit(id, "thread/queue/changed", { threadId: id });
3781
+ startTurn({
3782
+ threadId: id,
3783
+ input: next.input,
3784
+ clientUserMessageId: next.clientUserMessageId
3785
+ }, notify).catch(() => {});
3786
+ }
3787
+ });
3788
+ return { turn };
3789
+ }
3790
+ const methods = {
3791
+ initialize: () => ({
3792
+ codexHome: "",
3793
+ platformFamily: process.platform === "win32" ? "windows" : "unix",
3794
+ platformOs: {
3795
+ darwin: "macos",
3796
+ win32: "windows"
3797
+ }[process.platform] ?? process.platform,
3798
+ userAgent
3799
+ }),
3800
+ "account/read": () => ({
3801
+ account: {
3802
+ type: "chatgpt",
3803
+ email: null,
3804
+ planType: "unknown"
3805
+ },
3806
+ requiresOpenaiAuth: false
3807
+ }),
3808
+ "modelProvider/capabilities/read": () => ({
3809
+ imageGeneration: false,
3810
+ namespaceTools: false,
3811
+ webSearch: false
3812
+ }),
3813
+ "model/list": async () => {
3814
+ const value = await loadCatalog();
3815
+ const defaultKey = value.default ? modelKey(value.default) : void 0;
3816
+ const data = [];
3817
+ for (const group of value.groups ?? []) for (const model of group.models ?? []) {
3818
+ const key = modelKey({
3819
+ provider: group.id,
3820
+ model: model.id
3821
+ });
3822
+ const efforts = (model.reasoning?.efforts ?? []).map((effort) => ({
3823
+ reasoningEffort: effort.id,
3824
+ description: effort.name ?? effort.id
3825
+ }));
3826
+ const supported = efforts.length > 0 ? efforts : [{
3827
+ reasoningEffort: "medium",
3828
+ description: "Default"
3829
+ }];
3830
+ data.push({
3831
+ id: key,
3832
+ model: key,
3833
+ displayName: model.name ?? model.id,
3834
+ description: group.name ?? "",
3835
+ hidden: false,
3836
+ isDefault: key === defaultKey,
3837
+ supportedReasoningEfforts: supported,
3838
+ defaultReasoningEffort: model.reasoning?.defaultEffort ?? supported[0].reasoningEffort,
3839
+ inputModalities: ["text"],
3840
+ supportsPersonality: false,
3841
+ additionalSpeedTiers: [],
3842
+ upgrade: null,
3843
+ upgradeInfo: null,
3844
+ availabilityNux: null,
3845
+ serviceTiers: []
3846
+ });
3847
+ }
3848
+ return {
3849
+ data,
3850
+ nextCursor: null
3851
+ };
3852
+ },
3853
+ "configRequirements/read": () => ({ requirements: {
3854
+ allowedSandboxModes: ["workspace-write", "danger-full-access"],
3855
+ allowedApprovalPolicies: ["on-request", "never"],
3856
+ allowRemoteControl: true
3857
+ } }),
3858
+ "thread/settings/update": async (params) => {
3859
+ const agent = await agentFor(params?.threadId);
3860
+ await applySelection(agent, params.threadId, params);
3861
+ applyPermissions(agent, params);
3862
+ return {};
3863
+ },
3864
+ "thread/attachment/list": () => ({
3865
+ data: [],
3866
+ nextCursor: null
3867
+ }),
3868
+ "config/batchWrite": () => ({
3869
+ status: "ok",
3870
+ version: "1",
3871
+ filePath: process.platform === "win32" ? "C:dshconfig.toml" : "/dsh/config.toml",
3872
+ overriddenMetadata: null
3873
+ }),
3874
+ "command/exec": () => {
3875
+ throw new RpcError(-32600, "Running commands directly is not available on a DSH host; ask in the conversation instead");
3876
+ },
3877
+ "plugin/installed": () => ({
3878
+ marketplaceLoadErrors: [],
3879
+ marketplaces: []
3880
+ }),
3881
+ "fs/createDirectory": () => ({}),
3882
+ "fs/getMetadata": () => ({
3883
+ isDirectory: true,
3884
+ isFile: false,
3885
+ isSymlink: false,
3886
+ createdAtMs: Date.now(),
3887
+ modifiedAtMs: Date.now()
3888
+ }),
3889
+ "fs/readDirectory": () => ({ entries: [] }),
3890
+ "collaborationMode/list": () => ({ data: [] }),
3891
+ "permissionProfile/list": () => ({
3892
+ data: [],
3893
+ nextCursor: null
3894
+ }),
3895
+ "threadSection/list": () => ({
3896
+ data: [],
3897
+ nextCursor: null
3898
+ }),
3899
+ "thread/goal/get": () => ({ goal: null }),
3900
+ "experimentalFeature/list": () => ({
3901
+ data: [],
3902
+ nextCursor: null
3903
+ }),
3904
+ "skills/list": () => ({ data: [] }),
3905
+ "config/read": () => ({
3906
+ config: {},
3907
+ origins: {},
3908
+ layers: null
3909
+ }),
3910
+ "thread/loaded/list": () => ({
3911
+ data: [...subscribers.keys()],
3912
+ nextCursor: null
3913
+ }),
3914
+ "thread/unsubscribe": () => ({ status: "notSubscribed" }),
3915
+ "thread/list": async (params, { signal }) => {
3916
+ const value = await service().list({}, signal);
3917
+ const archived = new Set(workspaces()?.archivedSessionIds ?? []);
3918
+ const wantArchived = params?.archived === true;
3919
+ return {
3920
+ data: (value.items ?? []).filter((item) => item.origin !== "subagent" && !item.blank && archived.has(item.sessionId) === wantArchived).map((item) => thread({
3921
+ id: item.sessionId,
3922
+ cwd: item.cwd,
3923
+ createdAt: item.updatedAt,
3924
+ updatedAt: item.updatedAt,
3925
+ running: item.running,
3926
+ title: item.projections?.values?.title ?? null
3927
+ })).filter((entry) => !params?.searchTerm || `${entry.name ?? ""} ${entry.preview}`.toLocaleLowerCase().includes(String(params.searchTerm).toLocaleLowerCase())),
3928
+ nextCursor: null,
3929
+ backwardsCursor: null
3930
+ };
3931
+ },
3932
+ "thread/archive": async (params) => {
3933
+ const registry = workspaces();
3934
+ if (!registry) throw new RpcError(-32603, "DSH archive is unavailable in this profile");
3935
+ try {
3936
+ await registry.archiveSession(params?.threadId, { stopActivity: true });
3937
+ } catch {
3938
+ throw notFound(params?.threadId);
3939
+ }
3940
+ active.delete(params?.threadId);
3941
+ return {};
3942
+ },
3943
+ "thread/unarchive": async (params) => {
3944
+ const registry = workspaces();
3945
+ if (!registry) throw new RpcError(-32603, "DSH archive is unavailable in this profile");
3946
+ await registry.unarchiveSession(params?.threadId);
3947
+ return { thread: (await openThread(params?.threadId)).thread };
3948
+ },
3949
+ "thread/read": async (params) => ({ thread: (await openThread(params?.threadId)).thread }),
3950
+ "thread/resume": async (params, { notify }) => {
3951
+ const { agent, thread: value } = await openThread(params?.threadId, notify);
3952
+ return {
3953
+ ...settings(agent, value.cwd),
3954
+ thread: value
3955
+ };
3956
+ },
3957
+ "thread/start": async (params, { notify }) => {
3958
+ if (params?.ephemeral === true) return titleThread(params);
3959
+ const cwd = localFolder(params?.cwd);
3960
+ const created = await service().create(cwd ? { cwd } : {});
3961
+ const agent = await agentFor(created.sessionId);
3962
+ await applySelection(agent, created.sessionId, params);
3963
+ const { thread: value } = await openThread(created.sessionId, notify);
3964
+ emit(created.sessionId, "thread/started", { thread: value });
3965
+ return {
3966
+ ...settings(agent, value.cwd),
3967
+ thread: value
3968
+ };
3969
+ },
3970
+ "turn/start": (params, { notify }) => titles.has(params?.threadId) ? titleTurn(params, notify) : startTurn(params, notify),
3971
+ "turn/interrupt": (params) => {
3972
+ service().cancel({ sessionId: params?.threadId });
3973
+ return {};
3974
+ },
3975
+ "turn/steer": async (params) => {
3976
+ const text = inputText(params?.input);
3977
+ if (text === "") throw invalidParams("Only text input is supported");
3978
+ await service().prompt({
3979
+ requestId: randomUUID(),
3980
+ sessionId: params?.threadId,
3981
+ mode: "steer",
3982
+ content: [{
3983
+ type: "text",
3984
+ text
3985
+ }]
3986
+ }, AbortSignal.timeout(3e4));
3987
+ return { turnId: params?.expectedTurnId ?? active.get(params?.threadId)?.turn.id ?? "" };
3988
+ },
3989
+ "thread/name/set": async (params) => {
3990
+ await service().rename({
3991
+ sessionId: params?.threadId,
3992
+ title: String(params?.name ?? "")
3993
+ });
3994
+ return {};
3995
+ },
3996
+ "thread/queue/list": (params) => ({
3997
+ data: queues.get(params?.threadId) ?? [],
3998
+ nextCursor: null
3999
+ }),
4000
+ "thread/queue/add": async (params, { notify }) => {
4001
+ if (!Array.isArray(params?.input)) throw invalidParams("input is required");
4002
+ const id = params?.threadId;
4003
+ const queued = {
4004
+ id: randomUUID(),
4005
+ clientUserMessageId: String(params.clientUserMessageId ?? randomUUID()),
4006
+ input: params.input
4007
+ };
4008
+ subscribe(id, notify);
4009
+ if (!active.has(id)) await startTurn({
4010
+ threadId: id,
4011
+ input: queued.input,
4012
+ clientUserMessageId: queued.clientUserMessageId,
4013
+ model: params.model,
4014
+ effort: params.effort
4015
+ }, notify);
4016
+ else {
4017
+ queues.set(id, [...queues.get(id) ?? [], queued]);
4018
+ emit(id, "thread/queue/changed", { threadId: id });
4019
+ }
4020
+ return { queuedSubmission: queued };
4021
+ },
4022
+ "thread/queue/delete": (params) => {
4023
+ const list = queues.get(params?.threadId) ?? [];
4024
+ const next = list.filter((entry) => entry.id !== params?.queuedSubmissionId);
4025
+ queues.set(params?.threadId, next);
4026
+ if (next.length !== list.length) emit(params?.threadId, "thread/queue/changed", { threadId: params?.threadId });
4027
+ return { deleted: next.length !== list.length };
4028
+ },
4029
+ "thread/queue/reorder": (params) => {
4030
+ const order = Array.isArray(params?.queuedSubmissionIds) ? params.queuedSubmissionIds : [];
4031
+ const list = queues.get(params?.threadId) ?? [];
4032
+ queues.set(params?.threadId, [...order.map((id) => list.find((entry) => entry.id === id)).filter(Boolean), ...list.filter((entry) => !order.includes(entry.id))]);
4033
+ emit(params?.threadId, "thread/queue/changed", { threadId: params?.threadId });
4034
+ return {};
4035
+ },
4036
+ "thread/queue/start": async (params, { notify }) => {
4037
+ const list = queues.get(params?.threadId) ?? [];
4038
+ const queued = list.find((entry) => params?.queuedSubmissionId == null || entry.id === params.queuedSubmissionId);
4039
+ if (!queued) throw invalidParams("Nothing queued");
4040
+ queues.set(params.threadId, list.filter((entry) => entry !== queued));
4041
+ emit(params.threadId, "thread/queue/changed", { threadId: params.threadId });
4042
+ return startTurn({
4043
+ threadId: params.threadId,
4044
+ input: queued.input,
4045
+ clientUserMessageId: queued.clientUserMessageId
4046
+ }, notify);
4047
+ }
4048
+ };
4049
+ const callOf = (session, callId) => {
4050
+ for (const message of session.deriveMessages().toReversed()) {
4051
+ if (message.role !== "assistant") continue;
4052
+ const block = message.content.find((entry) => entry.type === "tool-call" && entry.id === callId);
4053
+ if (block) return block;
4054
+ }
4055
+ };
4056
+ const commandText = (session, request) => {
4057
+ const call = request.callId ? callOf(session, request.callId) : void 0;
4058
+ const args = call ? parseArguments(call.arguments) : void 0;
4059
+ const shown = typeof args?.command === "string" ? args.command : typeof args?.cmd === "string" ? args.cmd : call ? `${call.name} ${JSON.stringify(args)}` : request.toolName;
4060
+ return String(shown).slice(0, 2e3);
4061
+ };
4062
+ /**
4063
+ * DSH approval answerer: while a phone drives a turn, it answers that turn's approvals.
4064
+ * Without a phone (or if it drops) the request goes on to DSH's own prompt.
4065
+ */
4066
+ /** The phone driving this agent's running turn, when there is one. */
4067
+ const phoneFor = (request) => {
4068
+ const session = request.agent?.session;
4069
+ const running = session ? active.get(session.id) : void 0;
4070
+ const phone = session ? [...subscribers.get(session.id) ?? []].find((notify) => typeof notify.ask === "function") : void 0;
4071
+ return running && phone ? {
4072
+ session,
4073
+ running,
4074
+ phone
4075
+ } : void 0;
4076
+ };
4077
+ /**
4078
+ * Ask whichever phone drives the turn. A phone that drops (app switched away, network) gets the
4079
+ * same request again when it reopens the thread within PHONE_RETURN_MS; otherwise DSH decides.
4080
+ */
4081
+ const askPhone = async (request, method, params) => {
4082
+ const sessionId = request.agent.session.id;
4083
+ for (;;) {
4084
+ const phone = phoneFor(request)?.phone;
4085
+ if (!phone) throw new RpcError(-32800, "No phone");
4086
+ try {
4087
+ return await phone.ask(method, params, request.signal);
4088
+ } catch (error) {
4089
+ if (request.signal?.aborted || !active.has(sessionId) || phoneFor(request)?.phone === phone) throw error;
4090
+ trace({
4091
+ method: `wait:${method}`,
4092
+ session: sessionId
4093
+ });
4094
+ if (!phoneFor(request) && !await phoneReturns(sessionId, phoneReturnMs, request.signal)) throw error;
4095
+ }
4096
+ }
4097
+ };
4098
+ /** DSH's ask-user tool: show the questions on the phone that drives the turn, else leave them to DSH. */
4099
+ const onQuestion = (request, next) => {
4100
+ const found = phoneFor(request);
4101
+ trace({
4102
+ method: "hook:user-questions",
4103
+ phone: Boolean(found),
4104
+ session: request.agent?.session?.id ?? null
4105
+ });
4106
+ if (!found) return next();
4107
+ const { session, running } = found;
4108
+ const labels = new Map(request.questions.map((question) => [question.id, new Set((question.options ?? []).map((option) => option.label))]));
4109
+ return askPhone(request, "item/tool/requestUserInput", {
4110
+ threadId: session.id,
4111
+ turnId: running.turn.id,
4112
+ itemId: request.wait?.callId ?? randomUUID(),
4113
+ questions: request.questions.map((question) => ({
4114
+ id: question.id,
4115
+ header: question.header ?? "",
4116
+ question: question.detail ? `${question.question}\n\n${question.detail}` : question.question,
4117
+ isOther: true,
4118
+ isSecret: false,
4119
+ options: question.options?.length ? question.options.map((option) => ({
4120
+ label: option.label,
4121
+ description: option.description ?? ""
4122
+ })) : null
4123
+ }))
4124
+ }).then((answer) => ({ answers: request.questions.map((question) => {
4125
+ const given = Array.isArray(answer?.answers?.[question.id]?.answers) ? answer.answers[question.id].answers.map(String) : [];
4126
+ const known = labels.get(question.id);
4127
+ const selected = given.filter((value) => known.has(value));
4128
+ const custom = given.filter((value) => !known.has(value)).join("\n");
4129
+ return {
4130
+ id: question.id,
4131
+ selected,
4132
+ ...custom === "" ? {} : { custom }
4133
+ };
4134
+ }) }), () => next());
4135
+ };
4136
+ const onApproval = (request, next) => {
4137
+ const found = phoneFor(request);
4138
+ trace({
4139
+ method: "hook:approval",
4140
+ phone: Boolean(found),
4141
+ session: request.agent?.session?.id ?? null
4142
+ });
4143
+ if (!found) return next();
4144
+ const { session, running } = found;
4145
+ return askPhone(request, "item/commandExecution/requestApproval", {
4146
+ threadId: session.id,
4147
+ turnId: running.turn.id,
4148
+ itemId: request.callId ?? randomUUID(),
4149
+ startedAtMs: Date.now(),
4150
+ command: commandText(session, request),
4151
+ cwd: session.meta?.cwd ?? null,
4152
+ reason: request.displayReason?.en ?? request.reason ?? null,
4153
+ commandActions: [],
4154
+ availableDecisions: [
4155
+ "accept",
4156
+ "decline",
4157
+ "cancel"
4158
+ ]
4159
+ }).then((answer) => {
4160
+ const decision = typeof answer?.decision === "string" ? answer.decision : "";
4161
+ return decision === "accept" || decision === "acceptForSession" ? "allowed-once" : decision === "cancel" ? "cancelled" : "rejected";
4162
+ }, () => next());
4163
+ };
4164
+ /** Forward committed assistant and tool events of a phone-started turn as items. */
4165
+ const onSessionEvent = (session, event) => {
4166
+ const running = active.get(session.id);
4167
+ if (!running || !subscribers.has(session.id)) return;
4168
+ if (event.type === "turn/end") {
4169
+ running.ended = event.data?.reason;
4170
+ return;
4171
+ }
4172
+ if (event.type !== "assistant/message" && event.type !== "tool/result") return;
4173
+ const message = session.deriveMessages().at(-1);
4174
+ if (message?.role === "tool") {
4175
+ const item = running.turn.items.find((entry) => entry.type === "dynamicToolCall" && entry.id === message.toolCallId);
4176
+ if (item) {
4177
+ item.contentItems = [{
4178
+ type: "inputText",
4179
+ text: textOf(message.content)
4180
+ }];
4181
+ item.status = message.isError ? "failed" : "completed";
4182
+ item.success = !message.isError;
4183
+ emit(session.id, "item/completed", {
4184
+ threadId: session.id,
4185
+ turnId: running.turn.id,
4186
+ item,
4187
+ completedAtMs: Date.now()
4188
+ });
4189
+ }
4190
+ return;
4191
+ }
4192
+ if (!message || message.role !== "assistant") return;
4193
+ const items = projectTurns([{
4194
+ role: "user",
4195
+ id: "x",
4196
+ content: []
4197
+ }, message]).flatMap((turn) => turn.items.slice(1));
4198
+ for (const item of items) {
4199
+ running.turn.items.push(item);
4200
+ emit(session.id, "item/started", {
4201
+ threadId: session.id,
4202
+ turnId: running.turn.id,
4203
+ item,
4204
+ startedAtMs: Date.now()
4205
+ });
4206
+ emit(session.id, "item/completed", {
4207
+ threadId: session.id,
4208
+ turnId: running.turn.id,
4209
+ item,
4210
+ completedAtMs: Date.now()
4211
+ });
4212
+ }
4213
+ };
4214
+ return {
4215
+ methods,
4216
+ onSessionEvent,
4217
+ onApproval,
4218
+ onQuestion,
4219
+ forget: (notify) => {
4220
+ for (const set of subscribers.values()) set.delete(notify);
4221
+ }
4222
+ };
4223
+ }
4224
+ //#endregion
2680
4225
  //#region src/account-vault.js
2681
4226
  const VERSION = 1;
2682
4227
  const DEFAULT_LABEL = "Account 1";
@@ -6061,7 +7606,7 @@ const publicError = (code, message) => ({
6061
7606
  details: { issues: [] }
6062
7607
  }
6063
7608
  });
6064
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
7609
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, remoteControl, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
6065
7610
  return async (endpoint, payload, signal) => {
6066
7611
  if (endpoint === "default-model/status" || endpoint === "default-model/select") try {
6067
7612
  signal.throwIfAborted();
@@ -6076,6 +7621,23 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6076
7621
  if (error?.code === "unavailable") return publicError("unavailable", endpoint === "default-model/status" ? "The default model is unavailable" : "Could not save the default model");
6077
7622
  return publicError("internal", endpoint === "default-model/status" ? "Could not read the default model" : "Could not save the default model");
6078
7623
  }
7624
+ if ([
7625
+ "remote/status",
7626
+ "remote/enable",
7627
+ "remote/disable",
7628
+ "remote/pair"
7629
+ ].includes(endpoint)) try {
7630
+ signal.throwIfAborted();
7631
+ if (!remoteControl) return publicError("unavailable", "Remote Control is unavailable");
7632
+ const action = endpoint.split("/")[1];
7633
+ return {
7634
+ ok: true,
7635
+ value: action === "status" ? remoteControl.status() : action === "enable" ? await remoteControl.enable() : action === "disable" ? await remoteControl.disable() : await remoteControl.pair(signal)
7636
+ };
7637
+ } catch (error) {
7638
+ if (signal.aborted) throw error;
7639
+ return publicError(typeof error?.code === "string" ? error.code : "operation-error", "Remote Control request failed");
7640
+ }
6079
7641
  if (endpoint === "storage/status" || endpoint === "storage/clear-forecast") try {
6080
7642
  signal.throwIfAborted();
6081
7643
  if (endpoint === "storage/clear-forecast") await usageReader.clear();
@@ -6404,6 +7966,7 @@ function apply(ctx, config = {}) {
6404
7966
  compactionMode: settings.get().compactionMode ?? "dsh",
6405
7967
  connectionMode: settings.get().connectionMode ?? "sse",
6406
7968
  reviewModel: settings.get().reviewModel ?? "session",
7969
+ remoteControl: settings.get().remoteControl ?? "off",
6407
7970
  subagentBackend: settings.get().subagentBackend ?? "dsh",
6408
7971
  subagentBackendAvailable: subagentBackend !== void 0,
6409
7972
  subagentRuntimeInstalled: inspectSubagentRuntime().installed,
@@ -6597,6 +8160,65 @@ function apply(ctx, config = {}) {
6597
8160
  }
6598
8161
  });
6599
8162
  const coordinator = new CodexLoginCoordinator(auth);
8163
+ const remoteBridge = createDshRemoteControl({
8164
+ controller: () => ctx.get?.("sessionController"),
8165
+ agents: () => ctx.get?.("agents"),
8166
+ permissions: () => ctx.get?.("permissionPresets"),
8167
+ workspaces: () => ctx.get?.("workspaceRegistry"),
8168
+ trace: (entry) => {
8169
+ try {
8170
+ remoteControl.host.note(entry);
8171
+ } catch {}
8172
+ },
8173
+ userAgent: USER_AGENT
8174
+ });
8175
+ const remoteControl = createRemoteControl({
8176
+ credentials: (signal) => readSubscriptionCredentials(resolveAuth, (options) => store.read(PROVIDER, {
8177
+ ...options,
8178
+ signal
8179
+ }), signal),
8180
+ WebSocket,
8181
+ fetch: (input, init) => network.fetch("remote", input, init),
8182
+ stateFile: dshHomePath("state", "codex-subscription", "remote-control.json"),
8183
+ hostName: `DSH (${hostname()})`,
8184
+ userAgent: USER_AGENT,
8185
+ methods: remoteBridge.methods,
8186
+ onClose: (_client, notify) => remoteBridge.forget(notify)
8187
+ });
8188
+ ctx.on("session/event", (session, event) => {
8189
+ try {
8190
+ remoteBridge.onSessionEvent(session, event);
8191
+ } catch {}
8192
+ });
8193
+ ctx.on("approval/request", (request, next) => {
8194
+ try {
8195
+ return remoteBridge.onApproval(request, next);
8196
+ } catch {
8197
+ return next();
8198
+ }
8199
+ }, true);
8200
+ ctx.on("user-questions/request", (request, next) => {
8201
+ try {
8202
+ return remoteBridge.onQuestion(request, next);
8203
+ } catch {
8204
+ return next();
8205
+ }
8206
+ }, true);
8207
+ ctx.effect(() => {
8208
+ let wanted = false;
8209
+ const follow = (value) => {
8210
+ const next = value.remoteControl === "on";
8211
+ if (next === wanted) return;
8212
+ wanted = next;
8213
+ (next ? remoteControl.enable() : remoteControl.disable()).catch((error) => ctx.logger?.debug?.("Remote Control: %s", error?.code ?? error?.message));
8214
+ };
8215
+ follow(settings.get());
8216
+ const unwatch = settings.watch(follow);
8217
+ return () => {
8218
+ unwatch();
8219
+ remoteControl.disable().catch(() => {});
8220
+ };
8221
+ }, "codex-subscription: Remote Control host");
6600
8222
  const usageReader = createQuotaForecastReader({
6601
8223
  reader: createCodexUsageReader({
6602
8224
  getAuth: resolveAuth,
@@ -6682,6 +8304,7 @@ function apply(ctx, config = {}) {
6682
8304
  preferences,
6683
8305
  defaultModel,
6684
8306
  runtimeManagement,
8307
+ remoteControl,
6685
8308
  onAccountChanged: quotaRetryHandler.notifyAccountChanged,
6686
8309
  diagnosticsReader: () => createSubscriptionDiagnostics({
6687
8310
  auth,