@blue-soda/dsh-remote 0.4.32 → 0.5.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/cordis.patch.yml +30 -32
- package/dist/client.github.js +393 -1889
- package/dist/client.js +393 -1889
- package/dist/index.js +1267 -313
- package/dist/index.js.map +3 -3
- package/dsh-plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4249,27 +4249,15 @@ var identityKeySchema = external_exports.string().regex(/^[A-Za-z0-9_-]{42}[AEIM
|
|
|
4249
4249
|
var deviceVersionSchema = boundedUtf8Schema(1, MAX_DEVICE_VERSION_LENGTH);
|
|
4250
4250
|
var authTokenSchema = boundedUtf8Schema(16, MAX_AUTH_TOKEN_LENGTH);
|
|
4251
4251
|
var expiresAtSchema = external_exports.number().int().positive().safe();
|
|
4252
|
-
var
|
|
4252
|
+
var accountDeviceDescriptorSchema = external_exports.object({
|
|
4253
4253
|
deviceId: deviceIdSchema,
|
|
4254
4254
|
name: deviceNameSchema,
|
|
4255
|
-
role: external_exports.literal("host"),
|
|
4256
4255
|
platform: devicePlatformSchema,
|
|
4257
4256
|
identityKey: identityKeySchema,
|
|
4258
4257
|
clientVersion: deviceVersionSchema,
|
|
4259
4258
|
harnessVersion: deviceVersionSchema.optional()
|
|
4260
|
-
})
|
|
4261
|
-
var
|
|
4262
|
-
deviceId: deviceIdSchema,
|
|
4263
|
-
name: deviceNameSchema,
|
|
4264
|
-
role: external_exports.literal("client"),
|
|
4265
|
-
platform: devicePlatformSchema,
|
|
4266
|
-
identityKey: identityKeySchema,
|
|
4267
|
-
clientVersion: deviceVersionSchema
|
|
4268
|
-
}).strict();
|
|
4269
|
-
var accountDeviceDescriptorSchema = external_exports.union([
|
|
4270
|
-
hostDeviceDescriptorSchema,
|
|
4271
|
-
clientDeviceDescriptorSchema
|
|
4272
|
-
]);
|
|
4259
|
+
});
|
|
4260
|
+
var hostDeviceDescriptorSchema = accountDeviceDescriptorSchema;
|
|
4273
4261
|
var deviceRegistrationRequestSchema = external_exports.object({
|
|
4274
4262
|
v: external_exports.literal(PROTOCOL_VERSION),
|
|
4275
4263
|
device: accountDeviceDescriptorSchema
|
|
@@ -13357,6 +13345,7 @@ var DEFAULT_REMOTE_SERVER_URL = "https://sakakibara.ink:8443";
|
|
|
13357
13345
|
// src/config.ts
|
|
13358
13346
|
var entryConfigSchema = s.object({
|
|
13359
13347
|
enabled: s.boolean(),
|
|
13348
|
+
// Accepted and ignored, for the same reason as in `configSchema`: an upgrade must not brick a profile.
|
|
13360
13349
|
role: s.union(["host", "client", "both"]),
|
|
13361
13350
|
serverUrl: s.string(),
|
|
13362
13351
|
deviceName: s.string(),
|
|
@@ -13394,6 +13383,9 @@ var reconnectSchema = external_exports.union([
|
|
|
13394
13383
|
]);
|
|
13395
13384
|
var configSchema = external_exports.object({
|
|
13396
13385
|
enabled: external_exports.boolean().optional(),
|
|
13386
|
+
// Older patches - including the plugin's own shipped `cordis.patch.yml` - seed `role`. It means nothing
|
|
13387
|
+
// now: a device has one identity and the control switch decides whether it accepts control. It must stay
|
|
13388
|
+
// *accepted* all the same, or upgrading would fail to activate the plugin on every existing installation.
|
|
13397
13389
|
role: external_exports.enum(["host", "client", "both"]).optional(),
|
|
13398
13390
|
serverUrl: external_exports.string().url().optional(),
|
|
13399
13391
|
deviceName: external_exports.string().trim().min(1).max(80).optional(),
|
|
@@ -13421,7 +13413,6 @@ function resolveConfig(input2 = {}, env = process.env) {
|
|
|
13421
13413
|
}
|
|
13422
13414
|
return {
|
|
13423
13415
|
enabled: parsed.enabled ?? true,
|
|
13424
|
-
role: parsed.role ?? "host",
|
|
13425
13416
|
...serverUrl === void 0 ? {} : { serverUrl },
|
|
13426
13417
|
deviceName: parsed.deviceName ?? hostname(),
|
|
13427
13418
|
// `paused` keeps this machine unreachable without releasing its credentials:
|
|
@@ -13467,7 +13458,7 @@ function normalizeServerUrl(value) {
|
|
|
13467
13458
|
}
|
|
13468
13459
|
|
|
13469
13460
|
// src/version.ts
|
|
13470
|
-
var PLUGIN_VERSION = "0.
|
|
13461
|
+
var PLUGIN_VERSION = "0.5.0";
|
|
13471
13462
|
|
|
13472
13463
|
// src/server-api.ts
|
|
13473
13464
|
var ENABLED_QR_PROVIDERS = ["github"];
|
|
@@ -13479,10 +13470,9 @@ function oauthProviderName(provider) {
|
|
|
13479
13470
|
}
|
|
13480
13471
|
var TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
13481
13472
|
var HostServerApi = class {
|
|
13482
|
-
constructor(serverUrl, store, fetchImplementation = fetch
|
|
13473
|
+
constructor(serverUrl, store, fetchImplementation = fetch) {
|
|
13483
13474
|
this.store = store;
|
|
13484
13475
|
this.fetchImplementation = fetchImplementation;
|
|
13485
|
-
this.role = role;
|
|
13486
13476
|
this.baseUrl = normalizeServerUrl(serverUrl);
|
|
13487
13477
|
}
|
|
13488
13478
|
baseUrl;
|
|
@@ -13508,11 +13498,33 @@ var HostServerApi = class {
|
|
|
13508
13498
|
const identity = this.requireIdentity();
|
|
13509
13499
|
return await this.store.load(this.baseUrl, identity.deviceId) !== void 0;
|
|
13510
13500
|
}
|
|
13501
|
+
/**
|
|
13502
|
+
* Drop the in-memory credential cache so the next request reads what is on disk.
|
|
13503
|
+
*
|
|
13504
|
+
* Signing in writes fresh credentials for this device; a long-lived runtime that kept its own copy went on
|
|
13505
|
+
* presenting the rejected one and answered AUTH_INVALID until the process restarted.
|
|
13506
|
+
*/
|
|
13507
|
+
reloadCredentials() {
|
|
13508
|
+
this.credentials = void 0;
|
|
13509
|
+
this.credentialsPromise = void 0;
|
|
13510
|
+
}
|
|
13511
13511
|
async clearAuthorization() {
|
|
13512
13512
|
this.credentials = void 0;
|
|
13513
13513
|
this.credentialsPromise = void 0;
|
|
13514
13514
|
await this.store.clear();
|
|
13515
13515
|
}
|
|
13516
|
+
/**
|
|
13517
|
+
* Tell the Server whether this device accepts control.
|
|
13518
|
+
*
|
|
13519
|
+
* The flag lives on the Server so it holds across restarts and is visible to clients listing devices;
|
|
13520
|
+
* the local identity and credentials are untouched.
|
|
13521
|
+
*/
|
|
13522
|
+
async setHostControl(enabled) {
|
|
13523
|
+
await this.request("/api/v1/devices/self/control", {
|
|
13524
|
+
method: "POST",
|
|
13525
|
+
body: JSON.stringify({ enabled })
|
|
13526
|
+
});
|
|
13527
|
+
}
|
|
13516
13528
|
async revokeCurrentDevice() {
|
|
13517
13529
|
const identity = this.requireIdentity();
|
|
13518
13530
|
if (await this.store.load(this.baseUrl, identity.deviceId) === void 0) {
|
|
@@ -13623,9 +13635,6 @@ var HostServerApi = class {
|
|
|
13623
13635
|
};
|
|
13624
13636
|
}
|
|
13625
13637
|
async authorizeHostWithCode(identity, code) {
|
|
13626
|
-
if (this.role !== "host") {
|
|
13627
|
-
throw new ServerApiError("METHOD_NOT_ALLOWED", "Host registration codes can only authorize a Host device.", false);
|
|
13628
|
-
}
|
|
13629
13638
|
const registrationCode = code.trim().toUpperCase();
|
|
13630
13639
|
if (registrationCode.length === 0) {
|
|
13631
13640
|
throw new ServerApiError("INVALID_MESSAGE", "A Host registration code is required.", false);
|
|
@@ -13640,21 +13649,6 @@ var HostServerApi = class {
|
|
|
13640
13649
|
});
|
|
13641
13650
|
return { method: "host_registration_code" };
|
|
13642
13651
|
}
|
|
13643
|
-
async authorizeOwnedRole(identity, authorizingAccessToken, account) {
|
|
13644
|
-
this.bindIdentity(identity);
|
|
13645
|
-
const tokens = await this.publicRequest("/api/v1/devices/register-owned-role", {
|
|
13646
|
-
method: "POST",
|
|
13647
|
-
body: JSON.stringify({ v: 1, device: this.deviceDescriptor(identity) })
|
|
13648
|
-
}, authorizingAccessToken);
|
|
13649
|
-
this.credentials = await this.saveTokens(identity, validateTokens(tokens), {
|
|
13650
|
-
authorizationMethod: "owned_device",
|
|
13651
|
-
...account === void 0 ? {} : { account }
|
|
13652
|
-
});
|
|
13653
|
-
return {
|
|
13654
|
-
method: "owned_device",
|
|
13655
|
-
...account === void 0 ? {} : { account }
|
|
13656
|
-
};
|
|
13657
|
-
}
|
|
13658
13652
|
async authenticate(identity = this.requireIdentity()) {
|
|
13659
13653
|
this.bindIdentity(identity);
|
|
13660
13654
|
if (this.credentials !== void 0 && this.credentials.accessTokenExpiresAt > Date.now() + 3e4) {
|
|
@@ -13760,11 +13754,10 @@ var HostServerApi = class {
|
|
|
13760
13754
|
return {
|
|
13761
13755
|
deviceId: identity.deviceId,
|
|
13762
13756
|
name: identity.name,
|
|
13763
|
-
role: this.role,
|
|
13764
13757
|
platform: platform(),
|
|
13765
13758
|
identityKey: identity.publicKey,
|
|
13766
13759
|
clientVersion: PLUGIN_VERSION,
|
|
13767
|
-
...this.
|
|
13760
|
+
...this.harnessVersion === void 0 ? {} : { harnessVersion: this.harnessVersion }
|
|
13768
13761
|
};
|
|
13769
13762
|
}
|
|
13770
13763
|
saveTokens(identity, tokens, authorization) {
|
|
@@ -13832,7 +13825,7 @@ function normalizeOAuthScanUrl(value, baseUrl) {
|
|
|
13832
13825
|
}
|
|
13833
13826
|
var ClientServerApi = class extends HostServerApi {
|
|
13834
13827
|
constructor(serverUrl, store, fetchImplementation = fetch) {
|
|
13835
|
-
super(serverUrl, store, fetchImplementation
|
|
13828
|
+
super(serverUrl, store, fetchImplementation);
|
|
13836
13829
|
}
|
|
13837
13830
|
};
|
|
13838
13831
|
var ServerApiError = class extends Error {
|
|
@@ -13882,7 +13875,7 @@ function mapStatus(status2) {
|
|
|
13882
13875
|
}
|
|
13883
13876
|
function parseHostDevice(value) {
|
|
13884
13877
|
const item = requireRecord(value, "host device");
|
|
13885
|
-
if (
|
|
13878
|
+
if (typeof item.deviceId !== "string" || typeof item.name !== "string" || typeof item.platform !== "string" || typeof item.membershipId !== "string" || item.membershipId.length === 0) {
|
|
13886
13879
|
throw new ServerApiError("INVALID_MESSAGE", "The Server returned invalid host device data.", false);
|
|
13887
13880
|
}
|
|
13888
13881
|
return {
|
|
@@ -13891,6 +13884,7 @@ function parseHostDevice(value) {
|
|
|
13891
13884
|
platform: item.platform,
|
|
13892
13885
|
membershipId: item.membershipId,
|
|
13893
13886
|
...typeof item.online === "boolean" ? { online: item.online } : {},
|
|
13887
|
+
...typeof item.hostControl === "boolean" ? { hostControl: item.hostControl } : {},
|
|
13894
13888
|
...typeof item.lastSeenAt === "number" && Number.isSafeInteger(item.lastSeenAt) ? { lastSeenAt: item.lastSeenAt } : {},
|
|
13895
13889
|
...typeof item.clientVersion === "string" ? { clientVersion: item.clientVersion } : {},
|
|
13896
13890
|
...typeof item.harnessVersion === "string" ? { harnessVersion: item.harnessVersion } : {}
|
|
@@ -13898,17 +13892,17 @@ function parseHostDevice(value) {
|
|
|
13898
13892
|
}
|
|
13899
13893
|
function parseAuthorizedPeer(value) {
|
|
13900
13894
|
const item = requireRecord(value, "authorized peer");
|
|
13901
|
-
if (
|
|
13895
|
+
if (typeof item.deviceId !== "string" || item.deviceId.length === 0 || typeof item.name !== "string" || item.name.length === 0 || typeof item.platform !== "string" || item.platform.length === 0 || typeof item.identityKey !== "string" || !isIdentityKey(item.identityKey) || typeof item.membershipId !== "string" || item.membershipId.length === 0) {
|
|
13902
13896
|
throw new ServerApiError("INVALID_MESSAGE", "The Server returned invalid authorized peer data.", false);
|
|
13903
13897
|
}
|
|
13904
13898
|
return {
|
|
13905
13899
|
deviceId: item.deviceId,
|
|
13906
13900
|
name: item.name,
|
|
13907
|
-
role: item.role,
|
|
13908
13901
|
platform: item.platform,
|
|
13909
13902
|
identityKey: item.identityKey,
|
|
13910
13903
|
membershipId: item.membershipId,
|
|
13911
13904
|
...typeof item.online === "boolean" ? { online: item.online } : {},
|
|
13905
|
+
...typeof item.hostControl === "boolean" ? { hostControl: item.hostControl } : {},
|
|
13912
13906
|
...typeof item.lastSeenAt === "number" && Number.isSafeInteger(item.lastSeenAt) ? { lastSeenAt: item.lastSeenAt } : {}
|
|
13913
13907
|
};
|
|
13914
13908
|
}
|
|
@@ -14139,8 +14133,10 @@ var HostServerConnection = class {
|
|
|
14139
14133
|
socket.close(4008, "invalid control frame");
|
|
14140
14134
|
});
|
|
14141
14135
|
};
|
|
14142
|
-
socket.onerror = () => {
|
|
14143
|
-
if (!acknowledged)
|
|
14136
|
+
socket.onerror = (event) => {
|
|
14137
|
+
if (!acknowledged) {
|
|
14138
|
+
finish(new ControlConnectionError("CONNECTION_FAILED", `Unable to open the Server WebSocket.${socketFailureDetail(event)}`));
|
|
14139
|
+
}
|
|
14144
14140
|
};
|
|
14145
14141
|
socket.onclose = (event) => {
|
|
14146
14142
|
const close = async () => {
|
|
@@ -14246,7 +14242,7 @@ var HostServerConnection = class {
|
|
|
14246
14242
|
});
|
|
14247
14243
|
return;
|
|
14248
14244
|
}
|
|
14249
|
-
if (descriptor.
|
|
14245
|
+
if (descriptor.deviceId !== payload.clientDeviceId || descriptor.identityKey !== payload.clientIdentityKey) {
|
|
14250
14246
|
this.sendControl("connect.rejected", { connectionId: payload.connectionId });
|
|
14251
14247
|
this.logger.warn("connection rejected by peer identity validation", {
|
|
14252
14248
|
clientDeviceId: shortId(payload.clientDeviceId)
|
|
@@ -14840,6 +14836,21 @@ function connectionFailureDetail(error) {
|
|
|
14840
14836
|
...causeCode === void 0 ? {} : { causeCode }
|
|
14841
14837
|
};
|
|
14842
14838
|
}
|
|
14839
|
+
function socketFailureDetail(event) {
|
|
14840
|
+
const parts = [];
|
|
14841
|
+
if (event !== null && typeof event === "object") {
|
|
14842
|
+
const message = "message" in event && typeof event.message === "string" ? event.message : void 0;
|
|
14843
|
+
if (message !== void 0 && message !== "") parts.push(message);
|
|
14844
|
+
const error = "error" in event ? event.error : void 0;
|
|
14845
|
+
if (error !== null && typeof error === "object") {
|
|
14846
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
14847
|
+
const errorMessage = "message" in error && typeof error.message === "string" ? error.message : void 0;
|
|
14848
|
+
if (code !== void 0) parts.push(code);
|
|
14849
|
+
if (errorMessage !== void 0 && errorMessage !== "" && errorMessage !== message) parts.push(errorMessage);
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14852
|
+
return parts.length === 0 ? "" : ` (${parts.join(": ")})`;
|
|
14853
|
+
}
|
|
14843
14854
|
function errorCode2(error) {
|
|
14844
14855
|
return error instanceof ServerApiError || error instanceof ControlConnectionError ? error.code : "CONNECTION_FAILED";
|
|
14845
14856
|
}
|
|
@@ -15392,11 +15403,30 @@ function createRemoteId() {
|
|
|
15392
15403
|
var MAX_DISPLAY_ITEM_TEXT = 256 * 1024;
|
|
15393
15404
|
var CODEX_IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
15394
15405
|
var DATA_IMAGE_URL = /^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]+={0,2})$/u;
|
|
15406
|
+
var CODEX_CAPABILITY_EVENT = "codex.app.capability";
|
|
15395
15407
|
var CodexRemoteClient = class {
|
|
15396
15408
|
core;
|
|
15397
15409
|
constructor(core) {
|
|
15398
15410
|
this.core = core;
|
|
15399
15411
|
}
|
|
15412
|
+
/**
|
|
15413
|
+
* Listen for the Host's Codex availability changes.
|
|
15414
|
+
*
|
|
15415
|
+
* Fires whether or not this client is following any thread, which is what makes "the peer switched Codex off"
|
|
15416
|
+
* reach the session on screen immediately instead of at the next failing call. The Host sends this only on a
|
|
15417
|
+
* real transition, so `available: true` never means "you lost something".
|
|
15418
|
+
* @param handler - called with the new availability.
|
|
15419
|
+
* @returns an unsubscribe function.
|
|
15420
|
+
*/
|
|
15421
|
+
watchCapability(handler) {
|
|
15422
|
+
return this.core.onEvent((event) => {
|
|
15423
|
+
if (event.event !== CODEX_CAPABILITY_EVENT)
|
|
15424
|
+
return;
|
|
15425
|
+
if (!isRecord(event.data) || typeof event.data.available !== "boolean")
|
|
15426
|
+
return;
|
|
15427
|
+
handler(event.data.available);
|
|
15428
|
+
});
|
|
15429
|
+
}
|
|
15400
15430
|
/** Low-level allowlisted request used by the Desktop Web loopback facade. */
|
|
15401
15431
|
request(method, params, signal) {
|
|
15402
15432
|
const largeHistory = method === "dsh/sessionHistory" || method === "thread/read" && isRecord(params) && params.includeTurns === true;
|
|
@@ -17836,6 +17866,10 @@ function isHostWorkspaceEndpoint(endpoint) {
|
|
|
17836
17866
|
function isHostWorkspaceStreamEndpoint(endpoint) {
|
|
17837
17867
|
return HOST_WORKSPACE_STREAM_ENDPOINTS.has(endpoint);
|
|
17838
17868
|
}
|
|
17869
|
+
function sameWorkspaceRoot(left, right) {
|
|
17870
|
+
const normalise = (value) => value.trim().replace(/[\\/]+$/, "").replace(/\\/g, "/").toLowerCase();
|
|
17871
|
+
return normalise(left) === normalise(right);
|
|
17872
|
+
}
|
|
17839
17873
|
function codexProjectWorkspaceId(projectId) {
|
|
17840
17874
|
return `${CODEX_WORKSPACE_PREFIX}project:${projectId}`;
|
|
17841
17875
|
}
|
|
@@ -17849,6 +17883,9 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
17849
17883
|
this.sessionGeneration = sessionGeneration;
|
|
17850
17884
|
this.hostCarrier = hostCarrier;
|
|
17851
17885
|
this.api = this.createApiProxy();
|
|
17886
|
+
this.stopCapabilityWatch = this.client.watchCapability?.((available) => {
|
|
17887
|
+
if (!available) this.onSessionLost?.("domain-notice");
|
|
17888
|
+
});
|
|
17852
17889
|
}
|
|
17853
17890
|
api;
|
|
17854
17891
|
catalog;
|
|
@@ -17881,16 +17918,33 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
17881
17918
|
this.localCarrier = carrier;
|
|
17882
17919
|
}
|
|
17883
17920
|
localCarrier;
|
|
17921
|
+
onSessionLost;
|
|
17922
|
+
stopCapabilityWatch;
|
|
17923
|
+
/**
|
|
17924
|
+
* Register what to do when a Codex call fails because the session is gone.
|
|
17925
|
+
*
|
|
17926
|
+
* Set after construction rather than passed in, so neither the constructor nor the static factory signature has to
|
|
17927
|
+
* change: the runtime hooks its reconnect entry point here. The Host replacing or switching off its Codex domain
|
|
17928
|
+
* closes the bridges, and the next call would otherwise just be shown as an error.
|
|
17929
|
+
* @param handler - called once per detected loss, with what detected it, before the failure is returned.
|
|
17930
|
+
* @returns nothing.
|
|
17931
|
+
*/
|
|
17932
|
+
watchCodexSessionLost(handler) {
|
|
17933
|
+
this.onSessionLost = handler;
|
|
17934
|
+
}
|
|
17884
17935
|
static remote(core, host, sessionGeneration = "legacy", hostCarrier) {
|
|
17885
17936
|
return new _CodexVirtualHarness(new CodexRemoteClient(core), host, sessionGeneration, hostCarrier);
|
|
17886
17937
|
}
|
|
17887
17938
|
async workspaces(signal) {
|
|
17888
17939
|
return (await this.refreshCatalog(signal)).workspaces;
|
|
17889
17940
|
}
|
|
17890
|
-
async selectWorkspace(workspaceId, signal) {
|
|
17941
|
+
async selectWorkspace(workspaceId, signal, rootPath) {
|
|
17891
17942
|
const catalog = await loadCatalog(this.client, signal);
|
|
17892
|
-
const workspace = catalog.workspaces.find((item) => item.workspaceId === workspaceId);
|
|
17893
|
-
if (workspace === void 0)
|
|
17943
|
+
const workspace = catalog.workspaces.find((item) => item.workspaceId === workspaceId) ?? (rootPath === void 0 ? void 0 : catalog.workspaces.find((item) => sameWorkspaceRoot(item.path, rootPath)));
|
|
17944
|
+
if (workspace === void 0) {
|
|
17945
|
+
const seen = catalog.workspaces.slice(0, 5).map((item) => item.workspaceId + "@" + item.path).join(", ");
|
|
17946
|
+
throw new Error("The selected CodeX workspace is no longer available. (wanted: " + workspaceId + " root: " + String(rootPath) + " catalog: " + seen + ")");
|
|
17947
|
+
}
|
|
17894
17948
|
this.selectedWorkspaceId = workspace.workspaceId;
|
|
17895
17949
|
this.catalog = catalog;
|
|
17896
17950
|
return workspace;
|
|
@@ -17907,7 +17961,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
17907
17961
|
for (const sessionId of candidates) {
|
|
17908
17962
|
const threadId = nativeThreadId(sessionId);
|
|
17909
17963
|
try {
|
|
17910
|
-
|
|
17964
|
+
await this.fetchThread(threadId, signal);
|
|
17911
17965
|
return sessionId;
|
|
17912
17966
|
} catch {
|
|
17913
17967
|
}
|
|
@@ -18021,6 +18075,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
18021
18075
|
}
|
|
18022
18076
|
}
|
|
18023
18077
|
} catch (error) {
|
|
18078
|
+
if (isCodexSessionLoss(error)) this.onSessionLost?.("call-failed");
|
|
18024
18079
|
return failFrom(error);
|
|
18025
18080
|
}
|
|
18026
18081
|
}
|
|
@@ -18051,6 +18106,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
18051
18106
|
async close() {
|
|
18052
18107
|
if (this.closed) return;
|
|
18053
18108
|
this.closed = true;
|
|
18109
|
+
this.stopCapabilityWatch?.();
|
|
18054
18110
|
for (const stream of this.workspaceStreams) stream.close();
|
|
18055
18111
|
for (const stream of this.controlStreams) stream.close();
|
|
18056
18112
|
for (const stream of this.eventStreams.values()) stream.close();
|
|
@@ -18333,7 +18389,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
18333
18389
|
threadId,
|
|
18334
18390
|
(frame) => this.acceptCodexFrame(follow, frame),
|
|
18335
18391
|
signal,
|
|
18336
|
-
() => this.
|
|
18392
|
+
(reason) => this.noteRemoteStreamClosed(follow, reason)
|
|
18337
18393
|
);
|
|
18338
18394
|
follow.close = () => stream.close();
|
|
18339
18395
|
} catch (error) {
|
|
@@ -19107,7 +19163,7 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
19107
19163
|
threadId,
|
|
19108
19164
|
(frame) => this.acceptCodexFrame(follow, frame),
|
|
19109
19165
|
controller.signal,
|
|
19110
|
-
() => this.
|
|
19166
|
+
(reason) => this.noteRemoteStreamClosed(follow, reason)
|
|
19111
19167
|
);
|
|
19112
19168
|
follow.close = async () => {
|
|
19113
19169
|
controller.abort();
|
|
@@ -19129,6 +19185,21 @@ var CodexVirtualHarness = class _CodexVirtualHarness {
|
|
|
19129
19185
|
follow.queue.close();
|
|
19130
19186
|
this.emitRemoteEvent("api-session/status", [follow.sessionId, false]);
|
|
19131
19187
|
}
|
|
19188
|
+
/**
|
|
19189
|
+
* The Host closed a Codex stream this client was following.
|
|
19190
|
+
*
|
|
19191
|
+
* `failed` and `peer-disconnected` mean the Codex session behind it is gone - the Host switched Codex off,
|
|
19192
|
+
* replaced its domain, or its App Server died. That is a lost session like any other, so the runtime is told
|
|
19193
|
+
* now, which is what puts "reconnecting" on screen immediately instead of leaving the user to discover it as
|
|
19194
|
+
* an error the next time they touch the session. A stream we closed ourselves, or one that simply ended,
|
|
19195
|
+
* says nothing about the session.
|
|
19196
|
+
* @param follow - the projection that was following it.
|
|
19197
|
+
* @param reason - why the peer closed the stream.
|
|
19198
|
+
*/
|
|
19199
|
+
noteRemoteStreamClosed(follow, reason) {
|
|
19200
|
+
if (reason === "failed" || reason === "peer-disconnected") this.onSessionLost?.("stream-closed");
|
|
19201
|
+
this.closeFollowAfterRemoteStreamClosed(follow);
|
|
19202
|
+
}
|
|
19132
19203
|
async refreshAndPublishWorkspaces() {
|
|
19133
19204
|
const catalog = await this.refreshCatalog().catch(() => void 0);
|
|
19134
19205
|
if (catalog !== void 0) this.publishWorkspaceBaseline(catalog);
|
|
@@ -21596,7 +21667,7 @@ var QUICK_RECONNECT_ATTEMPTS = 2;
|
|
|
21596
21667
|
var QUICK_RECONNECT_WINDOW_MS = 1e4;
|
|
21597
21668
|
var QUICK_RECONNECT_RETRY_DELAY_MS = 5e3;
|
|
21598
21669
|
var QUICK_RECONNECT_FORCE_RELAY = true;
|
|
21599
|
-
var
|
|
21670
|
+
var BACKGROUND_RETRY_DELAYS_MS = [5e3, 1e4, 2e4, 3e4, 3e4, 3e4, 3e4];
|
|
21600
21671
|
var ClientModeRuntime = class {
|
|
21601
21672
|
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory, targetStore) {
|
|
21602
21673
|
this.config = config;
|
|
@@ -21613,6 +21684,16 @@ var ClientModeRuntime = class {
|
|
|
21613
21684
|
preview;
|
|
21614
21685
|
identity;
|
|
21615
21686
|
connected;
|
|
21687
|
+
/**
|
|
21688
|
+
* The connection the Remote panel browses a Host with.
|
|
21689
|
+
*
|
|
21690
|
+
* Deliberately separate from {@link connected}: opening the panel, connecting to a Host and looking at or
|
|
21691
|
+
* picking its workspaces is *not* a remote session. Nothing here may become "what the user is watching",
|
|
21692
|
+
* arm a reconnect, or survive as something a later boot would restore - the user is still in the local
|
|
21693
|
+
* shell until they actually open a workspace. Keeping it in this field is what enforces that: the recovery
|
|
21694
|
+
* path only ever looks at {@link connected}.
|
|
21695
|
+
*/
|
|
21696
|
+
browsing;
|
|
21616
21697
|
/**
|
|
21617
21698
|
* Set when a remote session dropped and the runtime fell back to local.
|
|
21618
21699
|
*
|
|
@@ -21646,6 +21727,29 @@ var ClientModeRuntime = class {
|
|
|
21646
21727
|
* the link is back.
|
|
21647
21728
|
*/
|
|
21648
21729
|
lastWorkspaceSelection;
|
|
21730
|
+
/**
|
|
21731
|
+
* The last workspaces listed per Host.
|
|
21732
|
+
*
|
|
21733
|
+
* A workspace picked in the panel is opened by id, and a replaced Codex domain hands out new ids for the same
|
|
21734
|
+
* directories. Keeping the previous listing is what lets a stale id be translated back to its root and opened
|
|
21735
|
+
* again instead of failing as "no longer available".
|
|
21736
|
+
*/
|
|
21737
|
+
lastCodexListing = /* @__PURE__ */ new Map();
|
|
21738
|
+
/**
|
|
21739
|
+
* Every workspace id this client has seen for a Host, mapped to its root.
|
|
21740
|
+
*
|
|
21741
|
+
* A panel entry can come from a listing made before the Host replaced its Codex domain, so the id is missing from
|
|
21742
|
+
* the latest listing while the directory itself is unchanged. Keeping the whole history is what lets such an id be
|
|
21743
|
+
* translated back to a root and reopened.
|
|
21744
|
+
*/
|
|
21745
|
+
codexWorkspaceRoots = /* @__PURE__ */ new Map();
|
|
21746
|
+
/**
|
|
21747
|
+
* Hosts this run has actually opened a Codex workspace on.
|
|
21748
|
+
*
|
|
21749
|
+
* Whether the peer merely never offered Codex or has just switched it off decides the wording the user sees, and
|
|
21750
|
+
* the remembered selection cannot answer that on its own: switching to local clears it by design.
|
|
21751
|
+
*/
|
|
21752
|
+
codexHostsSeen = /* @__PURE__ */ new Set();
|
|
21649
21753
|
codexVirtual;
|
|
21650
21754
|
proxySwitch;
|
|
21651
21755
|
gatewaySwitch;
|
|
@@ -21667,6 +21771,21 @@ var ClientModeRuntime = class {
|
|
|
21667
21771
|
* stops as soon as the user asks for local.
|
|
21668
21772
|
*/
|
|
21669
21773
|
reconnecting;
|
|
21774
|
+
/**
|
|
21775
|
+
* What the recovery is putting back, for the header.
|
|
21776
|
+
*
|
|
21777
|
+
* The window has to be able to say *which* session is coming back while the carriers are already local - a
|
|
21778
|
+
* Codex recovery that fell back to the local shell used to read "remote DSH, reconnecting", which describes
|
|
21779
|
+
* a different thing than the one being restored. The kind comes from the remembered selection, so it stays
|
|
21780
|
+
* right for the whole window without a second copy of the state to keep in sync.
|
|
21781
|
+
* @returns the payload the status carries for a running recovery.
|
|
21782
|
+
*/
|
|
21783
|
+
reconnectState() {
|
|
21784
|
+
const state = this.reconnecting;
|
|
21785
|
+
if (state === void 0) return { targetDeviceId: "", phase: "restore" };
|
|
21786
|
+
const kind = this.sessionTargetFor(state.targetDeviceId)?.kind;
|
|
21787
|
+
return { ...state, ...kind === void 0 ? {} : { kind } };
|
|
21788
|
+
}
|
|
21670
21789
|
/** Periodic proof of life for the live remote session; absent while nothing is connected. */
|
|
21671
21790
|
closed = false;
|
|
21672
21791
|
async start() {
|
|
@@ -21682,7 +21801,7 @@ var ClientModeRuntime = class {
|
|
|
21682
21801
|
if (this.config.hostControl?.enabled !== false && this.host !== void 0 && this.server.hasStoredAuthorization !== void 0) {
|
|
21683
21802
|
try {
|
|
21684
21803
|
if (await this.server.hasStoredAuthorization() && (this.host.hasStoredAuthorization === void 0 || !await this.host.hasStoredAuthorization())) {
|
|
21685
|
-
|
|
21804
|
+
this.logger.info("Host credentials are present; the Host connection starts on its own");
|
|
21686
21805
|
}
|
|
21687
21806
|
} catch (error) {
|
|
21688
21807
|
this.logger.warn("automatic Host authorization failed", { code: safeErrorCode(error) });
|
|
@@ -21700,20 +21819,6 @@ var ClientModeRuntime = class {
|
|
|
21700
21819
|
async hasStoredAuthorization() {
|
|
21701
21820
|
return this.server.hasStoredAuthorization === void 0 ? false : await this.server.hasStoredAuthorization();
|
|
21702
21821
|
}
|
|
21703
|
-
async authorizeHostByDefault() {
|
|
21704
|
-
try {
|
|
21705
|
-
if (this.host === void 0) return;
|
|
21706
|
-
if (this.config.hostControl?.enabled === false) return;
|
|
21707
|
-
const status2 = this.host.hostStatus();
|
|
21708
|
-
if (status2.authorized) return;
|
|
21709
|
-
const rejected = status2.error !== void 0 && HOST_AUTHORIZATION_ERRORS.has(status2.error);
|
|
21710
|
-
if (!rejected && this.host.hasStoredAuthorization !== void 0 && await this.host.hasStoredAuthorization()) return;
|
|
21711
|
-
const credentials = await this.server.authenticate(this.requireIdentity());
|
|
21712
|
-
await this.host.authorizeHostAsOwned(credentials.accessToken, credentials.account);
|
|
21713
|
-
} catch (error) {
|
|
21714
|
-
this.logger.warn("automatic Host authorization failed", { code: safeErrorCode(error) });
|
|
21715
|
-
}
|
|
21716
|
-
}
|
|
21717
21822
|
registerControl(connection, webServer) {
|
|
21718
21823
|
return registerControlRoute(connection, (endpoint, payload, signal) => this.handleControl(endpoint, payload, signal), webServer);
|
|
21719
21824
|
}
|
|
@@ -21728,7 +21833,7 @@ var ClientModeRuntime = class {
|
|
|
21728
21833
|
...targetStatus,
|
|
21729
21834
|
connected: this.connected !== void 0,
|
|
21730
21835
|
fellBackToLocal: this.fellBackToLocal,
|
|
21731
|
-
...this.reconnecting === void 0 ? {} : { reconnecting:
|
|
21836
|
+
...this.reconnecting === void 0 ? {} : { reconnecting: this.reconnectState() },
|
|
21732
21837
|
transport: this.connected?.client.getStats().mode ?? "Disconnected",
|
|
21733
21838
|
connectedTargetDeviceId: this.connected?.target.deviceId,
|
|
21734
21839
|
preferredTransports: this.config.forceRelay ? ["relay"] : ["lan", "p2p", "turn", "relay"],
|
|
@@ -21774,7 +21879,7 @@ var ClientModeRuntime = class {
|
|
|
21774
21879
|
};
|
|
21775
21880
|
}
|
|
21776
21881
|
async devices() {
|
|
21777
|
-
this.
|
|
21882
|
+
if (!await this.recoverHostAuthorization()) return [];
|
|
21778
21883
|
this.requireIdentity();
|
|
21779
21884
|
const serverDevices = await this.server.listDevices();
|
|
21780
21885
|
const remoteDevices = serverDevices.filter((device) => device.deviceId !== this.host?.hostStatus().deviceId);
|
|
@@ -21785,18 +21890,18 @@ var ClientModeRuntime = class {
|
|
|
21785
21890
|
}));
|
|
21786
21891
|
}
|
|
21787
21892
|
/**
|
|
21788
|
-
*
|
|
21789
|
-
*
|
|
21790
|
-
*
|
|
21791
|
-
*
|
|
21792
|
-
*
|
|
21793
|
-
*
|
|
21893
|
+
* Whether the Host credential is usable, after one attempt to re-authorize it.
|
|
21894
|
+
*
|
|
21895
|
+
* Device discovery runs through the local app control route and must not leak a device directory for a
|
|
21896
|
+
* Host the Server no longer accepts, but refusing outright was worse than useless: it told a user who was
|
|
21897
|
+
* already signing in to sign out first. Re-authorizing from the stored Client credential is the move that
|
|
21898
|
+
* helps; when that fails too - the device was removed from the account, so there is nothing left to
|
|
21899
|
+
* refresh - discovery reports nothing and the panel's authorization state takes over.
|
|
21900
|
+
* @returns whether the Host credential is usable.
|
|
21794
21901
|
*/
|
|
21795
|
-
|
|
21902
|
+
async recoverHostAuthorization() {
|
|
21796
21903
|
const status2 = this.host?.hostStatus();
|
|
21797
|
-
|
|
21798
|
-
const message = status2.error === "DEVICE_REVOKED" ? "The local Host was revoked on the Server. Sign out and authorize this Host again." : "The local Host authorization is no longer valid. Sign out and authorize this Host again.";
|
|
21799
|
-
throw new ClientModeError(status2.error, message);
|
|
21904
|
+
return status2 === void 0 || status2.error === void 0 || !HOST_AUTHORIZATION_ERRORS.has(status2.error);
|
|
21800
21905
|
}
|
|
21801
21906
|
async authorizeClientWithAccount(email, password) {
|
|
21802
21907
|
let authorization;
|
|
@@ -21804,11 +21909,10 @@ var ClientModeRuntime = class {
|
|
|
21804
21909
|
authorization = await this.server.authorizeWithAccount(this.requireIdentity(), email, password);
|
|
21805
21910
|
} catch (error) {
|
|
21806
21911
|
if (!(error instanceof ServerApiError) || error.code !== "DEVICE_REVOKED") throw error;
|
|
21807
|
-
|
|
21808
|
-
this.server.bindIdentity(
|
|
21809
|
-
authorization = await this.server.authorizeWithAccount(
|
|
21912
|
+
const identity = this.requireIdentity();
|
|
21913
|
+
this.server.bindIdentity(identity);
|
|
21914
|
+
authorization = await this.server.authorizeWithAccount(identity, email, password);
|
|
21810
21915
|
}
|
|
21811
|
-
await this.authorizeHostByDefault();
|
|
21812
21916
|
this.logger.info("Client account authorized");
|
|
21813
21917
|
return authorization;
|
|
21814
21918
|
}
|
|
@@ -21817,13 +21921,11 @@ var ClientModeRuntime = class {
|
|
|
21817
21921
|
}
|
|
21818
21922
|
async pollClientOAuthQrLogin(qrId) {
|
|
21819
21923
|
const result = await this.server.pollOAuthQrLogin(this.requireIdentity(), qrId, async () => {
|
|
21820
|
-
|
|
21821
|
-
this.server.bindIdentity(
|
|
21822
|
-
|
|
21823
|
-
return this.identity;
|
|
21924
|
+
const identity = this.requireIdentity();
|
|
21925
|
+
this.server.bindIdentity(identity);
|
|
21926
|
+
return identity;
|
|
21824
21927
|
});
|
|
21825
21928
|
if (result.status === "complete") this.logger.info("Client account authorized with QR login");
|
|
21826
|
-
if (result.status === "complete") await this.authorizeHostByDefault();
|
|
21827
21929
|
return result;
|
|
21828
21930
|
}
|
|
21829
21931
|
/**
|
|
@@ -21846,6 +21948,7 @@ var ClientModeRuntime = class {
|
|
|
21846
21948
|
this.connected = void 0;
|
|
21847
21949
|
this.connectionProgress = void 0;
|
|
21848
21950
|
this.pendingWorkspaceSelection = void 0;
|
|
21951
|
+
this.closeBrowsing();
|
|
21849
21952
|
await this.closeCodexVirtual();
|
|
21850
21953
|
this.proxySwitch?.selectLocal();
|
|
21851
21954
|
await this.closePreview();
|
|
@@ -21855,14 +21958,39 @@ var ClientModeRuntime = class {
|
|
|
21855
21958
|
this.remoteReconnectRun += 1;
|
|
21856
21959
|
await this.server.clearAuthorization();
|
|
21857
21960
|
}
|
|
21961
|
+
/**
|
|
21962
|
+
* Revoke this device on the Server, then drop the local authorization.
|
|
21963
|
+
*
|
|
21964
|
+
* Used by the Remote panel's removal action. The device deliberately keeps its identity: signing in
|
|
21965
|
+
* again re-registers the same installation instead of adding another device row, which is what stops
|
|
21966
|
+
* one machine from looking like several over time.
|
|
21967
|
+
*/
|
|
21968
|
+
async revokeCurrentDevice() {
|
|
21969
|
+
await this.server.revokeCurrentDevice();
|
|
21970
|
+
this.connected = void 0;
|
|
21971
|
+
this.connectionProgress = void 0;
|
|
21972
|
+
this.closeBrowsing();
|
|
21973
|
+
this.reconnecting = void 0;
|
|
21974
|
+
this.fellBackToLocal = false;
|
|
21975
|
+
this.remoteReconnectRun += 1;
|
|
21976
|
+
}
|
|
21977
|
+
/**
|
|
21978
|
+
* Switch whether this machine accepts control.
|
|
21979
|
+
*
|
|
21980
|
+
* Turning it off takes the Host connection away and tells the Server to refuse a new one, while the
|
|
21981
|
+
* account sign-in, the device identity and the client half stay exactly as they are - so the device
|
|
21982
|
+
* remains signed in and can still control other machines, and turning control back on reconnects
|
|
21983
|
+
* without another sign-in.
|
|
21984
|
+
*/
|
|
21985
|
+
/** Re-read this device's credentials from disk, so a sign-in is picked up without a restart. */
|
|
21986
|
+
reloadCredentials() {
|
|
21987
|
+
this.server.reloadCredentials();
|
|
21988
|
+
}
|
|
21858
21989
|
async setHostAuthorization(enabled) {
|
|
21859
21990
|
if (this.host === void 0) throw new ClientModeError("METHOD_NOT_ALLOWED", "This plugin is not running as a Host.");
|
|
21860
|
-
|
|
21861
|
-
|
|
21862
|
-
|
|
21863
|
-
}
|
|
21864
|
-
const credentials = await this.server.authenticate(this.requireIdentity());
|
|
21865
|
-
await this.host.authorizeHostAsOwned(credentials.accessToken, credentials.account);
|
|
21991
|
+
await this.host.setHostControl?.(enabled);
|
|
21992
|
+
if (enabled) await this.host.resumeHostConnection?.();
|
|
21993
|
+
else await this.host.pauseHostConnection?.();
|
|
21866
21994
|
return this.status();
|
|
21867
21995
|
}
|
|
21868
21996
|
async setMode(mode, targetDeviceId, signal) {
|
|
@@ -21876,6 +22004,7 @@ var ClientModeRuntime = class {
|
|
|
21876
22004
|
this.connectionProgress = void 0;
|
|
21877
22005
|
this.pendingWorkspaceSelection = void 0;
|
|
21878
22006
|
this.lastWorkspaceSelection = void 0;
|
|
22007
|
+
this.closeBrowsing();
|
|
21879
22008
|
await this.closeCodexStreams(previous2?.client);
|
|
21880
22009
|
await previous2?.client.close().catch(() => void 0);
|
|
21881
22010
|
this.fellBackToLocal = false;
|
|
@@ -21888,7 +22017,9 @@ var ClientModeRuntime = class {
|
|
|
21888
22017
|
if (targetDeviceId === void 0 || targetDeviceId.length === 0) {
|
|
21889
22018
|
throw new ClientModeError("INVALID_MESSAGE", "A targetDeviceId is required for remote mode.");
|
|
21890
22019
|
}
|
|
21891
|
-
const
|
|
22020
|
+
const browsingHere = this.browsing?.target.deviceId === targetDeviceId ? this.browsing : void 0;
|
|
22021
|
+
if (browsingHere !== void 0) this.browsing = void 0;
|
|
22022
|
+
const next = browsingHere ?? await this.connectWithReason(targetDeviceId, signal);
|
|
21892
22023
|
try {
|
|
21893
22024
|
this.assertRemoteCompatible(next);
|
|
21894
22025
|
} catch (error) {
|
|
@@ -21903,6 +22034,7 @@ var ClientModeRuntime = class {
|
|
|
21903
22034
|
this.pendingWorkspaceSelection = void 0;
|
|
21904
22035
|
await this.closeCodexVirtual();
|
|
21905
22036
|
this.selectRemoteTarget(next);
|
|
22037
|
+
this.closeBrowsing();
|
|
21906
22038
|
this.fellBackToLocal = false;
|
|
21907
22039
|
this.reconnecting = void 0;
|
|
21908
22040
|
await this.rememberTarget({ mode: "remote", hostDeviceId: next.target.deviceId, hostName: next.target.name });
|
|
@@ -21923,7 +22055,7 @@ var ClientModeRuntime = class {
|
|
|
21923
22055
|
const previous = this.connected;
|
|
21924
22056
|
this.supersededClient = previous?.client;
|
|
21925
22057
|
try {
|
|
21926
|
-
const next = await this.
|
|
22058
|
+
const next = await this.connectWithReason(targetDeviceId, void 0, forceRelay);
|
|
21927
22059
|
if (previous === void 0) {
|
|
21928
22060
|
this.supersededClient = void 0;
|
|
21929
22061
|
await next.client.close().catch(() => void 0);
|
|
@@ -21936,13 +22068,12 @@ var ClientModeRuntime = class {
|
|
|
21936
22068
|
await this.closeCodexStreams(previous.client);
|
|
21937
22069
|
await previous.client.close().catch(() => void 0);
|
|
21938
22070
|
this.supersededClient = void 0;
|
|
21939
|
-
this.finishReconnect("link re-established");
|
|
21940
22071
|
return true;
|
|
21941
22072
|
} catch (error) {
|
|
21942
22073
|
this.supersededClient = void 0;
|
|
21943
22074
|
this.logger.warn("fast reconnect attempt failed", {
|
|
21944
22075
|
targetDeviceId: shortId2(targetDeviceId),
|
|
21945
|
-
code:
|
|
22076
|
+
code: failureCode(error),
|
|
21946
22077
|
...connectionFailureDetail(error)
|
|
21947
22078
|
});
|
|
21948
22079
|
return false;
|
|
@@ -21951,6 +22082,83 @@ var ClientModeRuntime = class {
|
|
|
21951
22082
|
rememberWorkspaceSelection(selection) {
|
|
21952
22083
|
this.pendingWorkspaceSelection = selection;
|
|
21953
22084
|
this.lastWorkspaceSelection = { ...selection };
|
|
22085
|
+
const connected = this.connected;
|
|
22086
|
+
if (connected?.target.deviceId === selection.targetDeviceId) {
|
|
22087
|
+
void this.rememberTarget({
|
|
22088
|
+
mode: "remote",
|
|
22089
|
+
hostDeviceId: selection.targetDeviceId,
|
|
22090
|
+
hostName: connected.target.name
|
|
22091
|
+
});
|
|
22092
|
+
}
|
|
22093
|
+
}
|
|
22094
|
+
/**
|
|
22095
|
+
* What the user is watching, reduced to the only thing a recovery has to restore.
|
|
22096
|
+
*
|
|
22097
|
+
* A lost session is always "this device, this kind of workspace, this directory". Deriving that once -
|
|
22098
|
+
* instead of letting each recovery path re-read `lastWorkspaceSelection` and decide for itself - is what
|
|
22099
|
+
* keeps a Codex session from being restored onto the peer's DeepSeek carrier, which is exactly the bug the
|
|
22100
|
+
* reconnect used to produce.
|
|
22101
|
+
* @param targetDeviceId - the Host the lost session belonged to.
|
|
22102
|
+
* @returns the target, or undefined when nothing on that Host is being watched.
|
|
22103
|
+
*/
|
|
22104
|
+
sessionTargetFor(targetDeviceId) {
|
|
22105
|
+
const selection = this.lastWorkspaceSelection;
|
|
22106
|
+
if (selection === void 0 || selection.targetDeviceId !== targetDeviceId) return void 0;
|
|
22107
|
+
return {
|
|
22108
|
+
deviceId: targetDeviceId,
|
|
22109
|
+
kind: selection.backend === "codex" ? "codex" : "harness",
|
|
22110
|
+
workspaceId: selection.workspaceId,
|
|
22111
|
+
...selection.workspacePath === void 0 ? {} : { workspacePath: selection.workspacePath },
|
|
22112
|
+
...selection.sessionId === void 0 ? {} : { sessionId: selection.sessionId }
|
|
22113
|
+
};
|
|
22114
|
+
}
|
|
22115
|
+
/**
|
|
22116
|
+
* Put the session the user was watching back in place on the current link.
|
|
22117
|
+
*
|
|
22118
|
+
* The one step every recovery path ends with, whichever way the link came back: the retry schedule, the
|
|
22119
|
+
* quick window, or a boot restore. A Codex target rebuilds the Codex carrier - never the peer's Harness
|
|
22120
|
+
* workspace - and a Harness target republishes its workspace selection so the native UI re-reads its list.
|
|
22121
|
+
* @param target - the session to restore.
|
|
22122
|
+
* @param fallbackToLocal - whether a failed Codex rebuild may hand the view to the local shell. The quick
|
|
22123
|
+
* window passes false: its whole point is to keep the user looking at the session while it is rescued.
|
|
22124
|
+
* @returns true when the user is looking at that session again.
|
|
22125
|
+
*/
|
|
22126
|
+
async restoreSession(target2, options = {}) {
|
|
22127
|
+
if (target2.kind === "codex") {
|
|
22128
|
+
const restored = await this.restoreCodexCarrier(target2, options.fallbackToLocal !== false);
|
|
22129
|
+
if (restored) {
|
|
22130
|
+
this.logger.info("remote Codex session restored", {
|
|
22131
|
+
targetDeviceId: shortId2(target2.deviceId),
|
|
22132
|
+
kind: "codex",
|
|
22133
|
+
path: target2.workspacePath ?? "(unknown)"
|
|
22134
|
+
});
|
|
22135
|
+
}
|
|
22136
|
+
return restored;
|
|
22137
|
+
}
|
|
22138
|
+
this.restoreWorkspaceSelection(target2.deviceId);
|
|
22139
|
+
this.logger.info("remote DSH session restored", {
|
|
22140
|
+
targetDeviceId: shortId2(target2.deviceId),
|
|
22141
|
+
kind: "harness"
|
|
22142
|
+
});
|
|
22143
|
+
return true;
|
|
22144
|
+
}
|
|
22145
|
+
/**
|
|
22146
|
+
* Clear the recovery flag once the user is back on the session they were watching.
|
|
22147
|
+
*
|
|
22148
|
+
* The header reads this flag to show "reconnecting", so it must be cleared exactly when the recovery
|
|
22149
|
+
* succeeded - not when a transport happened to come back, and not while a Codex workspace is still missing.
|
|
22150
|
+
* @param targetDeviceId - the Host the recovery was about.
|
|
22151
|
+
* @param reason - what brought the session back, for the log.
|
|
22152
|
+
* @param kind - which carrier was restored.
|
|
22153
|
+
*/
|
|
22154
|
+
finishRecovery(targetDeviceId, reason, kind) {
|
|
22155
|
+
this.reconnecting = void 0;
|
|
22156
|
+
this.fellBackToLocal = false;
|
|
22157
|
+
this.logger.info("remote session recovered", {
|
|
22158
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22159
|
+
kind,
|
|
22160
|
+
reason
|
|
22161
|
+
});
|
|
21954
22162
|
}
|
|
21955
22163
|
/**
|
|
21956
22164
|
* Republish the workspace selection so the native UI re-reads its remote session list.
|
|
@@ -21968,12 +22176,110 @@ var ClientModeRuntime = class {
|
|
|
21968
22176
|
workspaceId: selection.workspaceId
|
|
21969
22177
|
});
|
|
21970
22178
|
}
|
|
21971
|
-
|
|
21972
|
-
|
|
21973
|
-
|
|
21974
|
-
|
|
21975
|
-
|
|
21976
|
-
|
|
22179
|
+
/**
|
|
22180
|
+
* Put the Codex carrier back onto the link that just replaced the old one.
|
|
22181
|
+
*
|
|
22182
|
+
* A reconnect leaves the carrier holding a transfer channel bound to a transport that no longer exists, and the
|
|
22183
|
+
* first use fails with "remote transport closed" while the UI still looks connected - the reported symptom:
|
|
22184
|
+
* Codex sessions vanish and clicking one errors. Rebuilding the carrier is what lets an open Codex workspace
|
|
22185
|
+
* survive a reconnect.
|
|
22186
|
+
*
|
|
22187
|
+
* The workspace is matched by directory, not by id: a Host that replaced its Codex domain renumbers the very
|
|
22188
|
+
* same directories, so an id that misses is retried once by root before the workspace is given up on.
|
|
22189
|
+
* @param target - the Codex session to restore.
|
|
22190
|
+
* @param fallbackToLocal - whether a failure may hand the view to the client's own shell. The quick window
|
|
22191
|
+
* passes false so the user keeps looking at the session for the whole window.
|
|
22192
|
+
* @returns true when the Codex carrier is back on the given workspace.
|
|
22193
|
+
*/
|
|
22194
|
+
async restoreCodexCarrier(target2, fallbackToLocal = true) {
|
|
22195
|
+
try {
|
|
22196
|
+
try {
|
|
22197
|
+
await this.openCodexWorkspace(target2.deviceId, target2.workspaceId);
|
|
22198
|
+
} catch (error) {
|
|
22199
|
+
const root = target2.workspacePath;
|
|
22200
|
+
if (root === void 0) throw error;
|
|
22201
|
+
const recovered = await this.codexWorkspaceIdForPath(target2.deviceId, root);
|
|
22202
|
+
if (recovered === void 0) throw error;
|
|
22203
|
+
if (recovered !== target2.workspaceId) {
|
|
22204
|
+
this.logger.info("CodeX workspace restored under a new id", {
|
|
22205
|
+
targetDeviceId: shortId2(target2.deviceId),
|
|
22206
|
+
path: root
|
|
22207
|
+
});
|
|
22208
|
+
}
|
|
22209
|
+
await this.openCodexWorkspace(target2.deviceId, recovered);
|
|
22210
|
+
}
|
|
22211
|
+
this.restoreWorkspaceSelection(target2.deviceId);
|
|
22212
|
+
return true;
|
|
22213
|
+
} catch (error) {
|
|
22214
|
+
this.logger.warn("CodeX workspace could not be restored", {
|
|
22215
|
+
targetDeviceId: shortId2(target2.deviceId),
|
|
22216
|
+
code: failureCode(error),
|
|
22217
|
+
path: target2.workspacePath ?? "(unknown)",
|
|
22218
|
+
...connectionFailureDetail(error)
|
|
22219
|
+
});
|
|
22220
|
+
if (fallbackToLocal) await this.fallBackToLocalForCodex();
|
|
22221
|
+
return false;
|
|
22222
|
+
}
|
|
22223
|
+
}
|
|
22224
|
+
/**
|
|
22225
|
+
* The id the Host currently uses for a workspace root.
|
|
22226
|
+
*
|
|
22227
|
+
* @param targetDeviceId - the Host to ask.
|
|
22228
|
+
* @param path - the workspace root as it was remembered.
|
|
22229
|
+
* @returns the current workspace id, or undefined when the Host no longer offers that root.
|
|
22230
|
+
*/
|
|
22231
|
+
async codexWorkspaceIdForPath(targetDeviceId, path) {
|
|
22232
|
+
const workspaces = await this.listCodexWorkspaces(targetDeviceId);
|
|
22233
|
+
const match = workspaces.find((candidate) => sameWorkspaceRoot(candidate.path, path));
|
|
22234
|
+
return match?.workspaceId;
|
|
22235
|
+
}
|
|
22236
|
+
/**
|
|
22237
|
+
* Show the client's own shell while a Codex session is unavailable.
|
|
22238
|
+
*
|
|
22239
|
+
* The peer's other carrier must not stand in for the Codex workspace that was lost, and this deliberately does not
|
|
22240
|
+
* touch the reconnect run counter: the retry schedule keeps trying to restore the Codex workspace while the user
|
|
22241
|
+
* works locally.
|
|
22242
|
+
* @returns nothing.
|
|
22243
|
+
*/
|
|
22244
|
+
async fallBackToLocalForCodex() {
|
|
22245
|
+
await this.closeCodexVirtual();
|
|
22246
|
+
this.proxySwitch?.selectLocal();
|
|
22247
|
+
this.gatewaySwitch.selectLocal();
|
|
22248
|
+
this.fellBackToLocal = true;
|
|
22249
|
+
}
|
|
22250
|
+
/**
|
|
22251
|
+
* The Codex session is gone while the transport is still up (the Host replaced or switched off its Codex
|
|
22252
|
+
* domain). Deliberately the same entry point as a lost link, so both carriers take one path: quick reconnect,
|
|
22253
|
+
* local fallback and the retry schedule.
|
|
22254
|
+
*
|
|
22255
|
+
* The trigger is logged, because "the banner did not appear" is otherwise indistinguishable between "no
|
|
22256
|
+
* signal arrived" and "the signal arrived but the UI did not show it".
|
|
22257
|
+
* @param trigger - what noticed it.
|
|
22258
|
+
* @returns nothing; the reconnect runs in the background.
|
|
22259
|
+
*/
|
|
22260
|
+
noteCodexSessionLost(trigger) {
|
|
22261
|
+
const connected = this.connected;
|
|
22262
|
+
if (connected === void 0 || this.reconnecting !== void 0) {
|
|
22263
|
+
this.logger.info("Codex session loss ignored", {
|
|
22264
|
+
targetDeviceId: connected === void 0 ? void 0 : shortId2(connected.target.deviceId),
|
|
22265
|
+
trigger,
|
|
22266
|
+
reason: connected === void 0 ? "not-connected" : "recovery-already-running"
|
|
22267
|
+
});
|
|
22268
|
+
return;
|
|
22269
|
+
}
|
|
22270
|
+
if (this.sessionTargetFor(connected.target.deviceId)?.kind !== "codex") {
|
|
22271
|
+
this.logger.info("Codex session loss ignored", {
|
|
22272
|
+
targetDeviceId: shortId2(connected.target.deviceId),
|
|
22273
|
+
trigger,
|
|
22274
|
+
reason: "watching-another-session"
|
|
22275
|
+
});
|
|
22276
|
+
return;
|
|
22277
|
+
}
|
|
22278
|
+
this.logger.warn("remote Codex session lost", {
|
|
22279
|
+
targetDeviceId: shortId2(connected.target.deviceId),
|
|
22280
|
+
trigger
|
|
22281
|
+
});
|
|
22282
|
+
void this.handleRemoteTransportLost(connected.client, connected.target.deviceId, "codex-closed");
|
|
21977
22283
|
}
|
|
21978
22284
|
/**
|
|
21979
22285
|
* Check the live session now, outside the cadence.
|
|
@@ -21986,34 +22292,56 @@ var ClientModeRuntime = class {
|
|
|
21986
22292
|
return this.status();
|
|
21987
22293
|
}
|
|
21988
22294
|
/**
|
|
21989
|
-
*
|
|
22295
|
+
* Re-read the connected Host's capabilities.
|
|
21990
22296
|
*
|
|
21991
|
-
*
|
|
21992
|
-
*
|
|
21993
|
-
*
|
|
21994
|
-
* @
|
|
21995
|
-
* @returns whether a session is in place again.
|
|
22297
|
+
* A Host that switches Codex (or the terminal) on or off changes what it advertises, and the cached feature
|
|
22298
|
+
* set used to be refreshed only when a workspace list happened to be requested - which is why the Remote card
|
|
22299
|
+
* could describe a Host it no longer matched. One round trip, no business payload, so the card can poll it.
|
|
22300
|
+
* @returns the connected Host's fresh feature set, or `{ connected: false }`.
|
|
21996
22301
|
*/
|
|
21997
|
-
async
|
|
21998
|
-
|
|
21999
|
-
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
|
|
22003
|
-
|
|
22004
|
-
|
|
22005
|
-
|
|
22006
|
-
|
|
22007
|
-
if (await this.reestablish(targetDeviceId, QUICK_RECONNECT_FORCE_RELAY)) return true;
|
|
22008
|
-
if (attempt === QUICK_RECONNECT_ATTEMPTS) break;
|
|
22009
|
-
const wait2 = Math.min(QUICK_RECONNECT_RETRY_DELAY_MS, Math.max(0, deadline - Date.now()));
|
|
22010
|
-
if (wait2 === 0) break;
|
|
22011
|
-
await new Promise((resolve4) => {
|
|
22012
|
-
setTimeout(resolve4, wait2);
|
|
22302
|
+
async probeRemoteFeatures() {
|
|
22303
|
+
const remote = this.connected;
|
|
22304
|
+
if (remote === void 0) return { connected: false };
|
|
22305
|
+
try {
|
|
22306
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
22307
|
+
} catch (error) {
|
|
22308
|
+
const code = failureCode(error);
|
|
22309
|
+
this.logger.warn("remote capability probe failed", {
|
|
22310
|
+
targetDeviceId: shortId2(remote.target.deviceId),
|
|
22311
|
+
code
|
|
22013
22312
|
});
|
|
22313
|
+
return { connected: true, targetDeviceId: remote.target.deviceId, features: remote.features, error: code };
|
|
22014
22314
|
}
|
|
22015
|
-
|
|
22016
|
-
|
|
22315
|
+
return { connected: true, targetDeviceId: remote.target.deviceId, features: remote.features };
|
|
22316
|
+
}
|
|
22317
|
+
/**
|
|
22318
|
+
* Re-establish the session the user is watching, without changing which session that is.
|
|
22319
|
+
*
|
|
22320
|
+
* What the Remote card's Refresh does for a live session: the transport may have died, the peer's Codex may
|
|
22321
|
+
* have come back, or a previous recovery may have given up. This never selects another Host and never another
|
|
22322
|
+
* kind of workspace - the remembered selection is the only thing it works from.
|
|
22323
|
+
* @returns the status after the attempt.
|
|
22324
|
+
*/
|
|
22325
|
+
async recoverCurrentSession() {
|
|
22326
|
+
const selection = this.lastWorkspaceSelection;
|
|
22327
|
+
if (selection === void 0) return this.status();
|
|
22328
|
+
if (this.reconnecting !== void 0) return this.status();
|
|
22329
|
+
const target2 = this.sessionTargetFor(selection.targetDeviceId);
|
|
22330
|
+
const connected = this.connected;
|
|
22331
|
+
if (connected === void 0) {
|
|
22332
|
+
this.reconnecting = { targetDeviceId: selection.targetDeviceId, phase: "fallback" };
|
|
22333
|
+
void this.reconnectRemoteSession(selection.targetDeviceId, target2);
|
|
22334
|
+
return this.status();
|
|
22335
|
+
}
|
|
22336
|
+
if (connected.target.deviceId !== selection.targetDeviceId) return this.status();
|
|
22337
|
+
const probe = await this.probeRemoteFeatures();
|
|
22338
|
+
if (probe.connected === true && probe.error === void 0) {
|
|
22339
|
+
if (target2 !== void 0) await this.restoreSession(target2);
|
|
22340
|
+
return this.status();
|
|
22341
|
+
}
|
|
22342
|
+
if (probe.error === void 0) return this.status();
|
|
22343
|
+
await this.handleRemoteTransportLost(connected.client, connected.target.deviceId, "manual");
|
|
22344
|
+
return this.status();
|
|
22017
22345
|
}
|
|
22018
22346
|
/**
|
|
22019
22347
|
* Shared cleanup for a session whose transport is gone.
|
|
@@ -22025,83 +22353,255 @@ var ClientModeRuntime = class {
|
|
|
22025
22353
|
* @param client - the client that was connected.
|
|
22026
22354
|
* @param targetDeviceId - the Host it was bound to.
|
|
22027
22355
|
*/
|
|
22356
|
+
/**
|
|
22357
|
+
* A user's explicit workspace choice supersedes a recovery that is still in flight.
|
|
22358
|
+
*
|
|
22359
|
+
* The retry schedule belongs to the session the user was watching. When they pick another workspace, that
|
|
22360
|
+
* schedule must not land later and move the view again: the reported failure was a Codex ladder restoring its
|
|
22361
|
+
* own workspace over the DSH workspace the user had just opened - "the user's own choice is the only thing
|
|
22362
|
+
* that changes which session is on screen" was broken by a background timer. Only the panel's open endpoints
|
|
22363
|
+
* call this; the recovery restores through the internal open paths, so it never cancels itself.
|
|
22364
|
+
*/
|
|
22365
|
+
supersedeRemoteRecovery() {
|
|
22366
|
+
if (this.reconnecting === void 0 && !this.fellBackToLocal) return;
|
|
22367
|
+
this.logger.info("a workspace was opened; stopping the recovery in flight", {
|
|
22368
|
+
phase: this.reconnecting?.phase ?? "fallback"
|
|
22369
|
+
});
|
|
22370
|
+
this.reconnecting = void 0;
|
|
22371
|
+
this.fellBackToLocal = false;
|
|
22372
|
+
this.remoteReconnectRun += 1;
|
|
22373
|
+
}
|
|
22374
|
+
/**
|
|
22375
|
+
* Whether the user still wants the session a recovery is putting back.
|
|
22376
|
+
*
|
|
22377
|
+
* The user's own choice is the record, not a counter: cancelling clears `lastWorkspaceSelection` (and writes
|
|
22378
|
+
* `local` to the target file), and opening another workspace points the selection somewhere else. A recovery is
|
|
22379
|
+
* work the user asked for *earlier*, so it may only adopt its result while that is still what they want. The
|
|
22380
|
+
* reported failure was an attempt that landed after the answer - it reconnected and put the session back on
|
|
22381
|
+
* screen, because nothing re-read the user's intent once the attempt was in flight.
|
|
22382
|
+
* @param targetDeviceId - the Host the recovery is restoring.
|
|
22383
|
+
* @returns true while the remembered target is still that Host.
|
|
22384
|
+
*/
|
|
22385
|
+
recoveryStillWanted(targetDeviceId) {
|
|
22386
|
+
if (this.reconnecting === void 0) return false;
|
|
22387
|
+
return this.lastWorkspaceSelection?.targetDeviceId === targetDeviceId;
|
|
22388
|
+
}
|
|
22389
|
+
/**
|
|
22390
|
+
* Undo a recovery the user answered while it was landing: their choice wins over work already in flight.
|
|
22391
|
+
* @param targetDeviceId - the Host the recovery was restoring.
|
|
22392
|
+
*/
|
|
22393
|
+
async abandonRecovery(targetDeviceId) {
|
|
22394
|
+
this.logger.info("the user answered while the recovery was landing; returning to the local shell", {
|
|
22395
|
+
targetDeviceId: shortId2(targetDeviceId)
|
|
22396
|
+
});
|
|
22397
|
+
await this.setMode("local");
|
|
22398
|
+
}
|
|
22028
22399
|
async handleRemoteTransportLost(client, targetDeviceId, reason) {
|
|
22400
|
+
const browsing = this.browsing?.client === client ? this.browsing : void 0;
|
|
22401
|
+
if (browsing !== void 0 && this.connected?.client !== client) {
|
|
22402
|
+
this.browsing = void 0;
|
|
22403
|
+
this.clearConnectionProgress(browsing.progressRunId);
|
|
22404
|
+
this.logger.info("browsing connection closed; the panel will open a new one", {
|
|
22405
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22406
|
+
reason
|
|
22407
|
+
});
|
|
22408
|
+
void browsing.client.close().catch(() => void 0);
|
|
22409
|
+
return;
|
|
22410
|
+
}
|
|
22029
22411
|
if (this.connected?.client !== client) return;
|
|
22030
22412
|
this.supersededClient = void 0;
|
|
22031
22413
|
const targetName = this.connected.target.name;
|
|
22032
|
-
|
|
22033
|
-
|
|
22034
|
-
|
|
22414
|
+
const target2 = this.sessionTargetFor(targetDeviceId);
|
|
22415
|
+
const kind = target2?.kind ?? "harness";
|
|
22416
|
+
if (target2 === void 0) {
|
|
22417
|
+
this.logger.info("connection closed with no remote session to recover; staying local", {
|
|
22418
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22419
|
+
reason
|
|
22035
22420
|
});
|
|
22421
|
+
const previous = this.connected;
|
|
22422
|
+
this.connected = void 0;
|
|
22423
|
+
this.connectionProgress = void 0;
|
|
22424
|
+
await this.closeCodexStreams(previous?.client);
|
|
22425
|
+
await previous?.client.close().catch(() => void 0);
|
|
22036
22426
|
return;
|
|
22037
22427
|
}
|
|
22428
|
+
const transportLost = reason !== "codex-closed";
|
|
22429
|
+
if (this.reconnecting === void 0) {
|
|
22430
|
+
const run = this.remoteReconnectRun;
|
|
22431
|
+
this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fast" };
|
|
22432
|
+
this.logger.warn("remote session lost; trying the quick window", {
|
|
22433
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22434
|
+
kind,
|
|
22435
|
+
reason,
|
|
22436
|
+
attempts: QUICK_RECONNECT_ATTEMPTS,
|
|
22437
|
+
windowMs: QUICK_RECONNECT_WINDOW_MS
|
|
22438
|
+
});
|
|
22439
|
+
const rescued = await this.quickReconnectSession(targetDeviceId, target2, transportLost, run);
|
|
22440
|
+
if (run !== this.remoteReconnectRun) {
|
|
22441
|
+
this.logger.info("the user answered during the quick window; the recovery stops here", {
|
|
22442
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22443
|
+
kind
|
|
22444
|
+
});
|
|
22445
|
+
return;
|
|
22446
|
+
}
|
|
22447
|
+
if (rescued) {
|
|
22448
|
+
this.finishRecovery(targetDeviceId, "quick-window", kind);
|
|
22449
|
+
this.logger.info("remote session kept its view through a quick reconnect", {
|
|
22450
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22451
|
+
kind
|
|
22452
|
+
});
|
|
22453
|
+
return;
|
|
22454
|
+
}
|
|
22455
|
+
}
|
|
22456
|
+
await this.fallBackToLocalShell(targetDeviceId, targetName, reason, kind, transportLost);
|
|
22457
|
+
void this.reconnectRemoteSession(targetDeviceId, target2);
|
|
22458
|
+
}
|
|
22459
|
+
/**
|
|
22460
|
+
* The quick window: rescue the same session without disturbing the view.
|
|
22461
|
+
*
|
|
22462
|
+
* At most {@link QUICK_RECONNECT_ATTEMPTS} attempts inside {@link QUICK_RECONNECT_WINDOW_MS}, spaced by
|
|
22463
|
+
* {@link QUICK_RECONNECT_RETRY_DELAY_MS}. Nothing here falls back to the local shell or republishes a
|
|
22464
|
+
* workspace - the user keeps looking at the session they were in, and the caller decides what happens once the
|
|
22465
|
+
* window is spent. A lost transport is rebuilt first, over relay only (the one path measured under five
|
|
22466
|
+
* seconds); a Codex-only loss already has its link, so its attempts only rebuild the carrier.
|
|
22467
|
+
* @param targetDeviceId - the Host the session was bound to.
|
|
22468
|
+
* @param target - the session to put back.
|
|
22469
|
+
* @param transportLost - whether the link itself has to be rebuilt.
|
|
22470
|
+
* @param run - the recovery run this window belongs to; the user answering bumps it.
|
|
22471
|
+
* @returns true when the session is on screen again.
|
|
22472
|
+
*/
|
|
22473
|
+
async quickReconnectSession(targetDeviceId, target2, transportLost, run) {
|
|
22474
|
+
let linkUp = !transportLost;
|
|
22475
|
+
const deadline = Date.now() + QUICK_RECONNECT_WINDOW_MS;
|
|
22476
|
+
for (let attempt = 1; attempt <= QUICK_RECONNECT_ATTEMPTS; attempt += 1) {
|
|
22477
|
+
if (!this.recoveryStillWanted(targetDeviceId) || run !== this.remoteReconnectRun) return false;
|
|
22478
|
+
if (!linkUp) linkUp = await this.reestablish(targetDeviceId, QUICK_RECONNECT_FORCE_RELAY);
|
|
22479
|
+
if (!this.recoveryStillWanted(targetDeviceId) || run !== this.remoteReconnectRun) return false;
|
|
22480
|
+
if (linkUp && await this.restoreSession(target2, { fallbackToLocal: false })) {
|
|
22481
|
+
if (!this.recoveryStillWanted(targetDeviceId) || run !== this.remoteReconnectRun) {
|
|
22482
|
+
await this.abandonRecovery(targetDeviceId);
|
|
22483
|
+
return false;
|
|
22484
|
+
}
|
|
22485
|
+
return true;
|
|
22486
|
+
}
|
|
22487
|
+
if (attempt === QUICK_RECONNECT_ATTEMPTS) break;
|
|
22488
|
+
const wait2 = Math.min(QUICK_RECONNECT_RETRY_DELAY_MS, Math.max(0, deadline - Date.now()));
|
|
22489
|
+
if (wait2 === 0) break;
|
|
22490
|
+
await sleep3(wait2);
|
|
22491
|
+
}
|
|
22492
|
+
if (!this.recoveryStillWanted(targetDeviceId) || run !== this.remoteReconnectRun) return false;
|
|
22493
|
+
const remaining = deadline - Date.now();
|
|
22494
|
+
if (remaining > 0) await sleep3(remaining);
|
|
22495
|
+
this.reconnecting = void 0;
|
|
22496
|
+
return false;
|
|
22497
|
+
}
|
|
22498
|
+
/**
|
|
22499
|
+
* Hand the session back to the client's own shell.
|
|
22500
|
+
*
|
|
22501
|
+
* Phase two of the ladder, and the same for every kind of loss. A link that broke is closed here; one that is
|
|
22502
|
+
* still up (a Codex-only loss) is kept, because the retry schedule needs it and dropping a healthy link only to
|
|
22503
|
+
* open it again five seconds later is churn the user pays for. Switching the carriers to local is what makes
|
|
22504
|
+
* the view fall back - the point of this step.
|
|
22505
|
+
* @param targetDeviceId - the Host the session was bound to.
|
|
22506
|
+
* @param targetName - its display name, kept for the header while the reconnect runs.
|
|
22507
|
+
* @param reason - why the session was lost, for the log.
|
|
22508
|
+
* @param kind - which carrier was lost.
|
|
22509
|
+
* @param transportLost - whether the link itself is gone.
|
|
22510
|
+
*/
|
|
22511
|
+
async fallBackToLocalShell(targetDeviceId, targetName, reason, kind, transportLost) {
|
|
22038
22512
|
void this.closePreview();
|
|
22039
|
-
this.connected = void 0;
|
|
22040
22513
|
this.connectionProgress = void 0;
|
|
22041
22514
|
this.pendingWorkspaceSelection = void 0;
|
|
22042
|
-
|
|
22515
|
+
await this.closeCodexVirtual();
|
|
22043
22516
|
this.proxySwitch?.selectLocal();
|
|
22044
22517
|
this.gatewaySwitch.selectLocal();
|
|
22045
22518
|
this.fellBackToLocal = true;
|
|
22046
22519
|
this.reconnecting = { targetDeviceId, ...targetName === void 0 ? {} : { targetName }, phase: "fallback" };
|
|
22047
|
-
|
|
22048
|
-
|
|
22520
|
+
if (transportLost) {
|
|
22521
|
+
const previous = this.connected;
|
|
22522
|
+
this.connected = void 0;
|
|
22523
|
+
await this.closeCodexStreams(previous?.client);
|
|
22524
|
+
void previous?.client.close().catch(() => void 0);
|
|
22525
|
+
}
|
|
22526
|
+
this.logger.warn("remote session lost; falling back to the local shell and retrying", {
|
|
22049
22527
|
targetDeviceId: shortId2(targetDeviceId),
|
|
22528
|
+
kind,
|
|
22050
22529
|
reason
|
|
22051
22530
|
});
|
|
22052
|
-
void this.reconnectRemoteSession(targetDeviceId);
|
|
22053
22531
|
}
|
|
22054
22532
|
/**
|
|
22055
|
-
*
|
|
22533
|
+
* The background retry ladder, shared by every way a session can be recovered.
|
|
22056
22534
|
*
|
|
22057
|
-
*
|
|
22058
|
-
*
|
|
22059
|
-
*
|
|
22060
|
-
*
|
|
22061
|
-
*
|
|
22062
|
-
* @param targetDeviceId - the Host the
|
|
22535
|
+
* It runs after the quick window has been spent (the caller has already fallen back to the local shell), or
|
|
22536
|
+
* straight away for a boot restore, which has no window behind it. Every attempt targets the same session; on
|
|
22537
|
+
* success the workspace and the session data are re-read, and when the ladder is spent the reconnect flag is
|
|
22538
|
+
* cleared so the user is simply local. A loop that is superseded, or one whose session came back another way,
|
|
22539
|
+
* stops quietly.
|
|
22540
|
+
* @param targetDeviceId - the Host the lost session was bound to.
|
|
22541
|
+
* @param target - the session to put back.
|
|
22542
|
+
* @param immediateFirstAttempt - true for a boot restore, which should not wait five seconds to start.
|
|
22063
22543
|
*/
|
|
22064
|
-
async reconnectRemoteSession(targetDeviceId) {
|
|
22544
|
+
async reconnectRemoteSession(targetDeviceId, target2 = this.sessionTargetFor(targetDeviceId), immediateFirstAttempt = false) {
|
|
22065
22545
|
const run = ++this.remoteReconnectRun;
|
|
22066
|
-
const
|
|
22546
|
+
const kind = target2?.kind ?? "harness";
|
|
22547
|
+
if (target2 === void 0) {
|
|
22548
|
+
this.reconnecting = void 0;
|
|
22549
|
+
this.fellBackToLocal = false;
|
|
22550
|
+
this.logger.info("no remote session to restore; staying in the local shell", {
|
|
22551
|
+
targetDeviceId: shortId2(targetDeviceId)
|
|
22552
|
+
});
|
|
22553
|
+
return;
|
|
22554
|
+
}
|
|
22555
|
+
const delays = immediateFirstAttempt ? [0, ...BACKGROUND_RETRY_DELAYS_MS] : BACKGROUND_RETRY_DELAYS_MS;
|
|
22067
22556
|
let attempt = 0;
|
|
22068
22557
|
for (; ; ) {
|
|
22069
|
-
if (attempt
|
|
22558
|
+
if (attempt >= delays.length) {
|
|
22070
22559
|
this.reconnecting = void 0;
|
|
22071
|
-
this.logger.warn("remote
|
|
22560
|
+
this.logger.warn("remote session recovery gave up; staying in the local shell", {
|
|
22072
22561
|
targetDeviceId: shortId2(targetDeviceId),
|
|
22562
|
+
kind,
|
|
22073
22563
|
attempts: attempt
|
|
22074
22564
|
});
|
|
22075
22565
|
return;
|
|
22076
22566
|
}
|
|
22077
|
-
const wait2 = delays[attempt] ??
|
|
22078
|
-
if (wait2 > 0) await
|
|
22079
|
-
setTimeout(resolve4, wait2);
|
|
22080
|
-
});
|
|
22567
|
+
const wait2 = delays[attempt] ?? BACKGROUND_RETRY_DELAYS_MS[BACKGROUND_RETRY_DELAYS_MS.length - 1];
|
|
22568
|
+
if (wait2 > 0) await sleep3(wait2);
|
|
22081
22569
|
attempt += 1;
|
|
22082
22570
|
if (run !== this.remoteReconnectRun) return;
|
|
22083
|
-
|
|
22571
|
+
const reconnecting = this.reconnecting;
|
|
22572
|
+
if (reconnecting === void 0) return;
|
|
22084
22573
|
try {
|
|
22085
|
-
|
|
22086
|
-
|
|
22087
|
-
|
|
22574
|
+
if (this.connected?.target.deviceId !== targetDeviceId) {
|
|
22575
|
+
await this.setMode("remote", targetDeviceId);
|
|
22576
|
+
this.reconnecting = reconnecting;
|
|
22577
|
+
}
|
|
22578
|
+
if (target2 !== void 0 && !await this.restoreSession(target2)) continue;
|
|
22579
|
+
if (!this.recoveryStillWanted(targetDeviceId)) {
|
|
22580
|
+
await this.abandonRecovery(targetDeviceId);
|
|
22581
|
+
return;
|
|
22582
|
+
}
|
|
22583
|
+
this.finishRecovery(targetDeviceId, "retry-schedule", kind);
|
|
22088
22584
|
return;
|
|
22089
22585
|
} catch (error) {
|
|
22090
|
-
const code =
|
|
22091
|
-
if (
|
|
22092
|
-
this.
|
|
22586
|
+
const code = failureCode(error);
|
|
22587
|
+
if (CREDENTIAL_FAILURE_CODES.has(code)) {
|
|
22588
|
+
this.reconnecting = void 0;
|
|
22589
|
+
this.logger.warn("remote session recovery stopped: authorization is required", {
|
|
22093
22590
|
targetDeviceId: shortId2(targetDeviceId),
|
|
22591
|
+
kind,
|
|
22094
22592
|
code
|
|
22095
22593
|
});
|
|
22096
|
-
this.reconnecting = void 0;
|
|
22097
22594
|
return;
|
|
22098
22595
|
}
|
|
22099
22596
|
if (attempt < 3 || attempt % 10 === 0) {
|
|
22100
|
-
|
|
22597
|
+
const detail = connectionFailureDetail(error);
|
|
22598
|
+
const systemCode = typeof detail.systemCode === "string" ? detail.systemCode : void 0;
|
|
22599
|
+
this.logger.warn("remote session recovery attempt failed", {
|
|
22101
22600
|
targetDeviceId: shortId2(targetDeviceId),
|
|
22601
|
+
kind,
|
|
22102
22602
|
attempt,
|
|
22103
|
-
code,
|
|
22104
|
-
...
|
|
22603
|
+
code: systemCode ?? code,
|
|
22604
|
+
...detail
|
|
22105
22605
|
});
|
|
22106
22606
|
}
|
|
22107
22607
|
}
|
|
@@ -22117,10 +22617,19 @@ var ClientModeRuntime = class {
|
|
|
22117
22617
|
*/
|
|
22118
22618
|
async rememberTarget(target2) {
|
|
22119
22619
|
if (this.targetStore === void 0) return;
|
|
22620
|
+
const selection = target2.mode === "remote" && target2.hostDeviceId !== void 0 ? this.sessionTargetFor(target2.hostDeviceId) : void 0;
|
|
22120
22621
|
try {
|
|
22121
22622
|
await this.targetStore.save({
|
|
22122
22623
|
...target2,
|
|
22123
|
-
...this.config.serverUrl === void 0 ? {} : { serverUrl: this.config.serverUrl }
|
|
22624
|
+
...this.config.serverUrl === void 0 ? {} : { serverUrl: this.config.serverUrl },
|
|
22625
|
+
...selection === void 0 ? {} : {
|
|
22626
|
+
workspace: {
|
|
22627
|
+
workspaceId: selection.workspaceId,
|
|
22628
|
+
kind: selection.kind,
|
|
22629
|
+
...selection.workspacePath === void 0 ? {} : { workspacePath: selection.workspacePath },
|
|
22630
|
+
...selection.sessionId === void 0 ? {} : { sessionId: selection.sessionId }
|
|
22631
|
+
}
|
|
22632
|
+
}
|
|
22124
22633
|
});
|
|
22125
22634
|
} catch (error) {
|
|
22126
22635
|
this.logger.warn("could not record the remote target", { code: safeErrorCode(error) });
|
|
@@ -22146,19 +22655,33 @@ var ClientModeRuntime = class {
|
|
|
22146
22655
|
});
|
|
22147
22656
|
return false;
|
|
22148
22657
|
}
|
|
22658
|
+
if (record7.workspace === void 0) {
|
|
22659
|
+
this.logger.info("the recorded target has no workspace; staying in the local shell", {
|
|
22660
|
+
targetDeviceId: shortId2(record7.hostDeviceId)
|
|
22661
|
+
});
|
|
22662
|
+
return false;
|
|
22663
|
+
}
|
|
22149
22664
|
this.reconnecting = {
|
|
22150
22665
|
targetDeviceId: record7.hostDeviceId,
|
|
22151
22666
|
...record7.hostName === void 0 ? {} : { targetName: record7.hostName },
|
|
22152
22667
|
phase: "restore"
|
|
22153
22668
|
};
|
|
22669
|
+
this.lastWorkspaceSelection = {
|
|
22670
|
+
targetDeviceId: record7.hostDeviceId,
|
|
22671
|
+
workspaceId: record7.workspace.workspaceId,
|
|
22672
|
+
...record7.workspace.kind === "codex" ? { backend: "codex" } : {},
|
|
22673
|
+
...record7.workspace.workspacePath === void 0 ? {} : { workspacePath: record7.workspace.workspacePath },
|
|
22674
|
+
...record7.workspace.sessionId === void 0 ? {} : { sessionId: record7.workspace.sessionId }
|
|
22675
|
+
};
|
|
22154
22676
|
this.logger.info("restoring the remote target of the previous run", {
|
|
22155
|
-
targetDeviceId: shortId2(record7.hostDeviceId)
|
|
22677
|
+
targetDeviceId: shortId2(record7.hostDeviceId),
|
|
22678
|
+
kind: record7.workspace.kind
|
|
22156
22679
|
});
|
|
22157
|
-
void this.reconnectRemoteSession(record7.hostDeviceId);
|
|
22680
|
+
void this.reconnectRemoteSession(record7.hostDeviceId, this.sessionTargetFor(record7.hostDeviceId), true);
|
|
22158
22681
|
return true;
|
|
22159
22682
|
}
|
|
22160
22683
|
async listRemoteDirectory(targetDeviceId, path, signal) {
|
|
22161
|
-
const remote = await this.
|
|
22684
|
+
const remote = await this.ensureBrowsing(targetDeviceId, signal);
|
|
22162
22685
|
if (remote.features.remoteGateway) {
|
|
22163
22686
|
const value = await new RemoteTypertGateway2(remote.client).invoke({
|
|
22164
22687
|
namespace: "directoryPicker",
|
|
@@ -22176,7 +22699,7 @@ var ClientModeRuntime = class {
|
|
|
22176
22699
|
return unwrapNativeResult(response);
|
|
22177
22700
|
}
|
|
22178
22701
|
async listRemoteWorkspaces(targetDeviceId, signal) {
|
|
22179
|
-
const remote = await this.
|
|
22702
|
+
const remote = await this.ensureBrowsing(targetDeviceId, signal);
|
|
22180
22703
|
if (remote.features.remoteGateway) {
|
|
22181
22704
|
return readRemoteWorkspaceBaseline(new RemoteTypertGateway2(remote.client), signal);
|
|
22182
22705
|
}
|
|
@@ -22215,31 +22738,108 @@ var ClientModeRuntime = class {
|
|
|
22215
22738
|
this.logger.info("Remote workspace opened", { targetDeviceId: shortId2(remote.target.deviceId) });
|
|
22216
22739
|
return { ...this.status(), workspace };
|
|
22217
22740
|
}
|
|
22741
|
+
/**
|
|
22742
|
+
* The refusal to report when a Host is not offering Codex right now.
|
|
22743
|
+
*
|
|
22744
|
+
* Two different facts hid behind one generic code, so the panel could not tell a user who had switched Codex
|
|
22745
|
+
* off from a Host that never had it. Which one it is comes from this client's own memory: a Host whose Codex
|
|
22746
|
+
* workspace was opened before has Codex, so its absence now means it was switched off (or is still starting);
|
|
22747
|
+
* a Host that never offered one simply does not provide it.
|
|
22748
|
+
*
|
|
22749
|
+
* A Codex that disappeared is also a lost session, so this hands it to the same recovery every other loss
|
|
22750
|
+
* uses before reporting it - a switched-off Codex that is switched back on is then restored on its own.
|
|
22751
|
+
* @param targetDeviceId - the Host the caller was talking to.
|
|
22752
|
+
* @returns the error to throw.
|
|
22753
|
+
*/
|
|
22754
|
+
remoteCodexUnavailable(targetDeviceId) {
|
|
22755
|
+
const expected = shouldRestoreCodexCarrier(this.lastWorkspaceSelection, targetDeviceId) || this.codexHostsSeen.has(targetDeviceId);
|
|
22756
|
+
if (expected) this.noteCodexSessionLost("capability-missing");
|
|
22757
|
+
return expected ? new ClientModeError("CODEX_DISABLED", "Codex is switched off or unavailable on the selected Host.") : new ClientModeError("CODEX_NOT_PROVIDED", "The selected Host does not provide CodeX workspaces.");
|
|
22758
|
+
}
|
|
22218
22759
|
async listCodexWorkspaces(targetDeviceId, signal) {
|
|
22219
|
-
const remote = await this.
|
|
22760
|
+
const remote = await this.ensureBrowsing(targetDeviceId, signal);
|
|
22220
22761
|
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
22221
22762
|
if (!remote.features.codex) {
|
|
22222
|
-
throw
|
|
22763
|
+
throw this.remoteCodexUnavailable(targetDeviceId);
|
|
22223
22764
|
}
|
|
22224
|
-
|
|
22765
|
+
let workspaces;
|
|
22766
|
+
try {
|
|
22767
|
+
workspaces = await discoverCodexVirtualWorkspaces(new CodexRemoteClient(remote.client), signal);
|
|
22768
|
+
} catch (error) {
|
|
22769
|
+
this.logger.warn("Codex workspaces could not be listed", {
|
|
22770
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22771
|
+
code: failureCode(error)
|
|
22772
|
+
});
|
|
22773
|
+
if (isCodexSessionLoss(error)) this.noteCodexSessionLost("list-failed");
|
|
22774
|
+
throw error;
|
|
22775
|
+
}
|
|
22776
|
+
this.lastCodexListing.set(targetDeviceId, workspaces);
|
|
22777
|
+
for (const item of workspaces) {
|
|
22778
|
+
this.codexWorkspaceRoots.set(`${targetDeviceId}|${item.workspaceId}`, item.path);
|
|
22779
|
+
}
|
|
22780
|
+
this.logger.info("Codex workspaces listed", {
|
|
22781
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22782
|
+
count: workspaces.length
|
|
22783
|
+
});
|
|
22784
|
+
return workspaces;
|
|
22225
22785
|
}
|
|
22226
22786
|
async openCodexWorkspace(targetDeviceId, workspaceId, signal) {
|
|
22227
22787
|
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
22228
22788
|
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
22229
22789
|
if (!remote.features.codex) {
|
|
22230
|
-
throw
|
|
22790
|
+
throw this.remoteCodexUnavailable(targetDeviceId);
|
|
22231
22791
|
}
|
|
22232
22792
|
this.assertLocalHarnessCarrierAvailable();
|
|
22233
22793
|
const virtual = CodexVirtualHarness.remote(remote.client, {
|
|
22234
22794
|
deviceId: remote.target.deviceId,
|
|
22235
22795
|
name: remote.target.name
|
|
22236
22796
|
}, harnessSessionGeneration(this.host?.localHarnessVersion?.()), new RemoteTypertGateway2(remote.client));
|
|
22797
|
+
virtual.watchCodexSessionLost((trigger) => this.noteCodexSessionLost(trigger));
|
|
22237
22798
|
let workspace;
|
|
22238
22799
|
try {
|
|
22239
22800
|
workspace = await virtual.selectWorkspace(workspaceId, signal);
|
|
22240
|
-
} catch {
|
|
22801
|
+
} catch (error) {
|
|
22802
|
+
const code = safeErrorCode(error);
|
|
22803
|
+
const listed = this.lastCodexListing.get(targetDeviceId);
|
|
22804
|
+
const previousRoot = listed?.find((item) => item.workspaceId === workspaceId)?.path ?? this.codexWorkspaceRoots.get(`${targetDeviceId}|${workspaceId}`) ?? this.lastWorkspaceSelection?.workspacePath;
|
|
22805
|
+
const inListing = listed?.some((item) => item.workspaceId === workspaceId) ?? false;
|
|
22806
|
+
if (previousRoot !== void 0) {
|
|
22807
|
+
const byRoot = await virtual.selectWorkspace(workspaceId, signal, previousRoot).catch(() => void 0);
|
|
22808
|
+
if (byRoot !== void 0) {
|
|
22809
|
+
this.logger.info("CodeX workspace opened by root after the id missed", {
|
|
22810
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22811
|
+
path: previousRoot
|
|
22812
|
+
});
|
|
22813
|
+
await virtual.close();
|
|
22814
|
+
return this.openCodexWorkspace(targetDeviceId, byRoot.workspaceId, signal);
|
|
22815
|
+
}
|
|
22816
|
+
}
|
|
22241
22817
|
await virtual.close();
|
|
22242
|
-
|
|
22818
|
+
let currentId;
|
|
22819
|
+
if (previousRoot !== void 0) {
|
|
22820
|
+
currentId = await this.codexWorkspaceIdForPath(targetDeviceId, previousRoot).catch(() => void 0);
|
|
22821
|
+
}
|
|
22822
|
+
if (currentId !== void 0 && currentId !== workspaceId) {
|
|
22823
|
+
this.logger.info("CodeX workspace reopened under a new id", {
|
|
22824
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22825
|
+
path: previousRoot
|
|
22826
|
+
});
|
|
22827
|
+
return this.openCodexWorkspace(targetDeviceId, currentId, signal);
|
|
22828
|
+
}
|
|
22829
|
+
const currentListing = this.lastCodexListing.get(targetDeviceId) ?? [];
|
|
22830
|
+
this.logger.warn("selected CodeX workspace could not be opened", {
|
|
22831
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22832
|
+
code,
|
|
22833
|
+
inListing,
|
|
22834
|
+
hasRoot: previousRoot !== void 0,
|
|
22835
|
+
roots: [...this.codexWorkspaceRoots.keys()].filter((key) => key.startsWith(`${targetDeviceId}|`)).length,
|
|
22836
|
+
listed: listed?.length ?? -1,
|
|
22837
|
+
// What was asked for against what the Host currently offers, so the next report needs no second round trip.
|
|
22838
|
+
wanted: workspaceId.slice(0, 60),
|
|
22839
|
+
root: previousRoot === void 0 ? "(none)" : previousRoot,
|
|
22840
|
+
current: currentListing.slice(0, 5).map((item) => item.workspaceId + "@" + item.path).join(", ")
|
|
22841
|
+
});
|
|
22842
|
+
throw error instanceof ClientModeError ? error : new ClientModeError("WORKSPACE_NOT_FOUND", "The selected CodeX workspace is no longer available.");
|
|
22243
22843
|
}
|
|
22244
22844
|
await this.closeCodexVirtual();
|
|
22245
22845
|
this.codexVirtual = virtual;
|
|
@@ -22249,25 +22849,37 @@ var ClientModeRuntime = class {
|
|
|
22249
22849
|
targetDeviceId: remote.target.deviceId,
|
|
22250
22850
|
workspaceId,
|
|
22251
22851
|
backend: "codex",
|
|
22852
|
+
...workspace.path === void 0 ? {} : { workspacePath: workspace.path },
|
|
22252
22853
|
...preferredSessionId === void 0 ? {} : { sessionId: preferredSessionId }
|
|
22253
22854
|
});
|
|
22855
|
+
this.codexHostsSeen.add(remote.target.deviceId);
|
|
22254
22856
|
this.logger.info("CodeX virtual workspace opened", { targetDeviceId: shortId2(remote.target.deviceId) });
|
|
22255
22857
|
return { ...this.status(), workspace };
|
|
22256
22858
|
}
|
|
22257
22859
|
async createCodexWorkspace(targetDeviceId, path, signal) {
|
|
22258
22860
|
const trimmedPath = path.trim();
|
|
22259
22861
|
if (trimmedPath === "") throw new ClientModeError("INVALID_MESSAGE", "A CodeX project directory is required.");
|
|
22260
|
-
const remote = await this.
|
|
22862
|
+
const remote = await this.ensureBrowsing(targetDeviceId, signal);
|
|
22261
22863
|
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
22262
22864
|
if (!remote.features.codex) {
|
|
22263
|
-
throw
|
|
22865
|
+
throw this.remoteCodexUnavailable(targetDeviceId);
|
|
22264
22866
|
}
|
|
22265
22867
|
this.assertLocalHarnessCarrierAvailable();
|
|
22266
|
-
|
|
22267
|
-
|
|
22268
|
-
|
|
22269
|
-
|
|
22270
|
-
|
|
22868
|
+
let result;
|
|
22869
|
+
try {
|
|
22870
|
+
result = record4(await new CodexRemoteClient(remote.client).request("project/create", {
|
|
22871
|
+
name: remoteWorkspaceTitle(trimmedPath),
|
|
22872
|
+
roots: [{ path: trimmedPath }],
|
|
22873
|
+
idempotencyKey: uuidV7()
|
|
22874
|
+
}, signal));
|
|
22875
|
+
} catch (error) {
|
|
22876
|
+
this.logger.warn("CodeX project could not be registered", {
|
|
22877
|
+
targetDeviceId: shortId2(targetDeviceId),
|
|
22878
|
+
code: failureCode(error)
|
|
22879
|
+
});
|
|
22880
|
+
if (isCodexSessionLoss(error)) this.noteCodexSessionLost("project-create-failed");
|
|
22881
|
+
throw error;
|
|
22882
|
+
}
|
|
22271
22883
|
const project = record4(result.project);
|
|
22272
22884
|
if (typeof project.id !== "string" || project.id.length === 0) {
|
|
22273
22885
|
throw new ClientModeError("INVALID_MESSAGE", "The Host returned an invalid CodeX project.");
|
|
@@ -22291,6 +22903,7 @@ var ClientModeRuntime = class {
|
|
|
22291
22903
|
this.gatewaySwitch.selectLocal();
|
|
22292
22904
|
this.pendingWorkspaceSelection = void 0;
|
|
22293
22905
|
await this.closeCodexVirtual();
|
|
22906
|
+
this.closeBrowsing();
|
|
22294
22907
|
await this.closeCodexStreams(this.connected?.client);
|
|
22295
22908
|
await this.connected?.client.close().catch(() => void 0);
|
|
22296
22909
|
this.connected = void 0;
|
|
@@ -22371,6 +22984,7 @@ var ClientModeRuntime = class {
|
|
|
22371
22984
|
return { opened: true, streamId: value.streamId, threadId: value.threadId };
|
|
22372
22985
|
}
|
|
22373
22986
|
publishLocalCodexFrame = async (event, data2) => {
|
|
22987
|
+
if (event === "codex.app.capability") return;
|
|
22374
22988
|
const streamId = data2.streamId;
|
|
22375
22989
|
const stream = this.codexStreams.get(streamId);
|
|
22376
22990
|
if (stream === void 0 || stream.target.kind !== "local") return;
|
|
@@ -22501,6 +23115,29 @@ var ClientModeRuntime = class {
|
|
|
22501
23115
|
"This Client does not provide a compatible Harness carrier for the selected remote workspace."
|
|
22502
23116
|
);
|
|
22503
23117
|
}
|
|
23118
|
+
/**
|
|
23119
|
+
* Connect, and report the control switch when that is what refused us.
|
|
23120
|
+
*
|
|
23121
|
+
* A device whose control switch is off has its host connection refused by the Server. Its presence may still read
|
|
23122
|
+
* online, so the failure comes back from whichever transport step gave up first - "adaptive transport has not been
|
|
23123
|
+
* authorized", "the authenticated Noise channel is not connected", "the selected Host is offline" - and none of
|
|
23124
|
+
* them name the switch. The device row does, so a failed attempt is re-read against it; an error that already names
|
|
23125
|
+
* a cause is passed through untouched.
|
|
23126
|
+
* @param targetDeviceId - the Host being connected to.
|
|
23127
|
+
* @param signal - aborts the attempt.
|
|
23128
|
+
* @param forceRelay - skips direct negotiation, as the reconnect window does.
|
|
23129
|
+
* @returns the connected remote session.
|
|
23130
|
+
*/
|
|
23131
|
+
async connectWithReason(targetDeviceId, signal, forceRelay = false) {
|
|
23132
|
+
try {
|
|
23133
|
+
return await this.connect(targetDeviceId, signal, forceRelay);
|
|
23134
|
+
} catch (error) {
|
|
23135
|
+
if (safeErrorCode(error) === "CONTROL_DISABLED") throw error;
|
|
23136
|
+
const row = await this.server.listDevices().then((list) => list.find((device) => device.deviceId === targetDeviceId)).catch(() => void 0);
|
|
23137
|
+
if (row?.hostControl === false) throw offlineConnectionError(false);
|
|
23138
|
+
throw error;
|
|
23139
|
+
}
|
|
23140
|
+
}
|
|
22504
23141
|
async connect(targetDeviceId, signal, forceRelay = false) {
|
|
22505
23142
|
signal?.throwIfAborted();
|
|
22506
23143
|
const progressRunId = this.connectionProgressRun + 1;
|
|
@@ -22516,7 +23153,7 @@ var ClientModeRuntime = class {
|
|
|
22516
23153
|
this.updateConnectionProgress(progressRunId, "authorizing-peer");
|
|
22517
23154
|
const target2 = await this.authorizeHostPeer(serverDevice);
|
|
22518
23155
|
const presence = await this.server.presenceFor(targetDeviceId);
|
|
22519
|
-
if (!presence.online) throw
|
|
23156
|
+
if (!presence.online) throw offlineConnectionError(serverDevice.hostControl);
|
|
22520
23157
|
const credentials = await this.server.authenticate(identity);
|
|
22521
23158
|
const rtcFactory = this.config.forceRelay ? void 0 : await this.rtcFactoryProvider({ routeTargets: [this.server.baseUrl] }).catch(() => void 0);
|
|
22522
23159
|
if (!this.config.forceRelay && rtcFactory === void 0) {
|
|
@@ -22629,13 +23266,56 @@ var ClientModeRuntime = class {
|
|
|
22629
23266
|
throw error;
|
|
22630
23267
|
}
|
|
22631
23268
|
}
|
|
23269
|
+
/**
|
|
23270
|
+
* Connect for a read-only panel request.
|
|
23271
|
+
*
|
|
23272
|
+
* Browsing a Host is not a session. This connection never becomes {@link connected}, so a later drop is
|
|
23273
|
+
* ignored by the recovery path and the window stays where the user left it. The session's own link is
|
|
23274
|
+
* reused when it already points at that Host, because the Server keeps only one control connection per
|
|
23275
|
+
* device and role.
|
|
23276
|
+
* @param targetDeviceId - the Host being browsed.
|
|
23277
|
+
* @param signal - aborts the connection attempt.
|
|
23278
|
+
* @returns the connection to read through.
|
|
23279
|
+
*/
|
|
23280
|
+
async ensureBrowsing(targetDeviceId, signal) {
|
|
23281
|
+
if (this.connected?.target.deviceId === targetDeviceId) return this.connected;
|
|
23282
|
+
if (this.browsing?.target.deviceId === targetDeviceId) return this.browsing;
|
|
23283
|
+
const next = await this.connectWithReason(targetDeviceId, signal);
|
|
23284
|
+
const previous = this.browsing;
|
|
23285
|
+
this.browsing = next;
|
|
23286
|
+
this.clearConnectionProgress(next.progressRunId);
|
|
23287
|
+
await previous?.client.close().catch(() => void 0);
|
|
23288
|
+
return next;
|
|
23289
|
+
}
|
|
23290
|
+
/**
|
|
23291
|
+
* Drop the panel's browsing connection.
|
|
23292
|
+
*
|
|
23293
|
+
* Called whenever the user's own decision makes the browse stale: returning to local, signing out, or
|
|
23294
|
+
* opening a session somewhere else. A browsing connection is never kept for later.
|
|
23295
|
+
* @param except - a browsing connection to keep (the one just promoted to a session).
|
|
23296
|
+
*/
|
|
23297
|
+
closeBrowsing(except) {
|
|
23298
|
+
const browsing = this.browsing;
|
|
23299
|
+
if (browsing === void 0 || browsing === except) return;
|
|
23300
|
+
this.browsing = void 0;
|
|
23301
|
+
void browsing.client.close().catch(() => void 0);
|
|
23302
|
+
}
|
|
22632
23303
|
async ensureConnected(targetDeviceId, signal) {
|
|
22633
23304
|
if (this.connected?.target.deviceId === targetDeviceId) return this.connected;
|
|
22634
|
-
const next = await this.connect(targetDeviceId, signal);
|
|
22635
23305
|
const previous = this.connected;
|
|
23306
|
+
if (this.browsing?.target.deviceId === targetDeviceId) {
|
|
23307
|
+
const next2 = this.browsing;
|
|
23308
|
+
this.browsing = void 0;
|
|
23309
|
+
await this.closePreview();
|
|
23310
|
+
this.connected = next2;
|
|
23311
|
+
await previous?.client.close().catch(() => void 0);
|
|
23312
|
+
return next2;
|
|
23313
|
+
}
|
|
23314
|
+
const next = await this.connectWithReason(targetDeviceId, signal);
|
|
22636
23315
|
await this.closePreview();
|
|
22637
23316
|
this.connected = next;
|
|
22638
23317
|
this.clearConnectionProgress(next.progressRunId);
|
|
23318
|
+
this.closeBrowsing();
|
|
22639
23319
|
await previous?.client.close().catch(() => void 0);
|
|
22640
23320
|
return next;
|
|
22641
23321
|
}
|
|
@@ -22668,6 +23348,12 @@ var ClientModeRuntime = class {
|
|
|
22668
23348
|
try {
|
|
22669
23349
|
if (endpoint === "status") return ok2(await this.detailedStatus());
|
|
22670
23350
|
if (endpoint === "client.connection.verify") return ok2(await this.verifyRemoteConnection());
|
|
23351
|
+
if (endpoint === "remote.features.probe") return ok2(await this.probeRemoteFeatures());
|
|
23352
|
+
if (endpoint === "remote.session.recover") return ok2(await this.recoverCurrentSession());
|
|
23353
|
+
if (endpoint === "client.device.revoke") {
|
|
23354
|
+
await this.revokeCurrentDevice();
|
|
23355
|
+
return ok2(this.status());
|
|
23356
|
+
}
|
|
22671
23357
|
if (endpoint === "devices") return ok2(await this.devices());
|
|
22672
23358
|
if (endpoint === "client.account.login") {
|
|
22673
23359
|
const value = record4(payload);
|
|
@@ -22721,6 +23407,7 @@ var ClientModeRuntime = class {
|
|
|
22721
23407
|
if (typeof value.targetDeviceId !== "string" || typeof value.path !== "string") {
|
|
22722
23408
|
throw new ClientModeError("INVALID_MESSAGE", "A Host and working directory are required.");
|
|
22723
23409
|
}
|
|
23410
|
+
this.supersedeRemoteRecovery();
|
|
22724
23411
|
return ok2(await this.openRemoteWorkspace(value.targetDeviceId, value.path, signal));
|
|
22725
23412
|
}
|
|
22726
23413
|
if (endpoint === "codex.workspace.open") {
|
|
@@ -22728,6 +23415,7 @@ var ClientModeRuntime = class {
|
|
|
22728
23415
|
if (typeof value.targetDeviceId !== "string" || typeof value.workspaceId !== "string") {
|
|
22729
23416
|
throw new ClientModeError("INVALID_MESSAGE", "A Host and CodeX Workspace are required.");
|
|
22730
23417
|
}
|
|
23418
|
+
this.supersedeRemoteRecovery();
|
|
22731
23419
|
return ok2(await this.openCodexWorkspace(value.targetDeviceId, value.workspaceId, signal));
|
|
22732
23420
|
}
|
|
22733
23421
|
if (endpoint === "codex.workspace.create") {
|
|
@@ -22843,6 +23531,21 @@ var ClientModeRuntime = class {
|
|
|
22843
23531
|
});
|
|
22844
23532
|
}
|
|
22845
23533
|
};
|
|
23534
|
+
function offlineConnectionError(hostControl) {
|
|
23535
|
+
return hostControl === false ? new ClientModeError(
|
|
23536
|
+
"CONTROL_DISABLED",
|
|
23537
|
+
'This device is not accepting control. Turn on "Allow other users to control this device" on that device.',
|
|
23538
|
+
true
|
|
23539
|
+
) : new ClientModeError("HOST_OFFLINE", "The selected Host is offline.", true);
|
|
23540
|
+
}
|
|
23541
|
+
function isCodexSessionLoss(error) {
|
|
23542
|
+
const own = error !== null && typeof error === "object" && "code" in error ? error.code : void 0;
|
|
23543
|
+
const code = typeof own === "string" ? own : safeErrorCode(error);
|
|
23544
|
+
return code === "CODEX_CONNECTION_CLOSED" || code === "CODEX_CLOSED" || code === "CODEX_UNAVAILABLE";
|
|
23545
|
+
}
|
|
23546
|
+
function shouldRestoreCodexCarrier(selection, targetDeviceId) {
|
|
23547
|
+
return selection !== void 0 && selection.targetDeviceId === targetDeviceId && selection.backend === "codex";
|
|
23548
|
+
}
|
|
22846
23549
|
var ClientModeError = class extends Error {
|
|
22847
23550
|
constructor(code, message, retryable = false) {
|
|
22848
23551
|
super(message);
|
|
@@ -22851,7 +23554,7 @@ var ClientModeError = class extends Error {
|
|
|
22851
23554
|
}
|
|
22852
23555
|
};
|
|
22853
23556
|
function assertAuthorizedHost(listed, descriptor) {
|
|
22854
|
-
if (descriptor.
|
|
23557
|
+
if (descriptor.deviceId !== listed.deviceId || descriptor.membershipId !== listed.membershipId) {
|
|
22855
23558
|
throw new ClientModeError("PEER_IDENTITY_MISMATCH", "Server Host details do not match the authorized device list.");
|
|
22856
23559
|
}
|
|
22857
23560
|
}
|
|
@@ -22957,6 +23660,15 @@ function fail2(error) {
|
|
|
22957
23660
|
function shortId2(value) {
|
|
22958
23661
|
return value.length <= 12 ? value : `${value.slice(0, 8)}\u2026${value.slice(-4)}`;
|
|
22959
23662
|
}
|
|
23663
|
+
function failureCode(error) {
|
|
23664
|
+
const own = error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
23665
|
+
return own ?? safeErrorCode(error);
|
|
23666
|
+
}
|
|
23667
|
+
function sleep3(ms) {
|
|
23668
|
+
return new Promise((resolve4) => {
|
|
23669
|
+
setTimeout(resolve4, ms);
|
|
23670
|
+
});
|
|
23671
|
+
}
|
|
22960
23672
|
function remoteHostFeatures(clientVersion) {
|
|
22961
23673
|
return {
|
|
22962
23674
|
commandList: isVersionAtLeast(clientVersion, REMOTE_COMMAND_LIST_MIN_VERSION),
|
|
@@ -23090,12 +23802,14 @@ var ClientTargetStore = class {
|
|
|
23090
23802
|
if (record7.mode === "remote" && (typeof record7.hostDeviceId !== "string" || record7.hostDeviceId.length === 0)) {
|
|
23091
23803
|
return void 0;
|
|
23092
23804
|
}
|
|
23805
|
+
const workspace = parseWorkspace(record7.workspace);
|
|
23093
23806
|
return {
|
|
23094
23807
|
schemaVersion: 1,
|
|
23095
23808
|
mode: record7.mode,
|
|
23096
23809
|
...typeof record7.serverUrl === "string" && record7.serverUrl.length > 0 ? { serverUrl: record7.serverUrl } : {},
|
|
23097
23810
|
...typeof record7.hostDeviceId === "string" && record7.hostDeviceId.length > 0 ? { hostDeviceId: record7.hostDeviceId } : {},
|
|
23098
23811
|
...typeof record7.hostName === "string" && record7.hostName.length > 0 ? { hostName: record7.hostName } : {},
|
|
23812
|
+
...workspace === void 0 ? {} : { workspace },
|
|
23099
23813
|
savedAt: typeof record7.savedAt === "number" ? record7.savedAt : 0
|
|
23100
23814
|
};
|
|
23101
23815
|
} catch {
|
|
@@ -23104,12 +23818,14 @@ var ClientTargetStore = class {
|
|
|
23104
23818
|
}
|
|
23105
23819
|
/** Record the current target. Failures reach the caller so a boot can log them. */
|
|
23106
23820
|
async save(target2) {
|
|
23821
|
+
const workspace = parseWorkspace(target2.workspace);
|
|
23107
23822
|
const value = {
|
|
23108
23823
|
schemaVersion: 1,
|
|
23109
23824
|
mode: target2.mode,
|
|
23110
23825
|
...target2.serverUrl === void 0 ? {} : { serverUrl: target2.serverUrl },
|
|
23111
23826
|
...target2.hostDeviceId === void 0 ? {} : { hostDeviceId: target2.hostDeviceId },
|
|
23112
23827
|
...target2.hostName === void 0 ? {} : { hostName: target2.hostName },
|
|
23828
|
+
...workspace === void 0 ? {} : { workspace },
|
|
23113
23829
|
savedAt: Date.now()
|
|
23114
23830
|
};
|
|
23115
23831
|
await mkdir2(this.directory, { recursive: true, mode: 448 });
|
|
@@ -23121,15 +23837,28 @@ var ClientTargetStore = class {
|
|
|
23121
23837
|
await chmod2(this.file, 384);
|
|
23122
23838
|
}
|
|
23123
23839
|
};
|
|
23840
|
+
function parseWorkspace(value) {
|
|
23841
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
23842
|
+
const record7 = value;
|
|
23843
|
+
if (typeof record7.workspaceId !== "string" || record7.workspaceId.length === 0) return void 0;
|
|
23844
|
+
if (record7.kind !== "harness" && record7.kind !== "codex") return void 0;
|
|
23845
|
+
return {
|
|
23846
|
+
workspaceId: record7.workspaceId,
|
|
23847
|
+
kind: record7.kind,
|
|
23848
|
+
...typeof record7.workspacePath === "string" && record7.workspacePath.length > 0 ? { workspacePath: record7.workspacePath } : {},
|
|
23849
|
+
...typeof record7.sessionId === "string" && record7.sessionId.length > 0 ? { sessionId: record7.sessionId } : {}
|
|
23850
|
+
};
|
|
23851
|
+
}
|
|
23124
23852
|
|
|
23125
23853
|
// src/control-runtime.ts
|
|
23126
23854
|
import { hostname as hostname2 } from "node:os";
|
|
23127
|
-
import { existsSync as
|
|
23855
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
23128
23856
|
import { execFileSync } from "node:child_process";
|
|
23129
23857
|
|
|
23130
23858
|
// src/identity-store.ts
|
|
23859
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
23131
23860
|
import { createHash } from "node:crypto";
|
|
23132
|
-
import { chmod as chmod3, mkdir as mkdir3, readFile as readFile4, rm as rm3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
23861
|
+
import { chmod as chmod3, cp, mkdir as mkdir3, readFile as readFile4, rm as rm3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
|
|
23133
23862
|
import { homedir } from "node:os";
|
|
23134
23863
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
23135
23864
|
var identitySchema = external_exports.object({
|
|
@@ -23263,6 +23992,19 @@ function serverStorageDirectory(root, serverUrl, role) {
|
|
|
23263
23992
|
const scope = createHash("sha256").update(origin).digest("hex").slice(0, 24);
|
|
23264
23993
|
return join6(root, "servers", scope, role);
|
|
23265
23994
|
}
|
|
23995
|
+
async function ensureDeviceDirectory(root, serverUrl) {
|
|
23996
|
+
const directory = serverStorageDirectory(root, serverUrl, "device");
|
|
23997
|
+
if (existsSync2(directory)) return directory;
|
|
23998
|
+
await mkdir3(dirname4(directory), { recursive: true, mode: 448 });
|
|
23999
|
+
for (const legacy of ["host", "client"]) {
|
|
24000
|
+
const from = serverStorageDirectory(root, serverUrl, legacy);
|
|
24001
|
+
if (!existsSync2(from)) continue;
|
|
24002
|
+
await cp(from, directory, { recursive: true });
|
|
24003
|
+
return directory;
|
|
24004
|
+
}
|
|
24005
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
24006
|
+
return directory;
|
|
24007
|
+
}
|
|
23266
24008
|
function fingerprint(publicKey) {
|
|
23267
24009
|
const compact = createHash("sha256").update(fromBase64Url2(publicKey)).digest("hex").slice(0, 12).toUpperCase();
|
|
23268
24010
|
return compact.match(/.{1,4}/g).join(" ");
|
|
@@ -23449,7 +24191,7 @@ var ControlStatusStream = class {
|
|
|
23449
24191
|
|
|
23450
24192
|
// src/codex/domain.ts
|
|
23451
24193
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
23452
|
-
import { accessSync, constants, existsSync as
|
|
24194
|
+
import { accessSync, constants, existsSync as existsSync3, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
23453
24195
|
import { readdir as readdir2, realpath, stat as stat4 } from "node:fs/promises";
|
|
23454
24196
|
import { homedir as homedir2 } from "node:os";
|
|
23455
24197
|
import { basename as basename2, isAbsolute as isAbsolute2, join as join7, posix, relative, resolve } from "node:path";
|
|
@@ -23646,7 +24388,11 @@ var CodexAppServerClient = class {
|
|
|
23646
24388
|
const pending = this.takePending(value.id);
|
|
23647
24389
|
if (pending === void 0) return;
|
|
23648
24390
|
if ("error" in value && value.error !== void 0) {
|
|
23649
|
-
|
|
24391
|
+
const upstreamCode = safeUpstreamCode(value.error);
|
|
24392
|
+
pending.reject(Object.assign(
|
|
24393
|
+
new CodexAppServerError("CODEX_UPSTREAM_ERROR", safeUpstreamError(value.error)),
|
|
24394
|
+
upstreamCode === void 0 ? {} : { upstreamCode }
|
|
24395
|
+
));
|
|
23650
24396
|
} else {
|
|
23651
24397
|
pending.resolve(value.result);
|
|
23652
24398
|
}
|
|
@@ -23663,7 +24409,13 @@ var CodexAppServerClient = class {
|
|
|
23663
24409
|
const child = this.process;
|
|
23664
24410
|
this.process = void 0;
|
|
23665
24411
|
child?.kill("SIGTERM");
|
|
23666
|
-
|
|
24412
|
+
const failure2 = cause;
|
|
24413
|
+
this.logger?.warn("Codex App Server communication failed", {
|
|
24414
|
+
code,
|
|
24415
|
+
causeCode: failure2.code ?? "none",
|
|
24416
|
+
...failure2.errno === void 0 ? {} : { errno: failure2.errno },
|
|
24417
|
+
...failure2.syscall === void 0 ? {} : { syscall: failure2.syscall }
|
|
24418
|
+
});
|
|
23667
24419
|
if (!this.closed) this.notifyUnavailable(code);
|
|
23668
24420
|
}
|
|
23669
24421
|
notifyUnavailable(code) {
|
|
@@ -23688,6 +24440,13 @@ function safeUpstreamError(value) {
|
|
|
23688
24440
|
if (message.includes("active writer")) return "Codex thread already has an active writer.";
|
|
23689
24441
|
return message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request.";
|
|
23690
24442
|
}
|
|
24443
|
+
function safeUpstreamCode(value) {
|
|
24444
|
+
if (!isRecord10(value)) return void 0;
|
|
24445
|
+
const raw = isRecord10(value.data) && value.data.code !== void 0 ? value.data.code : value.code;
|
|
24446
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return String(raw);
|
|
24447
|
+
if (typeof raw !== "string") return void 0;
|
|
24448
|
+
return /^[A-Za-z0-9_.:-]{1,48}$/u.test(raw) ? raw : void 0;
|
|
24449
|
+
}
|
|
23691
24450
|
function isRecord10(value) {
|
|
23692
24451
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23693
24452
|
}
|
|
@@ -23901,6 +24660,19 @@ var CodexPeerBridge = class {
|
|
|
23901
24660
|
await this.publish("codex.app.frame", data2);
|
|
23902
24661
|
}
|
|
23903
24662
|
}
|
|
24663
|
+
/**
|
|
24664
|
+
* Tell this connection whether the Host can serve Codex, and say it only when that actually changed.
|
|
24665
|
+
*
|
|
24666
|
+
* The per-stream closes below only reach a client that is watching a thread. A session parked on its history,
|
|
24667
|
+
* or a workspace with nothing open, heard nothing until its next call failed - which is why switching Codex
|
|
24668
|
+
* off looked like a plain error instead of a lost session. This states the fact rather than the conclusion, so
|
|
24669
|
+
* the client can decide whether the session it is showing is the one affected.
|
|
24670
|
+
* @param available - whether Codex can be served right now.
|
|
24671
|
+
*/
|
|
24672
|
+
async notifyCapability(available) {
|
|
24673
|
+
if (this.closed) return;
|
|
24674
|
+
await this.publish("codex.app.capability", { available }).catch(() => void 0);
|
|
24675
|
+
}
|
|
23904
24676
|
async failStreams(reason = "failed") {
|
|
23905
24677
|
if (this.closed) return;
|
|
23906
24678
|
const streamIds = [...this.streams.keys()];
|
|
@@ -23971,7 +24743,9 @@ function concatChunks(chunks, totalBytes) {
|
|
|
23971
24743
|
// src/codex/domain.ts
|
|
23972
24744
|
var APPROVAL_TTL_MS = 5 * 6e4;
|
|
23973
24745
|
var DEFAULT_RESTART_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
23974
|
-
var
|
|
24746
|
+
var CODEX_SLOW_RETRY_MS = 6e4;
|
|
24747
|
+
var ACCOUNT_PROBE_TIMEOUT_MS = 6e4;
|
|
24748
|
+
var CODEX_START_BUDGET_MS = 9e4;
|
|
23975
24749
|
var CODEX_PAGE_LIMIT2 = 100;
|
|
23976
24750
|
var MAX_CODEX_PAGES2 = 32;
|
|
23977
24751
|
var CODEX_HISTORY_PAGE_LIMIT = 25;
|
|
@@ -23979,10 +24753,10 @@ var MAX_CODEX_HISTORY_PAGES = 64;
|
|
|
23979
24753
|
var CODEX_DIRECTORY_ENTRY_LIMIT = 500;
|
|
23980
24754
|
var CodexRemoteDomain = class {
|
|
23981
24755
|
constructor(config, logger, createAppServer = (binary, targetLogger) => new CodexAppServerClient(binary, targetLogger), restartDelaysMs = DEFAULT_RESTART_DELAYS_MS) {
|
|
23982
|
-
this.config = config;
|
|
23983
24756
|
this.logger = logger;
|
|
23984
24757
|
this.createAppServer = createAppServer;
|
|
23985
24758
|
this.restartDelaysMs = restartDelaysMs;
|
|
24759
|
+
this.config = config;
|
|
23986
24760
|
}
|
|
23987
24761
|
appServer;
|
|
23988
24762
|
unsubscribeInbound;
|
|
@@ -23995,10 +24769,30 @@ var CodexRemoteDomain = class {
|
|
|
23995
24769
|
approvalExpiryTimer;
|
|
23996
24770
|
restartTimer;
|
|
23997
24771
|
restartAttempt = 0;
|
|
24772
|
+
/** Whether the "fast ladder is spent, retrying slowly" line has been logged for this outage. */
|
|
24773
|
+
slowRestartLogged = false;
|
|
23998
24774
|
available = false;
|
|
23999
24775
|
closed = false;
|
|
24776
|
+
/**
|
|
24777
|
+
* The last availability announced to peers, or undefined before the first announcement.
|
|
24778
|
+
*
|
|
24779
|
+
* Announcements are transitions, not states: the Host tells clients *when* Codex came or went, never "your
|
|
24780
|
+
* session is gone". Announcing on every restart made switching Codex **on** read as a loss, and the client
|
|
24781
|
+
* answered a loss with a fresh recovery - which pulled back a view the user had just cancelled.
|
|
24782
|
+
*/
|
|
24783
|
+
announcedAvailable;
|
|
24784
|
+
/**
|
|
24785
|
+
* Bumped by every restart() and close().
|
|
24786
|
+
*
|
|
24787
|
+
* A launch that began before the switch was flipped can finish after it, and its completion used to write
|
|
24788
|
+
* `available = true` onto a Codex the user had switched off - which made the next "switch on" look like a
|
|
24789
|
+
* transition out of availability and produced the false loss notice. Work from an older generation is discarded.
|
|
24790
|
+
*/
|
|
24791
|
+
generation = 0;
|
|
24000
24792
|
state = "disabled";
|
|
24001
24793
|
unavailableCode;
|
|
24794
|
+
/** The live configuration; replaced in place by {@link restart}. */
|
|
24795
|
+
config;
|
|
24002
24796
|
async start() {
|
|
24003
24797
|
if (this.closed) throw new RpcError("CODEX_CLOSED", "The Codex Remote domain is closed.");
|
|
24004
24798
|
if (!this.config.enabled) return;
|
|
@@ -24040,6 +24834,10 @@ var CodexRemoteDomain = class {
|
|
|
24040
24834
|
}
|
|
24041
24835
|
createPeer(context, publish) {
|
|
24042
24836
|
if (!this.config.enabled) return void 0;
|
|
24837
|
+
this.logger.info("Codex client attached", {
|
|
24838
|
+
peerDeviceId: context.peerDeviceId.slice(0, 12),
|
|
24839
|
+
connectionId: context.connectionId.slice(0, 12)
|
|
24840
|
+
});
|
|
24043
24841
|
const bridge = new CodexPeerBridge(this, context, publish, this.logger);
|
|
24044
24842
|
this.peers.set(context.connectionId, bridge);
|
|
24045
24843
|
this.peerDeviceIds.set(context.connectionId, context.peerDeviceId);
|
|
@@ -24216,13 +25014,79 @@ var CodexRemoteDomain = class {
|
|
|
24216
25014
|
async close() {
|
|
24217
25015
|
if (this.closed) return;
|
|
24218
25016
|
this.closed = true;
|
|
24219
|
-
this.
|
|
25017
|
+
this.generation += 1;
|
|
25018
|
+
await this.teardown();
|
|
24220
25019
|
this.state = this.config.enabled ? "unavailable" : "disabled";
|
|
24221
|
-
|
|
24222
|
-
|
|
25020
|
+
}
|
|
25021
|
+
/**
|
|
25022
|
+
* State the Codex capability to every attached connection - and only when it actually changed.
|
|
25023
|
+
*
|
|
25024
|
+
* This is the fact, not the conclusion: an unavailable Codex means "the session you are watching is gone" to a
|
|
25025
|
+
* client that is watching one, and means nothing to a client that is not. Sending it only on a real transition
|
|
25026
|
+
* is what keeps "switch Codex on" from being reported as a loss.
|
|
25027
|
+
* @returns when every attached connection has been told.
|
|
25028
|
+
*/
|
|
25029
|
+
async announceCapability() {
|
|
25030
|
+
const available = this.isAvailable();
|
|
25031
|
+
if (this.announcedAvailable === void 0) {
|
|
25032
|
+
this.announcedAvailable = available;
|
|
25033
|
+
return;
|
|
25034
|
+
}
|
|
25035
|
+
if (available === this.announcedAvailable) return;
|
|
25036
|
+
this.announcedAvailable = available;
|
|
25037
|
+
this.logger.info("Codex capability changed; telling every attached connection", { available });
|
|
25038
|
+
await Promise.all([...this.peers.values()].map((peer) => peer.notifyCapability(available).catch(() => void 0)));
|
|
25039
|
+
}
|
|
25040
|
+
/**
|
|
25041
|
+
* Apply a changed configuration without restarting DSH.
|
|
25042
|
+
*
|
|
25043
|
+
* Deliberately not close()+start(): close() is terminal, so that pair left the domain dead for the rest of the
|
|
25044
|
+
* process (the bug behind "the switch never comes back"). This shares close()'s teardown without setting the
|
|
25045
|
+
* terminal flag, and a disabled configuration simply stops - no launch, state `disabled`.
|
|
25046
|
+
*
|
|
25047
|
+
* The per-connection bridges are deliberately *kept*. They are the attached Remote clients' only handle on
|
|
25048
|
+
* this Host's Codex, and tearing them down here is what left every one of them holding a closed bridge:
|
|
25049
|
+
* the reported "The Codex connection is closed" that no refresh could clear, because nothing short of a full
|
|
25050
|
+
* transport reconnect could get a working bridge again. Everything the App Server owns - live streams, turn
|
|
25051
|
+
* leases, approvals, permission snapshots - is released instead, which is exactly what a crash already did.
|
|
25052
|
+
* @param config - the configuration to apply, when the caller changed it.
|
|
25053
|
+
* @returns when the replacement attempt finished.
|
|
25054
|
+
*/
|
|
25055
|
+
async restart(config) {
|
|
25056
|
+
if (this.closed) throw new RpcError("CODEX_CLOSED", "The Codex Remote domain is closed.");
|
|
25057
|
+
if (config !== void 0) this.config = config;
|
|
25058
|
+
this.generation += 1;
|
|
25059
|
+
const peers = [...this.peers.values()];
|
|
25060
|
+
await Promise.all(peers.map((peer) => peer.failStreams("failed")));
|
|
25061
|
+
await this.launching?.catch(() => void 0);
|
|
25062
|
+
await this.releaseAppServer();
|
|
25063
|
+
if (!this.config.enabled) await this.announceCapability();
|
|
25064
|
+
this.unavailableCode = void 0;
|
|
25065
|
+
this.restartAttempt = 0;
|
|
25066
|
+
this.slowRestartLogged = false;
|
|
25067
|
+
if (!this.config.enabled) {
|
|
25068
|
+
this.state = "disabled";
|
|
25069
|
+
return;
|
|
25070
|
+
}
|
|
25071
|
+
await this.start();
|
|
25072
|
+
}
|
|
25073
|
+
/** Release every live resource; shared by close() (terminal) and used by close() alone. */
|
|
25074
|
+
async teardown() {
|
|
24223
25075
|
for (const bridge of [...this.peers.values()]) await bridge.closeAll();
|
|
24224
25076
|
this.peers.clear();
|
|
24225
25077
|
this.peerDeviceIds.clear();
|
|
25078
|
+
await this.releaseAppServer();
|
|
25079
|
+
}
|
|
25080
|
+
/**
|
|
25081
|
+
* Release everything that belongs to the App Server, keeping the per-connection bridges.
|
|
25082
|
+
*
|
|
25083
|
+
* Shared by restart() and by a crash: in both cases the bridges must outlive the process so an attached
|
|
25084
|
+
* client's next call simply reaches the replacement instead of failing for good.
|
|
25085
|
+
*/
|
|
25086
|
+
async releaseAppServer() {
|
|
25087
|
+
this.available = false;
|
|
25088
|
+
if (this.restartTimer !== void 0) clearTimeout(this.restartTimer);
|
|
25089
|
+
this.restartTimer = void 0;
|
|
24226
25090
|
this.turnOwners.clear();
|
|
24227
25091
|
this.permissionPresets.clear();
|
|
24228
25092
|
if (this.approvalExpiryTimer !== void 0) clearTimeout(this.approvalExpiryTimer);
|
|
@@ -24235,6 +25099,27 @@ var CodexRemoteDomain = class {
|
|
|
24235
25099
|
await this.appServer?.close();
|
|
24236
25100
|
this.appServer = void 0;
|
|
24237
25101
|
}
|
|
25102
|
+
/**
|
|
25103
|
+
* The launch in flight, if any.
|
|
25104
|
+
*
|
|
25105
|
+
* Two overlapping launches fight over `this.appServer`, and whichever finishes first disposes the other's App
|
|
25106
|
+
* Server - which is answering its account probe at that moment, so the probe fails with `CODEX_CLOSED` and the
|
|
25107
|
+
* domain reports a closed Codex on a machine where nothing was closed. One launch at a time, and
|
|
25108
|
+
* `restart()` waits for it instead of closing it out from under the probe.
|
|
25109
|
+
*/
|
|
25110
|
+
launching;
|
|
25111
|
+
/**
|
|
25112
|
+
* Launch once, joining the launch already in flight instead of starting a second one.
|
|
25113
|
+
* @returns when this launch settled (either way).
|
|
25114
|
+
*/
|
|
25115
|
+
launchAppServerOnce() {
|
|
25116
|
+
if (this.launching !== void 0) return this.launching;
|
|
25117
|
+
const launch = this.launchAppServer().finally(() => {
|
|
25118
|
+
if (this.launching === launch) this.launching = void 0;
|
|
25119
|
+
});
|
|
25120
|
+
this.launching = launch;
|
|
25121
|
+
return launch;
|
|
25122
|
+
}
|
|
24238
25123
|
/**
|
|
24239
25124
|
* Launch the App Server within {@link CODEX_START_BUDGET_MS}.
|
|
24240
25125
|
*
|
|
@@ -24245,7 +25130,7 @@ var CodexRemoteDomain = class {
|
|
|
24245
25130
|
*/
|
|
24246
25131
|
async launchWithinBudget() {
|
|
24247
25132
|
let timer;
|
|
24248
|
-
const attempt = this.
|
|
25133
|
+
const attempt = this.launchAppServerOnce();
|
|
24249
25134
|
attempt.catch(() => void 0);
|
|
24250
25135
|
try {
|
|
24251
25136
|
await Promise.race([
|
|
@@ -24262,19 +25147,26 @@ var CodexRemoteDomain = class {
|
|
|
24262
25147
|
}
|
|
24263
25148
|
}
|
|
24264
25149
|
async launchAppServer() {
|
|
25150
|
+
let firstError;
|
|
24265
25151
|
let lastError;
|
|
25152
|
+
let index = 0;
|
|
24266
25153
|
for (const binary of codexBinaryCandidates(this.config.binary)) {
|
|
25154
|
+
index += 1;
|
|
24267
25155
|
try {
|
|
24268
25156
|
await this.launchAppServerCandidate(binary);
|
|
24269
25157
|
return;
|
|
24270
25158
|
} catch (error) {
|
|
25159
|
+
this.logger.warn("Codex App Server candidate failed", { candidate: index, code: errorCode4(error) });
|
|
25160
|
+
firstError ??= error;
|
|
24271
25161
|
lastError = error;
|
|
24272
25162
|
if (!canTryNextBinary(error)) throw error;
|
|
24273
25163
|
}
|
|
24274
25164
|
}
|
|
24275
|
-
|
|
25165
|
+
const chosen = errorCode4(lastError) === "CODEX_BINARY_UNAVAILABLE" && firstError !== void 0 ? firstError : lastError;
|
|
25166
|
+
throw chosen ?? new RpcError("CODEX_START_FAILED", "Codex App Server could not be started.");
|
|
24276
25167
|
}
|
|
24277
25168
|
async launchAppServerCandidate(binary) {
|
|
25169
|
+
const generation = this.generation;
|
|
24278
25170
|
const appServer = this.createAppServer(binary, this.logger);
|
|
24279
25171
|
this.appServer = appServer;
|
|
24280
25172
|
this.unsubscribeInbound = appServer.onInbound((message) => {
|
|
@@ -24289,7 +25181,17 @@ var CodexRemoteDomain = class {
|
|
|
24289
25181
|
});
|
|
24290
25182
|
try {
|
|
24291
25183
|
await appServer.start();
|
|
24292
|
-
|
|
25184
|
+
let account;
|
|
25185
|
+
try {
|
|
25186
|
+
account = await appServer.call("account/read", { refreshToken: false }, ACCOUNT_PROBE_TIMEOUT_MS);
|
|
25187
|
+
} catch (error) {
|
|
25188
|
+
const upstreamCode = error.upstreamCode;
|
|
25189
|
+
this.logger.warn("Codex account probe failed", {
|
|
25190
|
+
code: errorCode4(error),
|
|
25191
|
+
...upstreamCode === void 0 ? {} : { upstreamCode: String(upstreamCode) }
|
|
25192
|
+
});
|
|
25193
|
+
throw classifyAccountProbeFailure(error);
|
|
25194
|
+
}
|
|
24293
25195
|
if (!accountCanRun(account)) {
|
|
24294
25196
|
throw new RpcError("CODEX_AUTH_REQUIRED", "Codex is not signed in on this Host.");
|
|
24295
25197
|
}
|
|
@@ -24297,11 +25199,22 @@ var CodexRemoteDomain = class {
|
|
|
24297
25199
|
await appServer.close().catch(() => void 0);
|
|
24298
25200
|
return;
|
|
24299
25201
|
}
|
|
25202
|
+
if (generation !== this.generation || !this.config.enabled) {
|
|
25203
|
+
this.logger.info("Codex App Server started after the domain moved on; releasing it", {
|
|
25204
|
+
generation: this.generation - generation,
|
|
25205
|
+
enabled: this.config.enabled
|
|
25206
|
+
});
|
|
25207
|
+
await this.disposeAppServer(appServer);
|
|
25208
|
+
this.available = false;
|
|
25209
|
+
this.state = this.config.enabled ? "restarting" : "disabled";
|
|
25210
|
+
return;
|
|
25211
|
+
}
|
|
24300
25212
|
this.available = true;
|
|
24301
25213
|
this.state = "ready";
|
|
24302
25214
|
this.restartAttempt = 0;
|
|
24303
25215
|
this.unavailableCode = void 0;
|
|
24304
25216
|
this.logger.info("Codex Remote domain ready");
|
|
25217
|
+
await this.announceCapability();
|
|
24305
25218
|
} catch (error) {
|
|
24306
25219
|
await this.disposeAppServer(appServer);
|
|
24307
25220
|
throw error;
|
|
@@ -24318,14 +25231,27 @@ var CodexRemoteDomain = class {
|
|
|
24318
25231
|
if (this.approvalExpiryTimer !== void 0) clearTimeout(this.approvalExpiryTimer);
|
|
24319
25232
|
this.approvalExpiryTimer = void 0;
|
|
24320
25233
|
await Promise.all([...this.peers.values()].map((peer) => peer.failStreams("failed")));
|
|
25234
|
+
await this.announceCapability();
|
|
24321
25235
|
await this.disposeAppServer(appServer);
|
|
24322
25236
|
this.scheduleRestart();
|
|
24323
25237
|
}
|
|
24324
25238
|
scheduleRestart() {
|
|
24325
25239
|
if (this.closed || this.restartTimer !== void 0) return;
|
|
25240
|
+
if (!this.config.enabled) return;
|
|
24326
25241
|
if (this.restartAttempt >= this.restartDelaysMs.length) {
|
|
24327
25242
|
this.state = "unavailable";
|
|
24328
|
-
this.
|
|
25243
|
+
this.restartTimer = setTimeout(() => {
|
|
25244
|
+
this.restartTimer = void 0;
|
|
25245
|
+
void this.restartAfterFailure();
|
|
25246
|
+
}, CODEX_SLOW_RETRY_MS);
|
|
25247
|
+
this.restartTimer.unref?.();
|
|
25248
|
+
if (!this.slowRestartLogged) {
|
|
25249
|
+
this.slowRestartLogged = true;
|
|
25250
|
+
this.logger.warn("Codex App Server restart attempts exhausted; retrying every " + String(CODEX_SLOW_RETRY_MS) + "ms", {
|
|
25251
|
+
attempts: this.restartAttempt,
|
|
25252
|
+
code: this.unavailableCode
|
|
25253
|
+
});
|
|
25254
|
+
}
|
|
24329
25255
|
return;
|
|
24330
25256
|
}
|
|
24331
25257
|
const delayMs = Math.max(0, this.restartDelaysMs[this.restartAttempt] ?? 0);
|
|
@@ -24339,9 +25265,13 @@ var CodexRemoteDomain = class {
|
|
|
24339
25265
|
this.logger.warn("Codex App Server restart scheduled", { attempt: this.restartAttempt, delayMs });
|
|
24340
25266
|
}
|
|
24341
25267
|
async restartAfterFailure() {
|
|
24342
|
-
if (this.closed) return;
|
|
25268
|
+
if (this.closed || !this.config.enabled) return;
|
|
24343
25269
|
try {
|
|
24344
|
-
await this.
|
|
25270
|
+
await this.launchAppServerOnce();
|
|
25271
|
+
if (!this.config.enabled) {
|
|
25272
|
+
await this.releaseAppServer();
|
|
25273
|
+
this.state = "disabled";
|
|
25274
|
+
}
|
|
24345
25275
|
} catch (error) {
|
|
24346
25276
|
this.available = false;
|
|
24347
25277
|
this.state = "restarting";
|
|
@@ -24669,7 +25599,14 @@ var CodexRemoteDomain = class {
|
|
|
24669
25599
|
} catch (error) {
|
|
24670
25600
|
if (!isProjectListFallbackError(error)) throw error;
|
|
24671
25601
|
}
|
|
24672
|
-
if (roots.length > 0)
|
|
25602
|
+
if (roots.length > 0) {
|
|
25603
|
+
this.logger.debug("Codex workspace authority resolved from project/list", { projects: projectIds.size, roots: roots.length });
|
|
25604
|
+
return { projectIds, roots };
|
|
25605
|
+
}
|
|
25606
|
+
this.logger.warn("Codex project list yielded no roots; deriving workspaces from thread cwd", {
|
|
25607
|
+
projects: projectIds.size,
|
|
25608
|
+
listedThreads: listedThreads?.length ?? 0
|
|
25609
|
+
});
|
|
24673
25610
|
const threads = listedThreads === void 0 ? [] : [...listedThreads];
|
|
24674
25611
|
if (listedThreads === void 0) {
|
|
24675
25612
|
cursor2 = void 0;
|
|
@@ -24814,9 +25751,15 @@ var CodexRemoteDomain = class {
|
|
|
24814
25751
|
return paths;
|
|
24815
25752
|
}
|
|
24816
25753
|
};
|
|
24817
|
-
function codexBinaryCandidates(configured, hostPlatform = process.platform, userHome = homedir2()) {
|
|
25754
|
+
function codexBinaryCandidates(configured, hostPlatform = process.platform, userHome = homedir2(), localAppData = process.env.LOCALAPPDATA) {
|
|
24818
25755
|
if (configured !== "codex") return [configured];
|
|
24819
|
-
if (hostPlatform === "win32")
|
|
25756
|
+
if (hostPlatform === "win32") {
|
|
25757
|
+
return [.../* @__PURE__ */ new Set([
|
|
25758
|
+
...bundledWindowsCodex(userHome),
|
|
25759
|
+
...localAppData === void 0 ? [] : bundledWindowsCodexAt(localAppData),
|
|
25760
|
+
configured
|
|
25761
|
+
])];
|
|
25762
|
+
}
|
|
24820
25763
|
if (hostPlatform !== "darwin") return [configured];
|
|
24821
25764
|
const bundledCandidates = [
|
|
24822
25765
|
"/Applications/ChatGPT.app",
|
|
@@ -24829,7 +25772,7 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
|
|
|
24829
25772
|
return [];
|
|
24830
25773
|
}
|
|
24831
25774
|
const candidate = posix.join(codexCli, manifest.entrypoint);
|
|
24832
|
-
if (!
|
|
25775
|
+
if (!existsSync3(candidate)) return [];
|
|
24833
25776
|
accessSync(candidate, constants.X_OK);
|
|
24834
25777
|
return [candidate];
|
|
24835
25778
|
} catch {
|
|
@@ -24844,9 +25787,12 @@ function codexBinaryCandidates(configured, hostPlatform = process.platform, user
|
|
|
24844
25787
|
])];
|
|
24845
25788
|
}
|
|
24846
25789
|
function bundledWindowsCodex(userHome) {
|
|
24847
|
-
|
|
25790
|
+
return bundledWindowsCodexAt(join7(userHome, "AppData", "Local"));
|
|
25791
|
+
}
|
|
25792
|
+
function bundledWindowsCodexAt(localAppData) {
|
|
25793
|
+
const bin = join7(localAppData, "OpenAI", "Codex", "bin");
|
|
24848
25794
|
try {
|
|
24849
|
-
const newest = readdirSync2(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join7(bin, entry.name, "codex.exe")).filter((candidate2) =>
|
|
25795
|
+
const newest = readdirSync2(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join7(bin, entry.name, "codex.exe")).filter((candidate2) => existsSync3(candidate2)).map((candidate2) => ({ candidate: candidate2, modified: statSync2(candidate2).mtimeMs })).sort((left, right) => right.modified - left.modified);
|
|
24850
25796
|
const candidate = newest[0]?.candidate;
|
|
24851
25797
|
return candidate === void 0 ? [] : [candidate];
|
|
24852
25798
|
} catch {
|
|
@@ -24865,6 +25811,16 @@ function parseRespond(input2) {
|
|
|
24865
25811
|
}
|
|
24866
25812
|
return input2;
|
|
24867
25813
|
}
|
|
25814
|
+
function classifyAccountProbeFailure(error) {
|
|
25815
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25816
|
+
if (/unsupported_country_region_territory|country, region, or territory|403|forbidden/i.test(message)) {
|
|
25817
|
+
return new RpcError("CODEX_ACCOUNT_UNREACHABLE", "Codex cannot reach the account from this network region.");
|
|
25818
|
+
}
|
|
25819
|
+
if (/timed out|timeout/i.test(message)) {
|
|
25820
|
+
return new RpcError("CODEX_ACCOUNT_UNREACHABLE", "Codex could not reach the account in time.");
|
|
25821
|
+
}
|
|
25822
|
+
return error instanceof Error ? error : new RpcError("CODEX_UPSTREAM_ERROR", "Codex App Server could not complete the request.");
|
|
25823
|
+
}
|
|
24868
25824
|
function accountCanRun(result) {
|
|
24869
25825
|
if (!isRecord12(result) || typeof result.requiresOpenaiAuth !== "boolean") return false;
|
|
24870
25826
|
return result.requiresOpenaiAuth === false || isRecord12(result.account);
|
|
@@ -25128,7 +26084,6 @@ var PluginControlRuntime = class {
|
|
|
25128
26084
|
if (endpoint === "settings.get") return ok3(await this.settingsView());
|
|
25129
26085
|
if (endpoint === "settings.configure") return ok3(await this.configure(payload));
|
|
25130
26086
|
if (endpoint === "settings.server.set") return ok3(await this.setServer(payload));
|
|
25131
|
-
if (endpoint === "settings.role.set") return ok3(await this.setRole(payload));
|
|
25132
26087
|
if (endpoint === "settings.codex.set") return ok3(await this.setCodex(payload));
|
|
25133
26088
|
if (endpoint === "settings.acp.set") return ok3(await this.setAcp(payload));
|
|
25134
26089
|
if (endpoint === "settings.acp.add") return ok3(await this.addAcp(payload));
|
|
@@ -25183,7 +26138,7 @@ var PluginControlRuntime = class {
|
|
|
25183
26138
|
return ok3(await this.host.authorizeHostWithCode(value.code));
|
|
25184
26139
|
}
|
|
25185
26140
|
if (endpoint === "mode.set" && record5(payload).mode === "local") return ok3(this.hostOnlyStatus());
|
|
25186
|
-
throw new ClientModeError("METHOD_NOT_ALLOWED", "Remote Client mode is
|
|
26141
|
+
throw new ClientModeError("METHOD_NOT_ALLOWED", "Remote Client mode is unavailable in this profile.");
|
|
25187
26142
|
} catch (error) {
|
|
25188
26143
|
return fail3(error);
|
|
25189
26144
|
}
|
|
@@ -25193,22 +26148,20 @@ var PluginControlRuntime = class {
|
|
|
25193
26148
|
throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
|
|
25194
26149
|
}
|
|
25195
26150
|
const value = record5(payload);
|
|
25196
|
-
if (value.role !== "host" && value.role !== "client") {
|
|
25197
|
-
throw new ClientModeError("INVALID_MESSAGE", "Role must be Host or Client.");
|
|
25198
|
-
}
|
|
25199
26151
|
if (typeof value.serverUrl !== "string") {
|
|
25200
26152
|
throw new ClientModeError("INVALID_MESSAGE", "Server URL is required.");
|
|
25201
26153
|
}
|
|
25202
26154
|
const current = editableConfig(resolveConfig(this.settings.get()));
|
|
25203
|
-
const next = resolveConfig({ ...current,
|
|
25204
|
-
const
|
|
25205
|
-
|
|
25206
|
-
});
|
|
26155
|
+
const next = resolveConfig({ ...current, serverUrl: value.serverUrl });
|
|
26156
|
+
const directory = await ensureDeviceDirectory(this.identityDirectory, next.serverUrl);
|
|
26157
|
+
const identities = new IdentityStore({ directory });
|
|
25207
26158
|
const identity = await identities.loadOrCreate(hostname2());
|
|
25208
|
-
const
|
|
26159
|
+
const registrationCode = typeof value.registrationCode === "string" ? value.registrationCode.trim() : "";
|
|
26160
|
+
const hasRegistrationCode = registrationCode !== "";
|
|
26161
|
+
const api = hasRegistrationCode ? new HostServerApi(next.serverUrl, new ServerCredentialStore(directory)) : new ClientServerApi(next.serverUrl, new ServerCredentialStore(directory));
|
|
25209
26162
|
let authorization;
|
|
25210
|
-
if (
|
|
25211
|
-
authorization = await api.authorizeHostWithCode(identity,
|
|
26163
|
+
if (hasRegistrationCode) {
|
|
26164
|
+
authorization = await api.authorizeHostWithCode(identity, registrationCode);
|
|
25212
26165
|
} else if (value.provider === "deepseek") {
|
|
25213
26166
|
if (this.deepseekSession === void 0) {
|
|
25214
26167
|
throw new ClientModeError("METHOD_NOT_ALLOWED", "DeepSeek account sign-in is unavailable in this profile.");
|
|
@@ -25239,13 +26192,18 @@ var PluginControlRuntime = class {
|
|
|
25239
26192
|
}
|
|
25240
26193
|
authorization = await api.authorizeWithAccount(identity, value.email, value.password);
|
|
25241
26194
|
}
|
|
25242
|
-
|
|
25243
|
-
|
|
26195
|
+
this.client?.reloadCredentials?.();
|
|
26196
|
+
this.host?.reloadCredentials?.();
|
|
26197
|
+
const hostPaused = resolveConfig(this.settings.get()).hostControl?.paused === true;
|
|
26198
|
+
if (this.host !== void 0 && !hostPaused) {
|
|
26199
|
+
try {
|
|
26200
|
+
this.host.reconnectHost?.();
|
|
26201
|
+
} catch {
|
|
26202
|
+
}
|
|
25244
26203
|
}
|
|
25245
26204
|
await this.settings.replace(editableConfig(next));
|
|
25246
26205
|
return {
|
|
25247
26206
|
status: "authorized",
|
|
25248
|
-
role: value.role,
|
|
25249
26207
|
...authorization.account === void 0 ? {} : { account: authorization.account },
|
|
25250
26208
|
settings: await this.settingsView()
|
|
25251
26209
|
};
|
|
@@ -25263,22 +26221,6 @@ var PluginControlRuntime = class {
|
|
|
25263
26221
|
await this.settings.replace(editableConfig(next));
|
|
25264
26222
|
return this.settingsView();
|
|
25265
26223
|
}
|
|
25266
|
-
async setRole(payload) {
|
|
25267
|
-
if (this.settings === void 0) {
|
|
25268
|
-
throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
|
|
25269
|
-
}
|
|
25270
|
-
const role = record5(payload).role;
|
|
25271
|
-
if (role !== "host" && role !== "client") {
|
|
25272
|
-
throw new ClientModeError("INVALID_MESSAGE", "Role must be Host or Client.");
|
|
25273
|
-
}
|
|
25274
|
-
const current = editableConfig(resolveConfig(this.settings.get()));
|
|
25275
|
-
const currentRole = current.role === "client" ? "client" : "host";
|
|
25276
|
-
if (role !== currentRole && current.serverUrl !== void 0 && await this.association(current.serverUrl, role) === void 0) {
|
|
25277
|
-
await this.authorizeOwnedRole(current.serverUrl, currentRole, role);
|
|
25278
|
-
}
|
|
25279
|
-
await this.settings.replace({ ...current, role });
|
|
25280
|
-
return this.settingsView();
|
|
25281
|
-
}
|
|
25282
26224
|
async setDevelopment(payload) {
|
|
25283
26225
|
if (this.settings === void 0) throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
|
|
25284
26226
|
const value = record5(payload);
|
|
@@ -25320,6 +26262,7 @@ var PluginControlRuntime = class {
|
|
|
25320
26262
|
}
|
|
25321
26263
|
});
|
|
25322
26264
|
await this.settings.replace(editableConfig(next));
|
|
26265
|
+
void this.host?.restartCodex?.(next.codex);
|
|
25323
26266
|
return this.settingsView();
|
|
25324
26267
|
}
|
|
25325
26268
|
async setAcp(payload) {
|
|
@@ -25355,18 +26298,6 @@ var PluginControlRuntime = class {
|
|
|
25355
26298
|
await this.settings.replace(editableConfig({ ...current, acp: { enabled: current.acp?.enabled ?? true, backends } }));
|
|
25356
26299
|
return this.settingsView();
|
|
25357
26300
|
}
|
|
25358
|
-
async authorizeOwnedRole(serverUrl, sourceRole, targetRole) {
|
|
25359
|
-
const sourceDirectory = serverStorageDirectory(this.identityDirectory, serverUrl, sourceRole);
|
|
25360
|
-
const sourceIdentity = await new IdentityStore({ directory: sourceDirectory }).loadOrCreate(hostname2());
|
|
25361
|
-
const sourceStore = new ServerCredentialStore(sourceDirectory);
|
|
25362
|
-
if (await sourceStore.load(serverUrl, sourceIdentity.deviceId) === void 0) return;
|
|
25363
|
-
const sourceApi = sourceRole === "host" ? new HostServerApi(serverUrl, sourceStore) : new ClientServerApi(serverUrl, sourceStore);
|
|
25364
|
-
const sourceCredentials = await sourceApi.authenticate(sourceIdentity);
|
|
25365
|
-
const targetDirectory = serverStorageDirectory(this.identityDirectory, serverUrl, targetRole);
|
|
25366
|
-
const targetIdentity = await new IdentityStore({ directory: targetDirectory }).loadOrCreate(hostname2());
|
|
25367
|
-
const targetApi = targetRole === "host" ? new HostServerApi(serverUrl, new ServerCredentialStore(targetDirectory)) : new ClientServerApi(serverUrl, new ServerCredentialStore(targetDirectory));
|
|
25368
|
-
await targetApi.authorizeOwnedRole(targetIdentity, sourceCredentials.accessToken, sourceCredentials.account);
|
|
25369
|
-
}
|
|
25370
26301
|
async logout() {
|
|
25371
26302
|
if (this.settings === void 0) {
|
|
25372
26303
|
throw new ClientModeError("SETTINGS_UNAVAILABLE", "DSH user settings are unavailable in this profile.");
|
|
@@ -25379,10 +26310,8 @@ var PluginControlRuntime = class {
|
|
|
25379
26310
|
this.client?.clearClientAuthorization(),
|
|
25380
26311
|
this.host?.clearHostAuthorization()
|
|
25381
26312
|
]);
|
|
25382
|
-
await
|
|
25383
|
-
|
|
25384
|
-
await new ServerCredentialStore(directory).clear();
|
|
25385
|
-
}));
|
|
26313
|
+
const directory = await ensureDeviceDirectory(this.identityDirectory, config.serverUrl);
|
|
26314
|
+
await new ServerCredentialStore(directory).clear();
|
|
25386
26315
|
}
|
|
25387
26316
|
let deepseekSignedOut = false;
|
|
25388
26317
|
if (this.deepseekSession !== void 0) {
|
|
@@ -25422,9 +26351,9 @@ var PluginControlRuntime = class {
|
|
|
25422
26351
|
async settingsView() {
|
|
25423
26352
|
const config = this.settings === void 0 ? editableConfig(this.config) : editableConfig(resolveConfig(this.settings.get()));
|
|
25424
26353
|
const associations = await this.associations(config);
|
|
25425
|
-
const
|
|
25426
|
-
const association = associations[role];
|
|
26354
|
+
const association = associations.host;
|
|
25427
26355
|
const discovered = discoveredCodexBinary(config.codex?.binary ?? "codex");
|
|
26356
|
+
const codexStatus = this.host?.codexStatus?.();
|
|
25428
26357
|
return {
|
|
25429
26358
|
config,
|
|
25430
26359
|
deviceName: hostname2(),
|
|
@@ -25433,27 +26362,25 @@ var PluginControlRuntime = class {
|
|
|
25433
26362
|
associations,
|
|
25434
26363
|
acpAvailability: Object.fromEntries((config.acp?.backends ?? []).map((item) => [item.id, commandAvailable(item.command ?? "")])),
|
|
25435
26364
|
...association === void 0 ? {} : { association },
|
|
25436
|
-
...discovered === void 0 ? {} : { discoveredCodexBinary: discovered }
|
|
26365
|
+
...discovered === void 0 ? {} : { discoveredCodexBinary: discovered },
|
|
26366
|
+
...codexStatus === void 0 ? {} : { codexStatus }
|
|
25437
26367
|
};
|
|
25438
26368
|
}
|
|
25439
26369
|
async associations(config) {
|
|
25440
26370
|
if (config.serverUrl === void 0) return {};
|
|
25441
|
-
const
|
|
25442
|
-
|
|
25443
|
-
|
|
25444
|
-
]);
|
|
25445
|
-
return {
|
|
25446
|
-
...host === void 0 ? {} : { host },
|
|
25447
|
-
...client === void 0 ? {} : { client }
|
|
25448
|
-
};
|
|
26371
|
+
const association = await this.association(config.serverUrl);
|
|
26372
|
+
if (association === void 0) return {};
|
|
26373
|
+
return { host: association, client: association };
|
|
25449
26374
|
}
|
|
25450
|
-
async association(serverUrl
|
|
26375
|
+
async association(serverUrl) {
|
|
25451
26376
|
const identities = new IdentityStore({
|
|
25452
|
-
directory:
|
|
26377
|
+
directory: await ensureDeviceDirectory(this.identityDirectory, serverUrl)
|
|
25453
26378
|
});
|
|
25454
26379
|
const identity = await identities.loadOrCreate(hostname2());
|
|
25455
26380
|
const credentials = await new ServerCredentialStore(identities.directory).load(serverUrl, identity.deviceId);
|
|
25456
26381
|
if (credentials === void 0) return void 0;
|
|
26382
|
+
const hostStatus = this.host?.hostStatus();
|
|
26383
|
+
if (hostStatus?.error !== void 0 && HOST_AUTHORIZATION_ERRORS.has(hostStatus.error)) return void 0;
|
|
25457
26384
|
return {
|
|
25458
26385
|
method: credentials.authorizationMethod,
|
|
25459
26386
|
...credentials.account === void 0 ? {} : { account: credentials.account }
|
|
@@ -25479,14 +26406,13 @@ function commandAvailable(command) {
|
|
|
25479
26406
|
}
|
|
25480
26407
|
function discoveredCodexBinary(configured) {
|
|
25481
26408
|
for (const candidate of codexBinaryCandidates(configured)) {
|
|
25482
|
-
if (candidate !== configured &&
|
|
26409
|
+
if (candidate !== configured && existsSync4(candidate)) return candidate;
|
|
25483
26410
|
}
|
|
25484
26411
|
return void 0;
|
|
25485
26412
|
}
|
|
25486
26413
|
function editableConfig(config) {
|
|
25487
26414
|
return {
|
|
25488
26415
|
enabled: config.enabled,
|
|
25489
|
-
role: config.role,
|
|
25490
26416
|
...config.serverUrl === void 0 ? {} : { serverUrl: config.serverUrl },
|
|
25491
26417
|
terminal: config.terminal,
|
|
25492
26418
|
hostControl: config.hostControl ?? { enabled: true, paused: false },
|
|
@@ -25725,10 +26651,6 @@ var ConnectionController = class {
|
|
|
25725
26651
|
const peers = this.peerDeviceIds();
|
|
25726
26652
|
return peers.length === 1 ? peers[0] : void 0;
|
|
25727
26653
|
}
|
|
25728
|
-
connectionMode() {
|
|
25729
|
-
const connection = this.active.values().next().value;
|
|
25730
|
-
return connection?.channel.mode ?? (connection === void 0 ? "Disconnected" : "Relay");
|
|
25731
|
-
}
|
|
25732
26654
|
async send(message) {
|
|
25733
26655
|
await Promise.all([...this.active.values()].map((connection) => this.sendConnection(connection, message)));
|
|
25734
26656
|
}
|
|
@@ -26434,7 +27356,6 @@ var API_TRANSFER_IDLE_MS = TRANSFER_IDLE_MS;
|
|
|
26434
27356
|
var INLINE_TRANSFER_RESPONSE_BYTES2 = 2 * 1024 * 1024;
|
|
26435
27357
|
var HarnessApiBridge = class {
|
|
26436
27358
|
constructor(api, publish, maxStreams = 8, logger, typertGateway, harnessVersion) {
|
|
26437
|
-
this.api = api;
|
|
26438
27359
|
this.publish = publish;
|
|
26439
27360
|
this.maxStreams = maxStreams;
|
|
26440
27361
|
this.logger = logger;
|
|
@@ -27671,9 +28592,6 @@ var CodexWorkspaceBridge = class {
|
|
|
27671
28592
|
terminals;
|
|
27672
28593
|
ownedSubscribers = /* @__PURE__ */ new Set();
|
|
27673
28594
|
spawnTerminal;
|
|
27674
|
-
isCodeXScope(value) {
|
|
27675
|
-
return parseCodexSessionId(value) !== void 0;
|
|
27676
|
-
}
|
|
27677
28595
|
async call(endpoint, payload, signal) {
|
|
27678
28596
|
const args = argsOf(payload);
|
|
27679
28597
|
const scope = args.workspaceFileScopeId;
|
|
@@ -28113,7 +29031,7 @@ var HostPluginRuntime = class {
|
|
|
28113
29031
|
this.harnessVersion,
|
|
28114
29032
|
new TerminalPolicy(() => this.terminalEnabled, context.peerDeviceId, this.terminalOwners),
|
|
28115
29033
|
new CodexWorkspaceBridge(
|
|
28116
|
-
(threadId
|
|
29034
|
+
(threadId) => this.codex.resolveThreadWorkspace(context.connectionId, threadId),
|
|
28117
29035
|
() => this.terminalEnabled,
|
|
28118
29036
|
this.codexWorkspaceState,
|
|
28119
29037
|
this.terminalSpawner
|
|
@@ -28258,9 +29176,15 @@ var HostPluginRuntime = class {
|
|
|
28258
29176
|
* user who only wants to stop being remotely reachable would have to authorize
|
|
28259
29177
|
* again and would consume a device identity. Pausing closes the connection and
|
|
28260
29178
|
* keeps both.
|
|
29179
|
+
*
|
|
29180
|
+
* The Server's own flag is written here as well, not only by the panel's authorization endpoint:
|
|
29181
|
+
* it is what makes the device read as "not accepting control" to other clients and what makes the
|
|
29182
|
+
* Server refuse the next `role=host` hello. Leaving it to one entry point is how a device could
|
|
29183
|
+
* look online and controlling while its switch said off.
|
|
28261
29184
|
*/
|
|
28262
29185
|
async pauseHostConnection() {
|
|
28263
29186
|
this.paused = true;
|
|
29187
|
+
await this.recordHostControl(false);
|
|
28264
29188
|
await this.serverConnection?.stop();
|
|
28265
29189
|
this.logger.info("Host connection paused");
|
|
28266
29190
|
}
|
|
@@ -28268,18 +29192,51 @@ var HostPluginRuntime = class {
|
|
|
28268
29192
|
async resumeHostConnection() {
|
|
28269
29193
|
const wasPaused = this.paused;
|
|
28270
29194
|
this.paused = false;
|
|
29195
|
+
await this.recordHostControl(true);
|
|
28271
29196
|
this.serverConnection?.resume();
|
|
28272
29197
|
if (wasPaused) this.logger.info("Host connection resumed");
|
|
28273
29198
|
}
|
|
28274
|
-
|
|
28275
|
-
|
|
29199
|
+
/**
|
|
29200
|
+
* Mirror the control switch onto the Server.
|
|
29201
|
+
*
|
|
29202
|
+
* A machine that is signed out has no credential to write it with, and that is not an error: the
|
|
29203
|
+
* local pause still holds, and the next sign-in writes the flag from the switch.
|
|
29204
|
+
* @param enabled - whether this machine accepts control.
|
|
29205
|
+
*/
|
|
29206
|
+
async recordHostControl(enabled) {
|
|
29207
|
+
try {
|
|
29208
|
+
await this.serverApi?.setHostControl(enabled);
|
|
29209
|
+
} catch (error) {
|
|
29210
|
+
this.logger.warn("could not record the control switch on the Server", { code: errorCode5(error) });
|
|
29211
|
+
}
|
|
29212
|
+
}
|
|
29213
|
+
/** Re-read this device's credentials from disk, so a sign-in is picked up without a restart. */
|
|
29214
|
+
reloadCredentials() {
|
|
29215
|
+
this.serverApi?.reloadCredentials();
|
|
29216
|
+
}
|
|
29217
|
+
/**
|
|
29218
|
+
* Record whether this machine accepts control, on the Server.
|
|
29219
|
+
*
|
|
29220
|
+
* This is the explicit, failing form, used when the panel asks for the switch and must be told if the
|
|
29221
|
+
* Server refused. {@link pauseHostConnection} and {@link resumeHostConnection} write the same flag
|
|
29222
|
+
* best-effort, so the two stay consistent whichever entry point the caller used.
|
|
29223
|
+
*/
|
|
29224
|
+
async setHostControl(enabled) {
|
|
29225
|
+
await this.serverApi?.setHostControl(enabled);
|
|
29226
|
+
this.logger.info(enabled ? "Host control enabled" : "Host control disabled");
|
|
28276
29227
|
}
|
|
28277
29228
|
reconnectHost() {
|
|
28278
29229
|
if (this.closed) throw new Error("remote runtime is closed");
|
|
28279
29230
|
if (this.serverConnection === void 0) {
|
|
28280
29231
|
throw new ServerApiError("SERVER_NOT_CONFIGURED", "Configure serverUrl before reconnecting.", false);
|
|
28281
29232
|
}
|
|
28282
|
-
this.paused
|
|
29233
|
+
if (this.paused) {
|
|
29234
|
+
throw new ServerApiError(
|
|
29235
|
+
"CONTROL_DISABLED",
|
|
29236
|
+
'This device is not accepting control. Turn on "Allow control of this device" first.',
|
|
29237
|
+
false
|
|
29238
|
+
);
|
|
29239
|
+
}
|
|
28283
29240
|
this.serverConnection.reconnect();
|
|
28284
29241
|
}
|
|
28285
29242
|
async startHostOAuthQrLogin(provider) {
|
|
@@ -28322,26 +29279,6 @@ var HostPluginRuntime = class {
|
|
|
28322
29279
|
}
|
|
28323
29280
|
this.logger.info("Host authorization cleared");
|
|
28324
29281
|
}
|
|
28325
|
-
async authorizeHostAsOwned(accessToken, account) {
|
|
28326
|
-
if (this.serverApi === void 0) {
|
|
28327
|
-
throw new ServerApiError("SERVER_NOT_CONFIGURED", "Configure serverUrl before enabling Host access.", false);
|
|
28328
|
-
}
|
|
28329
|
-
let result;
|
|
28330
|
-
try {
|
|
28331
|
-
result = await this.serverApi.authorizeOwnedRole(this.currentIdentity(), accessToken, account);
|
|
28332
|
-
} catch (error) {
|
|
28333
|
-
if (!(error instanceof ServerApiError) || error.code !== "DEVICE_REVOKED") throw error;
|
|
28334
|
-
await this.serverConnection?.stop();
|
|
28335
|
-
this.identity = await this.identities.reset(this.config.deviceName);
|
|
28336
|
-
this.serverApi.bindIdentity(this.identity);
|
|
28337
|
-
this.serverConnection = this.createServerConnection(this.identity);
|
|
28338
|
-
result = await this.serverApi.authorizeOwnedRole(this.identity, accessToken, account);
|
|
28339
|
-
this.logger.info("Rotated revoked Host identity before owned-device authorization");
|
|
28340
|
-
}
|
|
28341
|
-
if (!this.paused) this.serverConnection?.resume();
|
|
28342
|
-
this.logger.info("Host authorized as an owned device");
|
|
28343
|
-
return result;
|
|
28344
|
-
}
|
|
28345
29282
|
async authorizeHostWithAccount(email, password) {
|
|
28346
29283
|
if (this.serverApi === void 0) {
|
|
28347
29284
|
throw new ServerApiError("SERVER_NOT_CONFIGURED", "Configure serverUrl before signing in.", false);
|
|
@@ -28365,6 +29302,22 @@ var HostPluginRuntime = class {
|
|
|
28365
29302
|
if (revoked) await this.connections.revoke(deviceId);
|
|
28366
29303
|
return revoked;
|
|
28367
29304
|
}
|
|
29305
|
+
/**
|
|
29306
|
+
* Restart the Codex connection so a changed setting can apply without restarting DSH.
|
|
29307
|
+
*
|
|
29308
|
+
* The domain is restarted in place, which keeps every connection's Codex bridge alive across the change.
|
|
29309
|
+
* Replacing the instance instead (close the old, build a new one) left each attached client holding a bridge
|
|
29310
|
+
* that belonged to a retired domain: every later Codex call answered "The Codex connection is closed" until
|
|
29311
|
+
* that client happened to reconnect on its own - and nothing in the panel could bring it back.
|
|
29312
|
+
* @returns when the restart attempt finished.
|
|
29313
|
+
*/
|
|
29314
|
+
async restartCodex(config) {
|
|
29315
|
+
await this.codex.restart(config);
|
|
29316
|
+
this.logger.info("Codex Remote domain restarted", {
|
|
29317
|
+
enabled: config.enabled,
|
|
29318
|
+
available: this.codex.isAvailable()
|
|
29319
|
+
});
|
|
29320
|
+
}
|
|
28368
29321
|
codexStatus() {
|
|
28369
29322
|
return this.codex.status();
|
|
28370
29323
|
}
|
|
@@ -28428,12 +29381,12 @@ var HostPluginRuntime = class {
|
|
|
28428
29381
|
}
|
|
28429
29382
|
async readHarnessVersion() {
|
|
28430
29383
|
let reportedVersion;
|
|
28431
|
-
let
|
|
29384
|
+
let errorCode7;
|
|
28432
29385
|
try {
|
|
28433
29386
|
const response = await this.apiProxy?.host.describe({ rpcId: randomUUID4(), payload: {} });
|
|
28434
29387
|
if (response === void 0) throw new Error("ApiProxy is unavailable");
|
|
28435
29388
|
if (!response.result.ok) {
|
|
28436
|
-
|
|
29389
|
+
errorCode7 = response.result.error.code;
|
|
28437
29390
|
} else {
|
|
28438
29391
|
reportedVersion = normalizeHarnessVersion(response.result.value.version);
|
|
28439
29392
|
}
|
|
@@ -28442,7 +29395,7 @@ var HostPluginRuntime = class {
|
|
|
28442
29395
|
const distributionVersion = reportedVersion === void 0 || reportedVersion === "0.0.1" ? await readHarnessDistributionVersion() : void 0;
|
|
28443
29396
|
const version = selectHarnessVersion(reportedVersion, distributionVersion);
|
|
28444
29397
|
if (version !== void 0) return version;
|
|
28445
|
-
this.logger.warn("Harness version is unavailable",
|
|
29398
|
+
this.logger.warn("Harness version is unavailable", errorCode7 === void 0 ? void 0 : { code: errorCode7 });
|
|
28446
29399
|
return void 0;
|
|
28447
29400
|
}
|
|
28448
29401
|
hostCapabilities() {
|
|
@@ -28493,6 +29446,10 @@ var HostPluginRuntime = class {
|
|
|
28493
29446
|
function shortId5(value) {
|
|
28494
29447
|
return value.length <= 12 ? value : `${value.slice(0, 8)}\u2026${value.slice(-4)}`;
|
|
28495
29448
|
}
|
|
29449
|
+
function errorCode5(error) {
|
|
29450
|
+
const own = error !== null && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
29451
|
+
return own ?? safeErrorCode(error);
|
|
29452
|
+
}
|
|
28496
29453
|
function isPlainRecord(value) {
|
|
28497
29454
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28498
29455
|
}
|
|
@@ -28819,7 +29776,7 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
|
|
|
28819
29776
|
} catch (error) {
|
|
28820
29777
|
return {
|
|
28821
29778
|
kind: "error",
|
|
28822
|
-
text: `Local Remote Host credentials were cleared, but Server revocation failed (${
|
|
29779
|
+
text: `Local Remote Host credentials were cleared, but Server revocation failed (${errorCode6(error)}).`
|
|
28823
29780
|
};
|
|
28824
29781
|
}
|
|
28825
29782
|
}
|
|
@@ -28875,7 +29832,7 @@ function registerOptional(ctx, feature, register2) {
|
|
|
28875
29832
|
return register2() ?? (() => {
|
|
28876
29833
|
});
|
|
28877
29834
|
} catch (error) {
|
|
28878
|
-
ctx.logger.warn(`dsh-TUI Remote ${feature} is unavailable`, { code:
|
|
29835
|
+
ctx.logger.warn(`dsh-TUI Remote ${feature} is unavailable`, { code: errorCode6(error) });
|
|
28879
29836
|
return () => {
|
|
28880
29837
|
};
|
|
28881
29838
|
}
|
|
@@ -28988,7 +29945,7 @@ var RemoteLoginController = class {
|
|
|
28988
29945
|
await this.poll(attempt, provider, session, target2.runtime);
|
|
28989
29946
|
} catch (error) {
|
|
28990
29947
|
if (attempt !== this.attempt) return;
|
|
28991
|
-
this.update({ phase: "error", provider, error:
|
|
29948
|
+
this.update({ phase: "error", provider, error: errorCode6(error) });
|
|
28992
29949
|
}
|
|
28993
29950
|
}
|
|
28994
29951
|
async poll(attempt, provider, session, runtime) {
|
|
@@ -29106,7 +30063,7 @@ function terminalLink2(url) {
|
|
|
29106
30063
|
function printableWidth(value) {
|
|
29107
30064
|
return value.replace(/\u001B\[[0-9;]*m/gu, "").length;
|
|
29108
30065
|
}
|
|
29109
|
-
function
|
|
30066
|
+
function errorCode6(error) {
|
|
29110
30067
|
if (error instanceof ServerApiError) return error.code;
|
|
29111
30068
|
if (error instanceof Error && "code" in error && typeof error.code === "string") return error.code;
|
|
29112
30069
|
return "CONNECTION_FAILED";
|
|
@@ -29281,9 +30238,8 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
|
|
|
29281
30238
|
console.error(message);
|
|
29282
30239
|
}
|
|
29283
30240
|
}, config.logLevel);
|
|
29284
|
-
const
|
|
29285
|
-
|
|
29286
|
-
});
|
|
30241
|
+
const deviceDirectory = config.serverUrl === void 0 ? defaultIdentityDirectory : await ensureDeviceDirectory(defaultIdentityDirectory, config.serverUrl);
|
|
30242
|
+
const hostIdentities = new IdentityStore({ directory: deviceDirectory });
|
|
29287
30243
|
const apiProxy = ctx.get("apiProxy");
|
|
29288
30244
|
const nativeTypertGateway = ctx.get("typertGateway");
|
|
29289
30245
|
const localTypertGateway = new TypertGatewaySwitch(nativeTypertGateway).local();
|
|
@@ -29324,9 +30280,7 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
|
|
|
29324
30280
|
let clientRuntime;
|
|
29325
30281
|
const hostControl = runtime;
|
|
29326
30282
|
if (config.serverUrl !== void 0 && connection !== void 0) {
|
|
29327
|
-
const clientIdentities =
|
|
29328
|
-
directory: serverStorageDirectory(defaultIdentityDirectory, config.serverUrl, "client")
|
|
29329
|
-
});
|
|
30283
|
+
const clientIdentities = hostIdentities;
|
|
29330
30284
|
clientRuntime = new ClientModeRuntime(
|
|
29331
30285
|
config,
|
|
29332
30286
|
clientIdentities,
|