dsh-codex-subscription 2.5.9 → 3.0.0-beta.2

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