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/README.en.md +6 -5
- package/README.md +6 -5
- package/lib/client.js +2619 -64
- package/lib/index.js +1628 -5
- package/package.json +6 -5
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 = "
|
|
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,
|