@codexhost/cli-linux-arm64 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4295,9 +4295,11 @@ async function runDelegationCli(input) {
4295
4295
  "INVALID_ARGUMENT",
4296
4296
  "--host conflicts with the Thread reference"
4297
4297
  );
4298
+ const caller = command === "read" ? callerThreadId(environment) : void 0;
4298
4299
  const body = {
4299
4300
  ...reference,
4300
4301
  ...hostId2 ? { hostId: hostId2 } : {},
4302
+ ...caller ? { callerThreadId: normalizeThreadId(caller) } : {},
4301
4303
  view,
4302
4304
  ...value(parsed2, "--cursor") ? { cursor: value(parsed2, "--cursor") } : {},
4303
4305
  ...value(parsed2, "--limit") ? { limit: positiveInteger(value(parsed2, "--limit"), "--limit", MAX_LIMIT) } : {},
@@ -4434,6 +4436,7 @@ import {
4434
4436
  realpath,
4435
4437
  stat,
4436
4438
  readdir as readdir2,
4439
+ unlink,
4437
4440
  writeFile as writeFile3
4438
4441
  } from "node:fs/promises";
4439
4442
  import { homedir } from "node:os";
@@ -19943,6 +19946,7 @@ config(en_default());
19943
19946
  var REMOTE_THREAD_REPLY_MAX_BYTES = 16 * 1024 * 1024;
19944
19947
  var REMOTE_THREAD_CONTROL_TIMEOUT_MS = 4e4;
19945
19948
  var DELEGATION_READ_METHOD = "codexhost/thread/delegation-read";
19949
+ var THREAD_REFERENCE_SOURCE_HOST_PARAM = "codexhostSourceHostId";
19946
19950
  var delegationReadParamsSchema = external_exports.strictObject({
19947
19951
  threadId: external_exports.string().min(1).max(1024),
19948
19952
  view: external_exports.enum(["result", "messages"]),
@@ -21467,14 +21471,37 @@ var RuntimeMaintenance = class {
21467
21471
  options2.environment.CODEXHOST_DATA_DIR ?? path7.join(home, ".codexhost", "remote", "data"),
21468
21472
  "remote-update.json"
21469
21473
  );
21474
+ this.#cancelPath = path7.join(path7.dirname(this.#statusPath), "remote-update.cancel");
21470
21475
  }
21471
21476
  options;
21472
21477
  #running;
21473
21478
  #statusPath;
21479
+ #cancelPath;
21474
21480
  #update = { phase: "idle", targetVersion: null, error: null };
21475
21481
  #blocked = false;
21482
+ #watch = null;
21476
21483
  #starting = null;
21477
21484
  #startingVersion = null;
21485
+ #ensureWatching() {
21486
+ if (this.#watch) return;
21487
+ this.#watch = setInterval(() => {
21488
+ void this.status().catch(() => void 0).then(() => {
21489
+ if (!this.#blocked && this.#watch) {
21490
+ clearInterval(this.#watch);
21491
+ this.#watch = null;
21492
+ }
21493
+ });
21494
+ }, UPDATER_WATCH_INTERVAL_MS);
21495
+ this.#watch.unref();
21496
+ }
21497
+ async #recordStartFailure(version2, failure) {
21498
+ await writeFile3(this.#cancelPath, JSON.stringify({ targetVersion: version2 }), { mode: 384 });
21499
+ await writeFile3(
21500
+ this.#statusPath,
21501
+ JSON.stringify({ phase: "failed", targetVersion: version2, error: failure }),
21502
+ { mode: 384 }
21503
+ );
21504
+ }
21478
21505
  #sshPending = /* @__PURE__ */ new Set();
21479
21506
  async setupSsh(input) {
21480
21507
  if (this.options.remote) throw new Error("SSH installation must run from the local computer");
@@ -21535,7 +21562,7 @@ var RuntimeMaintenance = class {
21535
21562
  installation(this.options.runtimePath, this.options.environment),
21536
21563
  this.#resources()
21537
21564
  ]);
21538
- if (this.#update.phase === "idle" || this.#blocked) {
21565
+ if (this.#update.phase === "idle" || this.#blocked || this.#update.phase === "failed") {
21539
21566
  try {
21540
21567
  const value2 = JSON.parse(await readFile5(this.#statusPath, "utf8"));
21541
21568
  const parsed2 = runtimeStatusSchema.shape.update.parse(value2);
@@ -21558,8 +21585,15 @@ var RuntimeMaintenance = class {
21558
21585
  parsed2.error = "The previous remote update was interrupted; retry the update";
21559
21586
  }
21560
21587
  }
21561
- if (this.#update === observedUpdate) this.#update = parsed2;
21562
- if (["failed", "succeeded"].includes(this.#update.phase)) this.#blocked = false;
21588
+ if (this.#update === observedUpdate) {
21589
+ this.#update = parsed2;
21590
+ if (["installing", "restarting"].includes(this.#update.phase)) {
21591
+ this.#blocked = true;
21592
+ this.#ensureWatching();
21593
+ } else if (["failed", "succeeded"].includes(this.#update.phase)) {
21594
+ this.#blocked = false;
21595
+ }
21596
+ }
21563
21597
  } catch {
21564
21598
  }
21565
21599
  }
@@ -21631,6 +21665,7 @@ var RuntimeMaintenance = class {
21631
21665
  await chmod2(helper, 448);
21632
21666
  const request = path7.join(work, "request.json");
21633
21667
  this.#update = { phase: "installing", targetVersion: version2, error: null };
21668
+ await unlink(this.#cancelPath).catch(() => void 0);
21634
21669
  await writeFile3(this.#statusPath, JSON.stringify(this.#update), { mode: 384 });
21635
21670
  await writeFile3(
21636
21671
  request,
@@ -21649,35 +21684,38 @@ var RuntimeMaintenance = class {
21649
21684
  );
21650
21685
  const starter = spawn3(
21651
21686
  "/bin/sh",
21652
- ["-c", '"$0" remote --request "$1" </dev/null >/dev/null 2>&1 &', helper, request],
21653
- { detached: true, stdio: "ignore", env: this.options.environment }
21687
+ ["-c", '"$0" remote --request "$1" </dev/null >/dev/null 2>&1 & echo $!', helper, request],
21688
+ { detached: true, stdio: ["ignore", "pipe", "ignore"], env: this.options.environment }
21654
21689
  );
21690
+ let pidOutput = "";
21691
+ starter.stdout?.on("data", (chunk) => {
21692
+ pidOutput += typeof chunk === "string" ? chunk : chunk.toString("utf8");
21693
+ });
21655
21694
  await new Promise((resolve, reject) => {
21656
21695
  starter.once("error", reject);
21657
21696
  starter.once(
21658
- "exit",
21697
+ "close",
21659
21698
  (code) => code === 0 ? resolve() : reject(new Error("Remote updater could not be started"))
21660
21699
  );
21661
21700
  });
21701
+ const helperPid = Number.parseInt(pidOutput.trim(), 10);
21702
+ if (!Number.isSafeInteger(helperPid) || helperPid <= 0) {
21703
+ throw new Error("Remote updater could not be started");
21704
+ }
21662
21705
  const deadline = Date.now() + UPDATER_START_TIMEOUT_MS;
21663
21706
  while (!await this.#helperReported()) {
21664
21707
  if (Date.now() >= deadline) {
21708
+ try {
21709
+ process.kill(helperPid);
21710
+ } catch {
21711
+ }
21665
21712
  const failure = "Remote updater did not start";
21666
- await writeFile3(
21667
- this.#statusPath,
21668
- JSON.stringify({ phase: "failed", targetVersion: version2, error: failure }),
21669
- { mode: 384 }
21670
- );
21713
+ await this.#recordStartFailure(version2, failure);
21671
21714
  throw new Error(failure);
21672
21715
  }
21673
21716
  await new Promise((resolve) => setTimeout(resolve, 50));
21674
21717
  }
21675
- const watch = setInterval(() => {
21676
- void this.status().catch(() => void 0).then(() => {
21677
- if (!this.#blocked) clearInterval(watch);
21678
- });
21679
- }, UPDATER_WATCH_INTERVAL_MS);
21680
- watch.unref();
21718
+ this.#ensureWatching();
21681
21719
  }
21682
21720
  async #helperReported() {
21683
21721
  try {
@@ -21691,6 +21729,7 @@ var RuntimeMaintenance = class {
21691
21729
 
21692
21730
  // packages/host-runtime/src/run-host-runtime.ts
21693
21731
  import { randomBytes as randomBytes4 } from "node:crypto";
21732
+ import { statSync } from "node:fs";
21694
21733
  import path37 from "node:path";
21695
21734
  import { homedir as homedir4 } from "node:os";
21696
21735
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -31745,6 +31784,16 @@ var ExternalThreadIdleRelease = class {
31745
31784
  const state = this.#activity.get(thread);
31746
31785
  if (state) state.outputFailed = true;
31747
31786
  }
31787
+ /** After a successful Session revival, allow idle release again. */
31788
+ clearOutputFailed(thread) {
31789
+ const state = this.#activity.get(thread);
31790
+ if (state) {
31791
+ state.outputFailed = false;
31792
+ state.lastActivity = Date.now();
31793
+ } else {
31794
+ this.touch(thread);
31795
+ }
31796
+ }
31748
31797
  failure(thread) {
31749
31798
  return this.#activity.get(thread)?.closeFailed ? CLOSE_FAILURE : void 0;
31750
31799
  }
@@ -31893,6 +31942,80 @@ var ExternalThreadIdleRelease = class {
31893
31942
  }
31894
31943
  };
31895
31944
 
31945
+ // packages/host-runtime/src/external-thread-lifecycle.ts
31946
+ var RELEASE_TIMEOUT_MS = 6e4;
31947
+ var ExternalSessionReleaseError = class extends Error {
31948
+ constructor(cause) {
31949
+ super(
31950
+ "External Session shutdown could not be confirmed. Retry to confirm release; recovery is blocked until the old Session closes and its output drains. If the Harness cannot retry shutdown, stop residual processes and restart Codex Desktop.",
31951
+ { cause }
31952
+ );
31953
+ this.name = "ExternalSessionReleaseError";
31954
+ }
31955
+ };
31956
+ var ExternalThreadLifecycle = class {
31957
+ #pending = /* @__PURE__ */ new Map();
31958
+ #closing = /* @__PURE__ */ new WeakMap();
31959
+ #releasing = /* @__PURE__ */ new Map();
31960
+ busy(id2) {
31961
+ return this.#pending.has(id2) || this.#releasing.has(id2);
31962
+ }
31963
+ pending() {
31964
+ return [...this.#pending];
31965
+ }
31966
+ run(id2, operation) {
31967
+ const previous = this.#pending.get(id2) ?? Promise.resolve();
31968
+ const task = previous.then(async () => {
31969
+ const releasing = this.#releasing.get(id2);
31970
+ if (releasing) await this.release(id2, releasing.session, releasing.output);
31971
+ return operation();
31972
+ });
31973
+ const settled = task.then(
31974
+ () => void 0,
31975
+ () => void 0
31976
+ );
31977
+ this.#pending.set(id2, settled);
31978
+ void settled.then(() => {
31979
+ if (this.#pending.get(id2) === settled) this.#pending.delete(id2);
31980
+ });
31981
+ return task;
31982
+ }
31983
+ /** Call inside the Thread's lifecycle operation. A timeout never cancels the owned close. */
31984
+ async release(id2, session, output) {
31985
+ this.#releasing.set(id2, { session, output });
31986
+ let closing = this.#closing.get(session);
31987
+ if (!closing) {
31988
+ closing = Promise.resolve().then(() => session.close());
31989
+ this.#closing.set(session, closing);
31990
+ const attempt = closing;
31991
+ void closing.catch(() => {
31992
+ if (this.#closing.get(session) === attempt) this.#closing.delete(session);
31993
+ });
31994
+ }
31995
+ let timeout;
31996
+ try {
31997
+ await Promise.race([
31998
+ closing.then(() => output),
31999
+ new Promise((_, reject) => {
32000
+ timeout = setTimeout(
32001
+ () => reject(new Error("Session shutdown timed out")),
32002
+ RELEASE_TIMEOUT_MS
32003
+ );
32004
+ timeout.unref();
32005
+ })
32006
+ ]);
32007
+ this.#releasing.delete(id2);
32008
+ } catch (error51) {
32009
+ throw new ExternalSessionReleaseError(error51);
32010
+ } finally {
32011
+ if (timeout) clearTimeout(timeout);
32012
+ }
32013
+ }
32014
+ async drain() {
32015
+ while (this.#pending.size) await Promise.all(this.#pending.values());
32016
+ }
32017
+ };
32018
+
31896
32019
  // packages/host-runtime/src/usage-metering.ts
31897
32020
  var TOKENS_PER_PRICE_UNIT = 1e6;
31898
32021
  var UsageMeter = class {
@@ -32175,6 +32298,11 @@ var ExternalThreadOpenError = class extends Error {
32175
32298
  function errorMessage3(error51) {
32176
32299
  return error51 instanceof Error ? error51.message : String(error51);
32177
32300
  }
32301
+ function recoveryError(error51) {
32302
+ if (error51 instanceof ExternalThreadOpenError) return error51.rpcError;
32303
+ if (error51 instanceof ExternalSessionReleaseError) return { code: -32075, message: error51.message };
32304
+ return { code: -32076, message: "External Thread recovery failed; retry the request" };
32305
+ }
32178
32306
  var ExternalThreadRuntime = class {
32179
32307
  idleRelease;
32180
32308
  #adapters;
@@ -32182,7 +32310,8 @@ var ExternalThreadRuntime = class {
32182
32310
  #diagnose;
32183
32311
  #environment;
32184
32312
  #repository;
32185
- #restores = /* @__PURE__ */ new Map();
32313
+ #lifecycle = new ExternalThreadLifecycle();
32314
+ #hasRunningDescendants;
32186
32315
  #subagentRunning;
32187
32316
  #threads = /* @__PURE__ */ new Map();
32188
32317
  constructor(input) {
@@ -32192,12 +32321,13 @@ var ExternalThreadRuntime = class {
32192
32321
  this.#consumeOutputs = input.consumeOutputs;
32193
32322
  this.#diagnose = input.diagnose;
32194
32323
  this.#subagentRunning = input.subagentRunning ?? (() => false);
32324
+ this.#hasRunningDescendants = input.hasRunningDescendants ?? (() => false);
32195
32325
  this.idleRelease = new ExternalThreadIdleRelease({
32196
32326
  threads: () => this.values(),
32197
32327
  get: (id2) => this.get(id2),
32198
32328
  remove: (id2) => this.remove(id2),
32199
32329
  queue: input.idleRelease?.queue ?? new DesktopRequestQueue(),
32200
- canRelease: input.idleRelease?.canRelease ?? (() => false),
32330
+ canRelease: (thread) => !thread.sessionUnavailable && !this.#lifecycle.busy(thread.id) && (input.idleRelease?.canRelease(thread) ?? false),
32201
32331
  ...input.idleRelease?.onClosed ? { onClosed: input.idleRelease.onClosed } : {},
32202
32332
  diagnose: input.diagnose
32203
32333
  });
@@ -32213,7 +32343,45 @@ var ExternalThreadRuntime = class {
32213
32343
  }
32214
32344
  clear() {
32215
32345
  this.#threads.clear();
32216
- this.#restores.clear();
32346
+ }
32347
+ drain() {
32348
+ return this.#lifecycle.drain();
32349
+ }
32350
+ #withLifecycle(threadId2, operation) {
32351
+ return this.#lifecycle.run(threadId2, async () => {
32352
+ await this.idleRelease.waitForClose(threadId2);
32353
+ return operation();
32354
+ });
32355
+ }
32356
+ markSessionUnavailable(thread) {
32357
+ thread.sessionUnavailable = true;
32358
+ }
32359
+ #idleForRestore(thread) {
32360
+ if (thread.running || thread.activeTurnId !== null) return false;
32361
+ if (thread.session.hasBackgroundWork?.()) return false;
32362
+ if (this.#hasRunningDescendants(thread.id)) return false;
32363
+ return true;
32364
+ }
32365
+ async #dropIdleSession(thread) {
32366
+ if (this.#threads.get(thread.id) !== thread) return;
32367
+ thread.sessionUnavailable = true;
32368
+ await this.#lifecycle.release(thread.id, thread.session, thread.outputTask);
32369
+ if (this.#threads.get(thread.id) !== thread) return;
32370
+ await this.#retireSubagents(thread);
32371
+ if (this.#threads.get(thread.id) === thread) this.#threads.delete(thread.id);
32372
+ }
32373
+ /** Called without awaiting from the output consumer: release itself drains that consumer. */
32374
+ async discardUnavailableIdle(thread) {
32375
+ const session = thread.session;
32376
+ try {
32377
+ await this.#withLifecycle(thread.id, async () => {
32378
+ if (this.#threads.get(thread.id) !== thread || thread.session !== session || !thread.sessionUnavailable || !this.#idleForRestore(thread))
32379
+ return;
32380
+ await this.#dropIdleSession(thread);
32381
+ });
32382
+ } catch (error51) {
32383
+ this.#diagnose(error51);
32384
+ }
32217
32385
  }
32218
32386
  register(input) {
32219
32387
  const harnessId = input.record.harnessId;
@@ -32258,7 +32426,8 @@ var ExternalThreadRuntime = class {
32258
32426
  ephemeralTurnIds: /* @__PURE__ */ new Set(),
32259
32427
  persistenceError: null,
32260
32428
  projectedTerminalTurnId: null,
32261
- ignoredInteractionIds: /* @__PURE__ */ new Set()
32429
+ ignoredInteractionIds: /* @__PURE__ */ new Set(),
32430
+ sessionUnavailable: false
32262
32431
  };
32263
32432
  this.idleRelease.touch(externalThread);
32264
32433
  externalThread.outputTask = this.#consumeOutputs(externalThread);
@@ -32285,11 +32454,12 @@ var ExternalThreadRuntime = class {
32285
32454
  } catch (error51) {
32286
32455
  this.#diagnose(error51);
32287
32456
  }
32288
- await this.#retireSubagents(current.id);
32457
+ await this.#retireSubagents(current, true);
32289
32458
  this.#threads.delete(current.id);
32290
32459
  return this.register(input);
32291
32460
  }
32292
- async #retireSubagents(parentId) {
32461
+ async #retireSubagents(parent, bestEffort = false) {
32462
+ const parentId = parent.id;
32293
32463
  const ancestors = /* @__PURE__ */ new Map();
32294
32464
  const descendant = async (record4) => {
32295
32465
  const visited = /* @__PURE__ */ new Set();
@@ -32306,24 +32476,35 @@ var ExternalThreadRuntime = class {
32306
32476
  }
32307
32477
  return false;
32308
32478
  };
32309
- for (const [id2, restoring] of [...this.#restores]) {
32310
- if (await descendant(await this.#repository.find(id2))) {
32311
- await restoring.catch(this.#diagnose);
32312
- }
32479
+ for (const [id2, pending] of this.#lifecycle.pending()) {
32480
+ if (id2 !== parentId && await descendant(await this.#repository.find(id2))) await pending;
32313
32481
  }
32314
- for (const child of this.#threads.values()) {
32482
+ for (const child of [...this.#threads.values()]) {
32315
32483
  if (!await descendant(child.record)) continue;
32316
- this.#threads.delete(child.id);
32317
- try {
32318
- await child.session.close();
32319
- await child.outputTask;
32320
- } catch (error51) {
32321
- this.#diagnose(error51);
32322
- }
32484
+ const retire = async () => {
32485
+ if (this.#threads.get(parentId) !== parent || this.#threads.get(child.id) !== child) return;
32486
+ if (bestEffort) {
32487
+ this.#threads.delete(child.id);
32488
+ await child.session.close();
32489
+ await child.outputTask;
32490
+ } else {
32491
+ child.sessionUnavailable = true;
32492
+ await this.#lifecycle.release(child.id, child.session, child.outputTask);
32493
+ if (this.#threads.get(child.id) === child) this.#threads.delete(child.id);
32494
+ }
32495
+ };
32496
+ if (bestEffort) await retire().catch(this.#diagnose);
32497
+ else await this.#withLifecycle(child.id, retire);
32323
32498
  }
32324
32499
  }
32325
32500
  async locate(threadId2) {
32326
- await this.idleRelease.waitForClose(threadId2);
32501
+ try {
32502
+ return await this.#withLifecycle(threadId2, () => this.#locate(threadId2));
32503
+ } catch (error51) {
32504
+ return { kind: "error", error: recoveryError(error51) };
32505
+ }
32506
+ }
32507
+ async #locate(threadId2) {
32327
32508
  const loaded = this.#threads.get(threadId2);
32328
32509
  if (loaded) {
32329
32510
  const failure = this.idleRelease.failure(loaded);
@@ -32349,37 +32530,61 @@ var ExternalThreadRuntime = class {
32349
32530
  return { kind: "external", record: record4, thread: null };
32350
32531
  }
32351
32532
  async resolve(threadId2) {
32352
- const location = await this.locate(threadId2);
32353
- if (location.kind !== "external") return location;
32354
- if (location.thread) {
32355
- this.idleRelease.touch(location.thread);
32356
- return { kind: "external", thread: location.thread, historyFresh: false };
32357
- }
32358
- const { record: record4 } = location;
32359
- let restoring = this.#restores.get(threadId2);
32360
- if (!restoring) {
32361
- const restored = this.#threads.get(threadId2);
32362
- if (restored) {
32363
- this.idleRelease.touch(restored);
32364
- return { kind: "external", thread: restored, historyFresh: false };
32365
- }
32366
- restoring = this.#restore(record4).finally(() => {
32367
- this.#restores.delete(threadId2);
32533
+ try {
32534
+ return await this.#withLifecycle(threadId2, async () => {
32535
+ const location = await this.#locate(threadId2);
32536
+ if (location.kind !== "external") return location;
32537
+ if (location.thread) {
32538
+ if (location.thread.sessionUnavailable && this.#idleForRestore(location.thread)) {
32539
+ await this.#dropIdleSession(location.thread);
32540
+ const current = this.#threads.get(threadId2);
32541
+ if (current) return { kind: "external", thread: current, historyFresh: false };
32542
+ } else {
32543
+ this.idleRelease.touch(location.thread);
32544
+ return { kind: "external", thread: location.thread, historyFresh: false };
32545
+ }
32546
+ }
32547
+ return {
32548
+ kind: "external",
32549
+ thread: await this.#restore(location.record),
32550
+ historyFresh: true
32551
+ };
32368
32552
  });
32369
- this.#restores.set(threadId2, restoring);
32553
+ } catch (error51) {
32554
+ return { kind: "error", error: recoveryError(error51) };
32370
32555
  }
32556
+ }
32557
+ async refresh(thread) {
32371
32558
  try {
32372
- return { kind: "external", thread: await restoring, historyFresh: true };
32559
+ return await this.#withLifecycle(thread.id, () => this.#refresh(thread));
32373
32560
  } catch (error51) {
32561
+ return recoveryError(error51);
32562
+ }
32563
+ }
32564
+ async #refresh(thread) {
32565
+ if (this.#threads.get(thread.id) !== thread) {
32374
32566
  return {
32375
- kind: "error",
32376
- error: error51 instanceof ExternalThreadOpenError ? error51.rpcError : { code: -32076, message: "External Thread recovery failed" }
32567
+ code: -32076,
32568
+ message: "External Session changed during recovery; retry the request"
32377
32569
  };
32378
32570
  }
32379
- }
32380
- async refresh(thread) {
32381
- const snapshot2 = await thread.session.readSnapshot();
32382
- if (!snapshot2.ok) return mapExternalThreadHarnessError(snapshot2.error, "read");
32571
+ const failure = this.idleRelease.failure(thread);
32572
+ if (failure) return { code: -32075, message: failure };
32573
+ if (thread.sessionUnavailable && this.#idleForRestore(thread)) {
32574
+ const revived = await this.#reviveIdleSession(thread);
32575
+ if (revived) return revived;
32576
+ }
32577
+ let snapshot2 = await thread.session.readSnapshot();
32578
+ if (!snapshot2.ok) {
32579
+ if (snapshot2.error.code === "invalidState" && this.#idleForRestore(thread) && this.#threads.get(thread.id) === thread) {
32580
+ const revived = await this.#reviveIdleSession(thread);
32581
+ if (revived) return revived;
32582
+ snapshot2 = await thread.session.readSnapshot();
32583
+ if (!snapshot2.ok) return mapExternalThreadHarnessError(snapshot2.error, "read");
32584
+ } else {
32585
+ return mapExternalThreadHarnessError(snapshot2.error, "read");
32586
+ }
32587
+ }
32383
32588
  try {
32384
32589
  const aligned = await this.#repository.alignSnapshot(thread.record, snapshot2.value);
32385
32590
  thread.record = aligned.record;
@@ -32396,6 +32601,122 @@ var ExternalThreadRuntime = class {
32396
32601
  return { code: -32081, message: "External Thread history could not be persisted" };
32397
32602
  }
32398
32603
  }
32604
+ async #reviveIdleSession(thread) {
32605
+ thread.sessionUnavailable = true;
32606
+ const harnessId = thread.record.harnessId;
32607
+ const adapter = this.#adapters.get(harnessId);
32608
+ if (!adapter || !thread.record.nativeSessionRef) {
32609
+ await this.#dropIdleSession(thread);
32610
+ return { code: -32077, message: "External Harness is unavailable" };
32611
+ }
32612
+ await this.#lifecycle.release(thread.id, thread.session, thread.outputTask);
32613
+ await this.#retireSubagents(thread);
32614
+ if (this.#threads.get(thread.id) !== thread) {
32615
+ return { code: -32076, message: "External Thread recovery failed" };
32616
+ }
32617
+ const restoredSelection = decodeExternalTransportSelection(
32618
+ harnessId,
32619
+ thread.record.transportModelId
32620
+ );
32621
+ const opened = await adapter.open({
32622
+ kind: "resume",
32623
+ cwd: thread.record.cwd,
32624
+ environment: { ...this.#environment, [DELEGATION_THREAD_ID_ENV]: thread.record.hostThreadId },
32625
+ nativeRef: thread.record.nativeSessionRef,
32626
+ knownTurnRefs: thread.record.turnMappings.map(({ nativeTurnRef }) => nativeTurnRef),
32627
+ ...restoredSelection?.model ? { model: restoredSelection.model } : {},
32628
+ ...restoredSelection?.thinkingOptionId ? { thinkingOptionId: restoredSelection.thinkingOptionId } : {},
32629
+ ...restoredSelection?.permissionModeId ? { permissionModeId: restoredSelection.permissionModeId } : {}
32630
+ });
32631
+ if (!opened.ok) {
32632
+ await this.#dropIdleSession(thread);
32633
+ return mapExternalThreadHarnessError(opened.error, "resume");
32634
+ }
32635
+ const session = opened.value;
32636
+ if (this.#threads.get(thread.id) !== thread) {
32637
+ await this.#lifecycle.release(thread.id, session, Promise.resolve());
32638
+ return { code: -32076, message: "External Thread recovery failed" };
32639
+ }
32640
+ try {
32641
+ const snapshot2 = await session.readSnapshot();
32642
+ if (!snapshot2.ok) {
32643
+ await this.#lifecycle.release(thread.id, session, Promise.resolve());
32644
+ await this.#dropIdleSession(thread);
32645
+ return mapExternalThreadHarnessError(snapshot2.error, "read");
32646
+ }
32647
+ let aligned = await this.#repository.alignSnapshot(thread.record, snapshot2.value);
32648
+ if (this.#threads.get(thread.id) !== thread) {
32649
+ await this.#lifecycle.release(thread.id, session, Promise.resolve());
32650
+ return { code: -32076, message: "External Thread recovery failed" };
32651
+ }
32652
+ const restoredState = snapshot2.value.state;
32653
+ const effectiveModel = restoredState ? restoredState.effectiveModel : restoredSelection?.model;
32654
+ const effectiveThinkingOptionId = restoredState ? restoredState.effectiveThinkingOptionId : restoredSelection?.thinkingOptionId;
32655
+ const effectivePermissionModeId = restoredState ? restoredState.effectivePermissionModeId : restoredSelection?.permissionModeId;
32656
+ let transportModelId = aligned.record.transportModelId;
32657
+ if (restoredState && effectiveModel) {
32658
+ const liveSelection = {
32659
+ model: effectiveModel,
32660
+ ...effectiveThinkingOptionId ? { thinkingOptionId: effectiveThinkingOptionId } : {},
32661
+ ...effectivePermissionModeId ? { permissionModeId: effectivePermissionModeId } : {}
32662
+ };
32663
+ transportModelId = encodeExternalTransportSelection(harnessId, liveSelection);
32664
+ if (transportModelId !== aligned.record.transportModelId) {
32665
+ try {
32666
+ aligned = {
32667
+ ...aligned,
32668
+ record: await this.#repository.setTransportModelId(
32669
+ aligned.record.hostThreadId,
32670
+ transportModelId
32671
+ )
32672
+ };
32673
+ } catch (error51) {
32674
+ this.#diagnose(error51);
32675
+ }
32676
+ }
32677
+ }
32678
+ if (this.#threads.get(thread.id) !== thread) {
32679
+ await this.#lifecycle.release(thread.id, session, Promise.resolve());
32680
+ return { code: -32076, message: "External Thread recovery failed" };
32681
+ }
32682
+ const initialState = restoredState ?? session.initialState;
32683
+ const observerState = {
32684
+ ...initialState,
32685
+ ...effectiveModel ? { effectiveModel } : {},
32686
+ ...effectiveThinkingOptionId ? { effectiveThinkingOptionId } : {},
32687
+ ...effectivePermissionModeId ? { effectivePermissionModeId } : {}
32688
+ };
32689
+ thread.session = session;
32690
+ thread.record = aligned.record;
32691
+ thread.turns = aligned.turns;
32692
+ thread.historyHydrated = true;
32693
+ thread.sessionUnavailable = false;
32694
+ thread.persistenceError = null;
32695
+ thread.transportModelId = transportModelId;
32696
+ if (effectiveModel) thread.requestedModel = effectiveModel;
32697
+ if (effectiveThinkingOptionId) thread.requestedThinkingOptionId = effectiveThinkingOptionId;
32698
+ if (effectivePermissionModeId) thread.requestedPermissionModeId = effectivePermissionModeId;
32699
+ thread.stateObserver = new SessionStateObserver(observerState);
32700
+ thread.latestUsage = session.initialUsage;
32701
+ thread.usageMeter = new UsageMeter();
32702
+ thread.usageTurnId = null;
32703
+ thread.thread = externalThreadValue({
32704
+ record: aligned.record,
32705
+ turns: aligned.turns,
32706
+ sessionId: thread.sessionId,
32707
+ running: thread.running
32708
+ });
32709
+ thread.outputTask = this.#consumeOutputs(thread);
32710
+ this.idleRelease.clearOutputFailed(thread);
32711
+ return null;
32712
+ } catch (error51) {
32713
+ if (error51 instanceof ExternalSessionReleaseError) throw error51;
32714
+ await this.#lifecycle.release(thread.id, session, Promise.resolve());
32715
+ this.#diagnose(error51);
32716
+ await this.#dropIdleSession(thread);
32717
+ return { code: -32081, message: "External Thread history could not be persisted" };
32718
+ }
32719
+ }
32399
32720
  async persistTerminalIdentity(thread, event) {
32400
32721
  if (thread.persistenceError) return thread.persistenceError;
32401
32722
  if (!event.nativeTurnRef) {
@@ -32534,7 +32855,8 @@ var ExternalThreadRuntime = class {
32534
32855
  transportModelId
32535
32856
  });
32536
32857
  } catch (error51) {
32537
- await session.close().catch(() => void 0);
32858
+ if (error51 instanceof ExternalSessionReleaseError) throw error51;
32859
+ await this.#lifecycle.release(record4.hostThreadId, session, Promise.resolve());
32538
32860
  throw error51;
32539
32861
  }
32540
32862
  }
@@ -37684,6 +38006,7 @@ var AppServerHost = class {
37684
38006
  #launchSettings;
37685
38007
  #accountInspections = new HarnessAccountInspectionCache();
37686
38008
  #externalRuntime;
38009
+ #executionHosts = /* @__PURE__ */ new WeakMap();
37687
38010
  #externalPrewarms = new ExternalThreadPrewarms();
37688
38011
  #externalSteering = new ExternalTurnSteering();
37689
38012
  #liveCommandCache = new LiveCommandCatalogCache();
@@ -37818,6 +38141,7 @@ var AppServerHost = class {
37818
38141
  consumeOutputs: (thread) => this.#consumeHarnessOutputs(thread),
37819
38142
  diagnose: (error51) => this.#diagnose(error51),
37820
38143
  subagentRunning: (threadId2) => this.#subagentThreadStatuses.get(threadId2) === "active",
38144
+ hasRunningDescendants: (threadId2) => this.#hasRunningSubagents(threadId2),
37821
38145
  idleRelease: {
37822
38146
  queue: this.#desktopRequests,
37823
38147
  onClosed: async (thread) => {
@@ -37876,7 +38200,11 @@ var AppServerHost = class {
37876
38200
  wait: (input) => this.#waitForPlugins().then(() => this.#delegationCoordinator.wait(input)),
37877
38201
  list: (input) => this.#waitForPlugins().then(() => this.#delegationCoordinator.list(input)),
37878
38202
  canHandleStart: (input) => this.#canHandleDelegationStart(input),
37879
- ownsThread: (threadId2) => this.#ownsDelegationThread(threadId2)
38203
+ ownsThread: (threadId2) => this.#ownsDelegationThread(threadId2),
38204
+ executionHostId: (threadId2) => {
38205
+ const thread = this.#externalRuntime.get(threadId2);
38206
+ return thread ? this.#executionHosts.get(thread) : void 0;
38207
+ }
37880
38208
  });
37881
38209
  this.#unregisterDelegationApi = typeof unregisterDelegationApi === "function" ? unregisterDelegationApi : void 0;
37882
38210
  }
@@ -38013,6 +38341,7 @@ var AppServerHost = class {
38013
38341
  await Promise.allSettled(threads.map(({ session }) => session.close()));
38014
38342
  await Promise.allSettled(threads.map(({ outputTask }) => outputTask));
38015
38343
  await this.#closeAdapters();
38344
+ await this.#externalRuntime.drain();
38016
38345
  for (const pending of [...this.#pendingDesktopApprovals.values()]) {
38017
38346
  await this.#resolveDesktopApproval(pending.interaction.interactionId).catch(
38018
38347
  () => void 0
@@ -40206,7 +40535,7 @@ var AppServerHost = class {
40206
40535
  this.#pendingExternalCommandRequests.delete(thread.id);
40207
40536
  }
40208
40537
  }
40209
- async #beginExternalCommand(thread, descriptor, arguments_2, requestedTurnId) {
40538
+ async #beginExternalCommand(thread, descriptor, arguments_2, requestedTurnId, sourceHostId) {
40210
40539
  const commands = thread.session.commands;
40211
40540
  if (!commands) {
40212
40541
  throw new ExternalCommandError(-32078, "External Harness does not expose commands");
@@ -40235,6 +40564,7 @@ var AppServerHost = class {
40235
40564
  thread.ephemeralTurnIds.add(turnId);
40236
40565
  if (descriptor.invocation === "/compact") this.#manualCompactionTurns.set(thread, turnId);
40237
40566
  else this.#manualCompactionTurns.delete(thread);
40567
+ const restoreExecutionHost = this.#setExecutionHost(thread, sourceHostId);
40238
40568
  try {
40239
40569
  if (thread.unsubmittedPrewarm && await this.#externalRuntime.submitPrewarm(thread)) {
40240
40570
  await this.#notifyExternalThreadStarted(thread.thread);
@@ -40247,6 +40577,7 @@ var AppServerHost = class {
40247
40577
  if (!result.ok) throw new ExternalCommandError(-32073, result.error.message);
40248
40578
  return { turnId: result.value.turnId, turn: projection.projector.pendingTurn(), gate };
40249
40579
  } catch (error51) {
40580
+ restoreExecutionHost();
40250
40581
  thread.running = false;
40251
40582
  thread.activeTurnId = null;
40252
40583
  thread.projectedTurns.delete(turnId);
@@ -41026,7 +41357,8 @@ var AppServerHost = class {
41026
41357
  thread,
41027
41358
  text,
41028
41359
  void 0,
41029
- typeof params.clientUserMessageId === "string" ? params.clientUserMessageId : void 0
41360
+ typeof params.clientUserMessageId === "string" ? params.clientUserMessageId : void 0,
41361
+ params[THREAD_REFERENCE_SOURCE_HOST_PARAM]
41030
41362
  );
41031
41363
  try {
41032
41364
  await this.#writer.json(rpcEnvelope(request, { result: { turn: started.turn } }));
@@ -41052,7 +41384,8 @@ var AppServerHost = class {
41052
41384
  thread,
41053
41385
  text,
41054
41386
  assertActive,
41055
- typeof requestObject(request).clientUserMessageId === "string" ? requestObject(request).clientUserMessageId : void 0
41387
+ typeof requestObject(request).clientUserMessageId === "string" ? requestObject(request).clientUserMessageId : void 0,
41388
+ requestObject(request)[THREAD_REFERENCE_SOURCE_HOST_PARAM]
41056
41389
  )
41057
41390
  );
41058
41391
  try {
@@ -41073,12 +41406,12 @@ var AppServerHost = class {
41073
41406
  }
41074
41407
  }
41075
41408
  /** Keep command restoration and dispatch identical for start and steer submissions. */
41076
- async #beginExternalInputTurn(thread, inputText, assertActive, clientUserMessageId) {
41409
+ async #beginExternalInputTurn(thread, inputText, assertActive, clientUserMessageId, sourceHostId) {
41077
41410
  const text = restoreHarnessCommandMentions(inputText);
41078
41411
  const commands = thread.session.commands;
41079
41412
  if (!commands || !isExternalCommandCandidate(text)) {
41080
41413
  assertActive?.();
41081
- return this.#beginExternalTurn(thread, text, clientUserMessageId);
41414
+ return this.#beginExternalTurn(thread, text, clientUserMessageId, sourceHostId);
41082
41415
  }
41083
41416
  this.#pendingExternalCommandRequests.add(thread.id);
41084
41417
  try {
@@ -41089,9 +41422,15 @@ var AppServerHost = class {
41089
41422
  assertActive?.();
41090
41423
  if (!command) {
41091
41424
  this.#pendingExternalCommandRequests.delete(thread.id);
41092
- return await this.#beginExternalTurn(thread, text, clientUserMessageId);
41425
+ return await this.#beginExternalTurn(thread, text, clientUserMessageId, sourceHostId);
41093
41426
  }
41094
- return await this.#beginExternalCommand(thread, command.descriptor, command.arguments);
41427
+ return await this.#beginExternalCommand(
41428
+ thread,
41429
+ command.descriptor,
41430
+ command.arguments,
41431
+ void 0,
41432
+ sourceHostId
41433
+ );
41095
41434
  } catch (error51) {
41096
41435
  if (error51 instanceof ExternalCommandError || error51 instanceof ExternalSteerError) throw error51;
41097
41436
  this.#diagnose(error51);
@@ -41103,7 +41442,20 @@ var AppServerHost = class {
41103
41442
  this.#pendingExternalCommandRequests.delete(thread.id);
41104
41443
  }
41105
41444
  }
41106
- async #beginExternalTurn(thread, text, clientUserMessageId) {
41445
+ /** Apply context before native execution; a rejected start restores the prior identity. */
41446
+ #setExecutionHost(thread, sourceHostId) {
41447
+ const previousHostId = this.#executionHosts.get(thread);
41448
+ if (typeof sourceHostId === "string" && sourceHostId.length > 0 && sourceHostId.length <= 1024 && !/[\u0000-\u0020\u007f]/u.test(sourceHostId)) {
41449
+ this.#executionHosts.set(thread, sourceHostId);
41450
+ } else {
41451
+ this.#executionHosts.delete(thread);
41452
+ }
41453
+ return () => {
41454
+ if (previousHostId === void 0) this.#executionHosts.delete(thread);
41455
+ else this.#executionHosts.set(thread, previousHostId);
41456
+ };
41457
+ }
41458
+ async #beginExternalTurn(thread, text, clientUserMessageId, sourceHostId) {
41107
41459
  if (this.#closeRequested || this.#externalRuntime.get(thread.id) !== thread) {
41108
41460
  throw new ExternalSteerError(-32073, "External Thread is no longer available");
41109
41461
  }
@@ -41130,6 +41482,7 @@ var AppServerHost = class {
41130
41482
  thread.projectedTerminalTurnId = null;
41131
41483
  thread.projectedTurns.set(turnId, projection);
41132
41484
  thread.responseGates.set(turnId, gate);
41485
+ const restoreExecutionHost = this.#setExecutionHost(thread, sourceHostId);
41133
41486
  try {
41134
41487
  if (thread.unsubmittedPrewarm && await this.#externalRuntime.submitPrewarm(thread)) {
41135
41488
  await this.#notifyExternalThreadStarted(thread.thread);
@@ -41142,6 +41495,7 @@ var AppServerHost = class {
41142
41495
  if (!result.ok) throw new ExternalSteerError(-32073, result.error.message);
41143
41496
  return { turnId, turn: projection.projector.pendingTurn(), gate };
41144
41497
  } catch (error51) {
41498
+ restoreExecutionHost();
41145
41499
  thread.running = false;
41146
41500
  thread.activeTurnId = null;
41147
41501
  thread.projectedTurns.delete(turnId);
@@ -41370,7 +41724,11 @@ var AppServerHost = class {
41370
41724
  if (event.type === "session.faulted") {
41371
41725
  this.#externalSteering.fault(thread.id, new Error(event.error.message));
41372
41726
  thread.stateObserver.fault(new Error(event.error.message));
41727
+ this.#externalRuntime.markSessionUnavailable(thread);
41373
41728
  this.#diagnose(`${thread.harnessId} Harness Session faulted: ${event.error.message}`);
41729
+ if (!thread.running && !thread.activeTurnId && !this.#hasRunningSubagents(thread.id) && !thread.session.hasBackgroundWork?.()) {
41730
+ void this.#externalRuntime.discardUnavailableIdle(thread);
41731
+ }
41374
41732
  return;
41375
41733
  }
41376
41734
  if (event.type === "turn.autonomous.started") {
@@ -41499,6 +41857,11 @@ var AppServerHost = class {
41499
41857
  return { ...subagent, subagentId: record4.hostThreadId };
41500
41858
  }
41501
41859
  async #refreshOpenSubagentThread(threadId2, terminal3 = true) {
41860
+ await this.#desktopRequests.run(threadId2, async () => {
41861
+ await this.#refreshOpenSubagentThreadBody(threadId2, terminal3);
41862
+ });
41863
+ }
41864
+ async #refreshOpenSubagentThreadBody(threadId2, terminal3) {
41502
41865
  const child = this.#externalRuntime.get(threadId2);
41503
41866
  if (!child) return;
41504
41867
  const previousItems = new Map(
@@ -43523,15 +43886,28 @@ var DelegationControlRegistry = class {
43523
43886
  return (await this.#registrationForThread(input.threadId)).cancel(input);
43524
43887
  }
43525
43888
  async read(input) {
43526
- if (input.hostId !== void 0 && input.hostId !== "local") {
43527
- if (!input.hostId || !this.#remoteRead)
43889
+ const { callerThreadId: callerThreadId2, ...read } = input;
43890
+ if (read.hostId !== void 0 && read.hostId !== "local") {
43891
+ if (typeof callerThreadId2 === "string" && callerThreadId2) {
43892
+ const callers = await this.#matching(
43893
+ (registration) => registration.executionHostId?.(callerThreadId2) === read.hostId && registration.ownsThread(callerThreadId2)
43894
+ );
43895
+ if (callers.length > 1)
43896
+ only(callers, "Calling Thread has ambiguous execution Host context");
43897
+ if (callers.length === 1) {
43898
+ const { hostId: hostId2, ...local } = read;
43899
+ const snapshot2 = await (await this.#registrationForThread(local.threadId)).read(local);
43900
+ return { ...snapshot2, hostId: hostId2 };
43901
+ }
43902
+ }
43903
+ if (!read.hostId || !this.#remoteRead)
43528
43904
  throw new DelegationControlError(
43529
43905
  "RUNTIME_UNREACHABLE",
43530
43906
  "Remote Thread reading is unavailable"
43531
43907
  );
43532
- return this.#remoteRead(input);
43908
+ return this.#remoteRead(read);
43533
43909
  }
43534
- return (await this.#registrationForThread(input.threadId)).read(input);
43910
+ return (await this.#registrationForThread(read.threadId)).read(read);
43535
43911
  }
43536
43912
  async wait(input) {
43537
43913
  if (input.hostId !== void 0 && input.hostId !== "local")
@@ -45555,9 +45931,21 @@ function requiredRuntimeConfiguration(environment) {
45555
45931
  if (!stockCodexPath) throw new Error(`${STOCK_CODEX_PATH_ENV} is required`);
45556
45932
  return { stockCodexPath };
45557
45933
  }
45558
- function delegationCliEnvironment(environment) {
45559
- const cliPath = environment[DELEGATION_CLI_PATH_ENV] ?? environment[UPDATE_RUNTIME_ENV.launcherExecutable];
45560
- const nodePath2 = environment[UPDATE_RUNTIME_ENV.npmNodePath];
45934
+ function delegationCliEnvironment(environment, hostRuntimePath) {
45935
+ let packagedCli;
45936
+ if (hostRuntimePath && path37.isAbsolute(hostRuntimePath)) {
45937
+ const candidate = path37.resolve(
45938
+ path37.dirname(hostRuntimePath),
45939
+ "../bin",
45940
+ process.platform === "win32" ? "codexhost.exe" : "codexhost"
45941
+ );
45942
+ try {
45943
+ if (statSync(candidate).isFile()) packagedCli = candidate;
45944
+ } catch {
45945
+ }
45946
+ }
45947
+ const cliPath = environment[DELEGATION_CLI_PATH_ENV] ?? environment[UPDATE_RUNTIME_ENV.launcherExecutable] ?? packagedCli;
45948
+ const nodePath2 = environment[DELEGATION_CLI_NODE_PATH_ENV] ?? environment[UPDATE_RUNTIME_ENV.npmNodePath] ?? (packagedCli && cliPath === packagedCli ? process.execPath : void 0);
45561
45949
  return {
45562
45950
  ...cliPath ? { [DELEGATION_CLI_PATH_ENV]: cliPath } : {},
45563
45951
  ...nodePath2 && path37.isAbsolute(nodePath2) ? { [DELEGATION_CLI_NODE_PATH_ENV]: nodePath2 } : {}
@@ -45575,7 +45963,7 @@ async function prepareDelegationRuntime(input) {
45575
45963
  const server = await startDelegationControlServer({ token, api: registry2, watchApi: registry2 });
45576
45964
  const environment = {
45577
45965
  ...input.environment,
45578
- ...delegationCliEnvironment(input.environment),
45966
+ ...delegationCliEnvironment(input.environment, input.hostRuntimePath),
45579
45967
  [DELEGATION_RUNTIME_ENDPOINT_ENV]: server.endpoint,
45580
45968
  [DELEGATION_RUNTIME_TOKEN_ENV]: token
45581
45969
  };
@@ -45650,6 +46038,7 @@ async function runHostRuntime(input) {
45650
46038
  const environment = remoteControlPlan?.environment ?? input.environment;
45651
46039
  return prepareDelegationRuntime({
45652
46040
  environment,
46041
+ ...hostRuntimePath ? { hostRuntimePath } : {},
45653
46042
  createHost: async (delegationEnvironment, onDelegationApi, registry2) => {
45654
46043
  const official = await prepareLocalCodex({
45655
46044
  stockCodexPath,
@@ -45758,6 +46147,7 @@ async function runHostRuntime(input) {
45758
46147
  if (!listenUrl) throw new Error("Remote app-server listener URL is unavailable");
45759
46148
  return prepareDelegationRuntime({
45760
46149
  environment: input.environment,
46150
+ ...hostRuntimePath ? { hostRuntimePath } : {},
45761
46151
  createHost: async (delegationEnvironment, _onDelegationApi, registry2) => {
45762
46152
  const socketPath = remoteAppServerSocketPath(delegationEnvironment, listenUrl);
45763
46153
  const officialPlan = createRemoteOfficialAppServerPlan(input.arguments, socketPath);
@@ -47991,7 +48381,7 @@ import {
47991
48381
  readdirSync,
47992
48382
  renameSync,
47993
48383
  rmSync,
47994
- statSync,
48384
+ statSync as statSync2,
47995
48385
  writeFileSync
47996
48386
  } from "node:fs";
47997
48387
  import os12 from "node:os";
@@ -48021,7 +48411,7 @@ function pruneRuntimeLogs(input) {
48021
48411
  if (!Number.isSafeInteger(pid)) continue;
48022
48412
  try {
48023
48413
  const filePath = path42.join(input.directory, entry.name);
48024
- const stats = statSync(filePath);
48414
+ const stats = statSync2(filePath);
48025
48415
  files.push({
48026
48416
  filePath,
48027
48417
  pid,