codexteer 0.16.0 → 0.18.0
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/CHANGELOG.md +37 -2
- package/README.md +154 -16
- package/assets/remote-device-key.swift +94 -0
- package/bin/codexteer-update.mjs +3 -0
- package/bin/codexteer.mjs +6 -2
- package/docs/cloud.md +113 -0
- package/docs/desktop-cloud.md +36 -0
- package/docs/distribution.md +21 -9
- package/docs/orchestration.md +120 -0
- package/docs/remote-cli-validation.json +139 -0
- package/docs/remote-control.md +65 -0
- package/docs/remote-device-enrollment.md +91 -0
- package/docs/remote-observation.md +57 -0
- package/docs/remote-operations.md +89 -0
- package/docs/thread-create.md +90 -0
- package/package.json +13 -2
- package/scripts/update-desktop.applescript +51 -0
- package/src/agent-launch.mjs +27 -0
- package/src/app-server.mjs +34 -10
- package/src/checkpoint.mjs +52 -16
- package/src/cli.mjs +233 -19
- package/src/cloud-api.mjs +118 -0
- package/src/cloud-cli.mjs +319 -0
- package/src/cloud-connection.mjs +64 -0
- package/src/cloud-id.mjs +16 -0
- package/src/cloud-observe.mjs +178 -0
- package/src/cloud-send.mjs +33 -0
- package/src/cloud.mjs +172 -0
- package/src/connection.mjs +23 -5
- package/src/creation-verification.mjs +36 -0
- package/src/desktop-cloud-api.mjs +119 -0
- package/src/desktop-cloud-id.mjs +18 -0
- package/src/desktop-cloud-project.mjs +148 -0
- package/src/desktop-cloud.mjs +98 -0
- package/src/desktop.mjs +3 -1
- package/src/directive.mjs +5 -2
- package/src/distribution.mjs +6 -6
- package/src/evidence.mjs +3 -0
- package/src/findings.mjs +7 -7
- package/src/help.mjs +155 -26
- package/src/journal.mjs +37 -9
- package/src/launcher.mjs +16 -9
- package/src/monitor.mjs +18 -6
- package/src/native-command.mjs +100 -0
- package/src/native-rpc.mjs +116 -0
- package/src/native-worker.mjs +25 -0
- package/src/observe.mjs +21 -7
- package/src/orchestrate-prompt.mjs +96 -0
- package/src/orchestrate.mjs +29 -0
- package/src/paged-read.mjs +7 -1
- package/src/projects.mjs +104 -0
- package/src/prompt-command.mjs +25 -0
- package/src/prompt.mjs +35 -11
- package/src/remote-client-info.mjs +16 -0
- package/src/remote-control-auth.mjs +109 -0
- package/src/remote-control-socket.mjs +137 -0
- package/src/remote-control.mjs +134 -0
- package/src/remote-device-key.mjs +41 -0
- package/src/remote-device-login.mjs +76 -0
- package/src/remote-device-protocol.mjs +72 -0
- package/src/remote-device-store.mjs +71 -0
- package/src/remote-device-worker.mjs +48 -0
- package/src/remote-device.mjs +202 -0
- package/src/remote-io.mjs +163 -0
- package/src/remote-key-access.mjs +76 -0
- package/src/remote-key-worker.mjs +38 -0
- package/src/remote-keychain.mjs +72 -0
- package/src/remote-management.mjs +234 -0
- package/src/remote.mjs +132 -0
- package/src/resource.mjs +8 -6
- package/src/runner.mjs +6 -1
- package/src/runtime.mjs +4 -2
- package/src/socket-endpoint.mjs +40 -0
- package/src/standalone-create.mjs +135 -0
- package/src/store.mjs +6 -3
- package/src/supervise.mjs +32 -55
- package/src/supervision.mjs +41 -19
- package/src/target.mjs +25 -0
- package/src/task-id.mjs +23 -0
- package/src/thread-create.mjs +128 -0
- package/src/thread-id.mjs +21 -6
- package/src/thread-verify.mjs +108 -0
- package/src/turn-repair.mjs +228 -0
- package/src/update.mjs +158 -0
- package/src/wrapper.mjs +68 -5
package/src/update.mjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdir, lstat, readFile, writeFile, rename, rmdir, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { codexHome } from "./runtime.mjs";
|
|
7
|
+
import { prepareCodexHome, prepareDeployment } from "./distribution.mjs";
|
|
8
|
+
import { APP_PATH, startDesktop, appServerDoctor } from "./launcher.mjs";
|
|
9
|
+
|
|
10
|
+
const script = fileURLToPath(new URL("../scripts/update-desktop.applescript", import.meta.url));
|
|
11
|
+
const error = (code, message) => Object.assign(new Error(message), { code });
|
|
12
|
+
function run(command, args) {
|
|
13
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout: 10000 });
|
|
14
|
+
if (result.error || result.status !== 0) throw error("UPDATE_COMMAND_FAILED", result.error?.message ?? result.stderr?.trim() ?? `${command} failed`);
|
|
15
|
+
return result.stdout.trim();
|
|
16
|
+
}
|
|
17
|
+
export function updateUi(operation) { return run("/usr/bin/osascript", [script, operation]); }
|
|
18
|
+
export function updateBuild() {
|
|
19
|
+
return run("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleVersion", `${APP_PATH}/Contents/Info.plist`]);
|
|
20
|
+
}
|
|
21
|
+
export function desktopPids() {
|
|
22
|
+
return run("/bin/ps", ["-axo", "pid=,comm="]).split("\n").flatMap(line => {
|
|
23
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
24
|
+
return match?.[2] === `${APP_PATH}/Contents/MacOS/ChatGPT` ? [Number(match[1])] : [];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Separate orchestration from platform effects to exercise restarts/failures without
|
|
29
|
+
// updating the developer's app. No signal-based termination or task resumption.
|
|
30
|
+
export async function performUpdate({ timeoutMs = 600000, initialBuild, initialPids }, {
|
|
31
|
+
build = updateBuild, pids = desktopPids, ui = updateUi,
|
|
32
|
+
quit = () => run("/usr/bin/osascript", ["-e", 'tell application id "com.openai.codex" to quit']),
|
|
33
|
+
start = startDesktop, doctor = appServerDoctor, save = async () => {}, sleep = delay, now = Date.now,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const deadline = now() + timeoutMs;
|
|
36
|
+
let lastPhase = "checking", newBuild;
|
|
37
|
+
await save({ phase: lastPhase, initial_build: initialBuild });
|
|
38
|
+
await ui("check");
|
|
39
|
+
while (now() < deadline) {
|
|
40
|
+
const running = await pids();
|
|
41
|
+
try { newBuild = await build(); }
|
|
42
|
+
catch (e) {
|
|
43
|
+
// Sparkle can temporarily move the bundle away during installation.
|
|
44
|
+
if (!running.length && ["downloading", "installing"].includes(lastPhase)) { await sleep(1000); continue; }
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
// Both the on-disk build and process generation must change before quitting
|
|
48
|
+
// the relaunched app. Do not interrupt Sparkle's installation or its old app.
|
|
49
|
+
if (newBuild !== initialBuild && running.length && running.every(pid => !initialPids.includes(pid))) break;
|
|
50
|
+
if (running.length && newBuild === initialBuild && lastPhase !== "installing") {
|
|
51
|
+
let phase;
|
|
52
|
+
try { phase = await ui("advance"); }
|
|
53
|
+
catch (e) {
|
|
54
|
+
// The app can exit between the process snapshot and the AX request.
|
|
55
|
+
if (!(await pids()).length) { await sleep(1000); continue; }
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
if (phase === "up_to_date") return { phase, initial_build: initialBuild, final_build: newBuild, wrapper_verified: false };
|
|
59
|
+
if (["downloading", "installing"].includes(phase) && phase !== lastPhase) {
|
|
60
|
+
lastPhase = phase; await save({ phase });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await sleep(1000);
|
|
64
|
+
}
|
|
65
|
+
if (now() >= deadline) throw error("UPDATE_TIMEOUT", "Update/relaunch did not complete in time. Inspect Codex or its updater for dialogs. No processes were forcibly stopped.");
|
|
66
|
+
await save({ phase: "restoring_wrapper", final_build: newBuild });
|
|
67
|
+
await quit();
|
|
68
|
+
const quitDeadline = now() + 30000;
|
|
69
|
+
while ((await pids()).length) {
|
|
70
|
+
if (now() >= quitDeadline) throw error("UPDATE_QUIT_TIMEOUT", "Codex did not quit normally. Finish its work, quit it, and run codexteer desktop start.");
|
|
71
|
+
await sleep(500);
|
|
72
|
+
}
|
|
73
|
+
await start();
|
|
74
|
+
const report = await doctor();
|
|
75
|
+
if (!report.ready || report.connection?.mode !== "shared" || !report.checks?.bundled_wrapper_node || report.cli_version_status !== "matched") {
|
|
76
|
+
throw error("UPDATE_WRAPPER_UNVERIFIED", "Update installed, but wrapper verification failed. Run codexteer doctor --json.");
|
|
77
|
+
}
|
|
78
|
+
return { phase: "complete", initial_build: initialBuild, final_build: newBuild, wrapper_verified: true, doctor: report };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function privateDirectory(directory) {
|
|
82
|
+
await mkdir(directory, { mode: 0o700 }).catch(e => { if (e.code !== "EEXIST") throw e; });
|
|
83
|
+
const info = await lstat(directory);
|
|
84
|
+
if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid() || (info.mode & 0o077)) throw error("UPDATE_STATE_UNSAFE", `Unsafe update state directory: ${directory}`);
|
|
85
|
+
}
|
|
86
|
+
async function stateRoot(requestedHome = codexHome()) {
|
|
87
|
+
const home = await prepareCodexHome(requestedHome);
|
|
88
|
+
const parent = path.join(home, "codex-steer");
|
|
89
|
+
await privateDirectory(parent);
|
|
90
|
+
const root = path.join(parent, "desktop-update");
|
|
91
|
+
await privateDirectory(root);
|
|
92
|
+
return { home, root };
|
|
93
|
+
}
|
|
94
|
+
async function writeStatus(root, state) {
|
|
95
|
+
const temporary = path.join(root, `status-${process.pid}.tmp`);
|
|
96
|
+
await writeFile(temporary, JSON.stringify({ ...state, updated_at: new Date().toISOString() }) + "\n", { mode: 0o600, flag: "wx" });
|
|
97
|
+
await rename(temporary, path.join(root, "status.json"));
|
|
98
|
+
}
|
|
99
|
+
export async function desktopUpdateStatus() {
|
|
100
|
+
const { root } = await stateRoot();
|
|
101
|
+
try {
|
|
102
|
+
const file = path.join(root, "status.json"), info = await lstat(file);
|
|
103
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid() || (info.mode & 0o077)) throw error("UPDATE_STATE_UNSAFE", "Unsafe update status file.");
|
|
104
|
+
const state = JSON.parse(await readFile(file, "utf8"));
|
|
105
|
+
if (state.worker_pid && !["complete", "up_to_date", "failed"].includes(state.phase)) {
|
|
106
|
+
try { process.kill(state.worker_pid, 0); state.worker_alive = true; }
|
|
107
|
+
catch (e) { if (e.code !== "ESRCH") throw e; state.worker_alive = false; }
|
|
108
|
+
}
|
|
109
|
+
return state;
|
|
110
|
+
} catch (e) { if (e.code === "ENOENT") return { phase: "not_started" }; throw e; }
|
|
111
|
+
}
|
|
112
|
+
export async function launchDesktopUpdate({ dryRun = false, timeoutMs = 600000 } = {}, {
|
|
113
|
+
platform = process.platform, executablePath = process.execPath, pids = desktopPids,
|
|
114
|
+
ui = updateUi, build = updateBuild, prepare = prepareDeployment, spawnWorker = spawn,
|
|
115
|
+
home: requestedHome = codexHome(),
|
|
116
|
+
} = {}) {
|
|
117
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 10000 || timeoutMs > 3600000) throw error("UPDATE_ARGUMENT", "--timeout-ms must be an integer between 10000 and 3600000.");
|
|
118
|
+
if (dryRun) return { dry_run: true, started: false, timeout_ms: timeoutMs, steps: ["official_update_ui", "wait_for_new_build_and_relaunch", "normal_quit", "wrapper_start", "doctor"] };
|
|
119
|
+
if (platform !== "darwin") throw error("UPDATE_PLATFORM", "Desktop update requires macOS.");
|
|
120
|
+
if (process.env.CODEX_APP_SERVER_WS_URL?.trim()) throw error("UPDATE_ENVIRONMENT", "Remove CODEX_APP_SERVER_WS_URL before updating with a shared wrapper.");
|
|
121
|
+
const executable = await realpath(executablePath);
|
|
122
|
+
if (executable.includes(".app/Contents/")) throw error("UPDATE_NODE", "Run desktop update with an external Node installation, so replacing the app cannot remove the worker runtime.");
|
|
123
|
+
const initialPids = pids();
|
|
124
|
+
if (initialPids.length !== 1) throw error("UPDATE_APP_STATE", "Exactly one running Codex Desktop is required.");
|
|
125
|
+
if (ui("preflight") !== "checking") throw error("UPDATE_UI_UNAVAILABLE", "Codex update menu is unavailable. Allow Accessibility/Automation for your terminal and retry.");
|
|
126
|
+
const initialBuild = build();
|
|
127
|
+
const { home, root } = await stateRoot(requestedHome);
|
|
128
|
+
const deployment = await prepare(home);
|
|
129
|
+
const lock = path.join(root, "lock");
|
|
130
|
+
await mkdir(lock, { mode: 0o700 }).catch(e => {
|
|
131
|
+
if (e.code === "EEXIST") throw error("UPDATE_BUSY", `An update lock exists. Check desktop update-status; if its worker has stopped, inspect the app before manually removing ${lock}.`);
|
|
132
|
+
throw e;
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
await writeStatus(root, { phase: "starting", initial_build: initialBuild });
|
|
136
|
+
const child = spawnWorker(executable, [path.join(deployment.directory, "bin/codexteer-update.mjs"), JSON.stringify({ root, timeoutMs, initialBuild, initialPids })], {
|
|
137
|
+
detached: true, stdio: "ignore", cwd: deployment.directory, env: { ...process.env, CODEX_HOME: home },
|
|
138
|
+
});
|
|
139
|
+
await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
|
|
140
|
+
child.unref();
|
|
141
|
+
return { started: true, worker_pid: child.pid, status_command: "codexteer desktop update-status --json", status_path: path.join(root, "status.json") };
|
|
142
|
+
} catch (e) { await writeStatus(root, { phase: "failed", error: e.message }); await rmdir(lock); throw e; }
|
|
143
|
+
}
|
|
144
|
+
export async function updateWorker(options) {
|
|
145
|
+
let state = { phase: "starting", worker_pid: process.pid, initial_build: options.initialBuild };
|
|
146
|
+
const save = async patch => { state = { ...state, ...patch }; await writeStatus(options.root, state); };
|
|
147
|
+
try {
|
|
148
|
+
await save({});
|
|
149
|
+
const running = desktopPids();
|
|
150
|
+
if (updateBuild() !== options.initialBuild || running.length !== 1 || running[0] !== options.initialPids[0]) {
|
|
151
|
+
throw error("UPDATE_APP_CHANGED", "Codex changed after preflight. Inspect its update state before retrying.");
|
|
152
|
+
}
|
|
153
|
+
await save(await performUpdate(options, { save }));
|
|
154
|
+
} catch (e) {
|
|
155
|
+
await save({ phase: "failed", error: { code: e.code ?? "UPDATE_FAILED", message: e.message }, recovery: "Inspect Codex and the updater. After installation finishes, quit normally and run codexteer desktop start, then codexteer doctor --json." });
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
} finally { await rmdir(path.join(options.root, "lock")); }
|
|
158
|
+
}
|
package/src/wrapper.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { accessSync, constants as fsConstants, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { once } from "node:events";
|
|
4
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
import { claimRuntime } from "./runtime.mjs";
|
|
6
7
|
import { connectSocket } from "./rpc.mjs";
|
|
7
8
|
import { relay } from "./bridge.mjs";
|
|
@@ -9,8 +10,61 @@ import { DesktopSubscriptions, serveSubscriptions } from "./subscription.mjs";
|
|
|
9
10
|
import { VERSION, PACKAGE_NAME } from "./version.mjs";
|
|
10
11
|
import { RUNTIME_PROTOCOL, RUNTIME_CAPABILITIES } from "./compatibility.mjs";
|
|
11
12
|
import { describeDistribution } from "./distribution.mjs";
|
|
13
|
+
import { inspectAppServerSocket } from "./socket-endpoint.mjs";
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
const DESKTOP_RESOURCES = "/Applications/ChatGPT.app/Contents/Resources";
|
|
16
|
+
const LEGACY_BUNDLED_CLI = path.join(DESKTOP_RESOURCES, "codex");
|
|
17
|
+
|
|
18
|
+
function isWithinDirectory(directory, candidate) {
|
|
19
|
+
const relative = path.relative(directory, candidate);
|
|
20
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveManifestCli(packageDirectory, resourcesDirectory) {
|
|
24
|
+
try {
|
|
25
|
+
const root = realpathSync(packageDirectory);
|
|
26
|
+
if (!isWithinDirectory(resourcesDirectory, root)) return null;
|
|
27
|
+
const manifest = JSON.parse(readFileSync(path.join(root, "codex-package.json"), "utf8"));
|
|
28
|
+
if ((manifest.variant != null && manifest.variant !== "codex")
|
|
29
|
+
|| (manifest.layoutVersion != null && (!Number.isSafeInteger(manifest.layoutVersion) || manifest.layoutVersion < 1))
|
|
30
|
+
|| typeof manifest.entrypoint !== "string" || manifest.entrypoint.length === 0
|
|
31
|
+
|| manifest.entrypoint.includes("\0") || path.isAbsolute(manifest.entrypoint)
|
|
32
|
+
|| manifest.entrypoint.split(/[\\/]/).some(part => part === "" || part === "." || part === "..")) return null;
|
|
33
|
+
|
|
34
|
+
const entrypoint = realpathSync(path.resolve(root, manifest.entrypoint));
|
|
35
|
+
if (!isWithinDirectory(root, entrypoint) || !statSync(entrypoint).isFile()) return null;
|
|
36
|
+
accessSync(entrypoint, fsConstants.X_OK);
|
|
37
|
+
return entrypoint;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveBundledCli() {
|
|
44
|
+
try {
|
|
45
|
+
const resourcesDirectory = realpathSync(DESKTOP_RESOURCES);
|
|
46
|
+
const packages = readdirSync(resourcesDirectory, { withFileTypes: true })
|
|
47
|
+
.filter(entry => entry.isDirectory() || entry.isSymbolicLink())
|
|
48
|
+
.map(entry => path.join(resourcesDirectory, entry.name))
|
|
49
|
+
.sort();
|
|
50
|
+
const manifestCli = packages.map(directory => resolveManifestCli(directory, resourcesDirectory)).find(Boolean);
|
|
51
|
+
if (manifestCli) return manifestCli;
|
|
52
|
+
} catch {
|
|
53
|
+
// Fall through to the pre-manifest app layout.
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (statSync(LEGACY_BUNDLED_CLI).isFile()) {
|
|
58
|
+
accessSync(LEGACY_BUNDLED_CLI, fsConstants.X_OK);
|
|
59
|
+
return LEGACY_BUNDLED_CLI;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Preserve the legacy path for a useful ENOENT diagnostic at startup.
|
|
63
|
+
}
|
|
64
|
+
return LEGACY_BUNDLED_CLI;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const BUNDLED_CLI = resolveBundledCli();
|
|
14
68
|
export const BUNDLED_NODE = "/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node";
|
|
15
69
|
const DESKTOP_EXECUTABLE = "/Applications/ChatGPT.app/Contents/MacOS/ChatGPT";
|
|
16
70
|
|
|
@@ -95,6 +149,7 @@ export async function runWrapper(args, { executable = BUNDLED_CLI, env = process
|
|
|
95
149
|
codex_steer_package: PACKAGE_NAME, codex_steer_version: VERSION, codex_steer_protocol: RUNTIME_PROTOCOL,
|
|
96
150
|
codex_steer_capabilities: RUNTIME_CAPABILITIES,
|
|
97
151
|
distribution_sha256: distribution.sha256,
|
|
152
|
+
native_tools_pipe: env.CODEX_APP_TOOLS_PIPE_PATH ?? null,
|
|
98
153
|
});
|
|
99
154
|
let child;
|
|
100
155
|
let socket;
|
|
@@ -110,14 +165,22 @@ export async function runWrapper(args, { executable = BUNDLED_CLI, env = process
|
|
|
110
165
|
});
|
|
111
166
|
let spawnError;
|
|
112
167
|
child.on("error", error => { spawnError = error; });
|
|
168
|
+
let endpoint;
|
|
169
|
+
let socketError;
|
|
113
170
|
for (let i = 0; i < 100; i++) {
|
|
114
171
|
if (interrupted || spawnError || child.exitCode != null || child.signalCode != null) throw new Error("App Server exited during startup.");
|
|
115
|
-
|
|
172
|
+
try {
|
|
173
|
+
endpoint = await inspectAppServerSocket(lease.paths.socket);
|
|
174
|
+
break;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.code !== "ENOENT") throw error;
|
|
177
|
+
socketError = error;
|
|
178
|
+
}
|
|
116
179
|
await delay(100);
|
|
117
180
|
}
|
|
118
|
-
|
|
181
|
+
if (!endpoint) throw socketError ?? new Error("App Server socket did not become ready during startup.");
|
|
119
182
|
await lease.update({ server_pid: child.pid });
|
|
120
|
-
socket = await connectSocket(
|
|
183
|
+
socket = await connectSocket(endpoint.socket);
|
|
121
184
|
subscriptions = new DesktopSubscriptions(socket);
|
|
122
185
|
stopSubscriptions = await serveSubscriptions(lease.paths.control, subscriptions);
|
|
123
186
|
let initId;
|