clawdex-mobile 5.1.3-internal.7 → 5.1.3-internal.9

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.
@@ -84,10 +84,11 @@ gh codespace ports visibility 8787:public 8788:public
84
84
 
85
85
  ### Codespaces Bootstrap
86
86
 
87
- The repo devcontainer now includes:
87
+ The source repo devcontainer now includes:
88
88
 
89
- - `postCreateCommand`: `npm install --include=dev && npm run codespaces:bootstrap -- --prepare-only`
90
- - `postStartCommand`: `npm run codespaces:bootstrap`
89
+ - `updateContentCommand`: `npm install --include=dev --prefer-offline --no-audit --fund=false && npm run codespaces:bootstrap -- --prepare-only`
90
+ - `postStartCommand`: fire-and-forget `npm run codespaces:bootstrap`, writing setup output to `.bridge-bootstrap.log`
91
+ - `waitFor`: `updateContentCommand`
91
92
 
92
93
  `npm run codespaces:bootstrap` does the following:
93
94
 
@@ -99,9 +100,9 @@ The repo devcontainer now includes:
99
100
 
100
101
  Clawdex-created Codespaces request a 45-minute idle timeout. The bridge emits a lightweight active-turn keepalive while a Codex, OpenCode, or Cursor turn is running, so active work has activity even if a long step is otherwise quiet. When no turn is running, the keepalive stops and GitHub can pause the Codespace normally to save cost.
101
102
 
102
- That means the first Codespace create now front-loads the expensive bridge compile during `postCreateCommand`, so the later `postStartCommand` can usually start the bridge much faster.
103
+ That means prebuild-enabled Codespaces can snapshot the expensive dependency install and bridge compile during `updateContentCommand`. The later `postStartCommand` starts the runtime bridge asynchronously so Codespaces does not block editor/app access while the bridge finishes warming up.
103
104
 
104
- The same bootstrap script is included in the published `clawdex-mobile` npm package. That lets the `clawdex-codespace` template stay minimal: it can install `clawdex-mobile@latest` globally in the devcontainer and invoke the packaged bootstrap against the current workspace instead of copying `scripts/*` and `services/rust-bridge/*` into the template repo.
105
+ The same bootstrap script is included in the published `clawdex-mobile` npm package. That lets the `clawdex-codespace` template stay minimal: it installs `clawdex-mobile@internal` globally in `updateContentCommand` and invokes the packaged bootstrap against the current workspace instead of copying `scripts/*` and `services/rust-bridge/*` into the template repo. Because the published package ships Linux bridge binaries, the template does not need Rust, Cargo, or a local bridge compile.
105
106
 
106
107
  Manual examples:
107
108
 
@@ -115,7 +116,7 @@ CLAWDEX_CODESPACES_ENGINES=codex,opencode,cursor npm run codespaces:bootstrap
115
116
  Minimal template equivalent:
116
117
 
117
118
  ```bash
118
- npm install -g clawdex-mobile@latest @openai/codex
119
+ npm install -g --no-fund --no-audit clawdex-mobile@internal @openai/codex
119
120
  CLAWDEX_WORKSPACE_ROOT="$PWD" node "$(npm root -g)/clawdex-mobile/scripts/codespaces-bootstrap.js" --prepare-only
120
121
  CLAWDEX_WORKSPACE_ROOT="$PWD" node "$(npm root -g)/clawdex-mobile/scripts/codespaces-bootstrap.js"
121
122
  ```
@@ -60,7 +60,7 @@ gh codespace ports visibility 8787:public 8788:public
60
60
 
61
61
  ## GitHub Codespaces bootstrap did not start the bridge
62
62
 
63
- - Check the post-start command output in the Codespace terminal or rerun it manually:
63
+ - Check `.bridge-bootstrap.log` for the post-start bootstrap and `.bridge.log` for the bridge process, or rerun the bootstrap manually:
64
64
 
65
65
  ```bash
66
66
  npm run codespaces:bootstrap -- --prepare-only
@@ -81,8 +81,9 @@ CLAWDEX_WORKSPACE_ROOT="$PWD" node "$(npm root -g)/clawdex-mobile/scripts/codesp
81
81
  - Bridge startup logs and runtime state live in the Codespace repo root:
82
82
 
83
83
  ```bash
84
+ tail -n 200 .bridge-bootstrap.log
84
85
  tail -n 200 .bridge.log
85
- ls -la .bridge.pid .bridge.log .env.secure
86
+ ls -la .bridge.pid .bridge.log .bridge-bootstrap.log .env.secure
86
87
  ```
87
88
 
88
89
  - To only rewrite `.env.secure` without starting the bridge:
@@ -102,7 +103,7 @@ npm run codespaces:bootstrap -- --no-start
102
103
 
103
104
  - The bridge can transcribe with `OPENAI_API_KEY`, `BRIDGE_CHATGPT_ACCESS_TOKEN`, a legacy bridge token cache, or the Codex-managed ChatGPT token in `$CODEX_HOME/auth.json`.
104
105
  - In GitHub Codespaces, finish the Codex login step from the app first. Codex writes the login to `$HOME/.codex/auth.json`, and `.env.secure` sets `CODEX_HOME` to that persistent location.
105
- - If you still see the error after logging in, restart the bridge once so app-server reloads the Codex auth home:
106
+ - Current app builds automatically restart the Codespace Codex app-server after the Codex login step so it reloads the Codex auth home. On older builds, restart the bridge once after logging in:
106
107
 
107
108
  ```bash
108
109
  npm run secure:bridge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawdex-mobile",
3
- "version": "5.1.3-internal.7",
3
+ "version": "5.1.3-internal.9",
4
4
  "description": "Private-network mobile bridge and CLI for Codex and OpenCode",
5
5
  "keywords": [
6
6
  "codex",
@@ -16,6 +16,7 @@ const {
16
16
  } = require("./bridge-binary");
17
17
 
18
18
  const DEFAULT_HEALTH_TIMEOUT_MS = 15000;
19
+ const CODESPACES_HEALTH_TIMEOUT_MS = 60000;
19
20
  const DEV_HEALTH_TIMEOUT_MS = 60000;
20
21
  let qrcodeTerminal = null;
21
22
  let qrcodeTerminalLoaded = false;
@@ -346,6 +347,13 @@ function formatCodespacesVisibilityCommand(env, ports) {
346
347
  return ["gh", "codespace", "ports", "visibility", ...visibilityArgs, ...selectionArgs].join(" ");
347
348
  }
348
349
 
350
+ function sleepSync(ms) {
351
+ if (!Number.isFinite(ms) || ms <= 0) {
352
+ return;
353
+ }
354
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
355
+ }
356
+
349
357
  function updateCodespacesBrowseUrls(env, ports) {
350
358
  if (!commandExists("gh")) {
351
359
  return;
@@ -416,38 +424,57 @@ function ensureCodespacesPortsArePublic(env, ports) {
416
424
  return notes;
417
425
  }
418
426
 
419
- const result = spawnSync(
420
- "gh",
421
- [
422
- "codespace",
423
- "ports",
424
- "visibility",
425
- ...uniquePorts.map((port) => `${port}:public`),
426
- ...codespaceSelectionArgs(env),
427
- ],
428
- {
429
- encoding: "utf8",
430
- env: ghAuthEnv(env),
431
- stdio: ["ignore", "pipe", "pipe"],
427
+ const publishedPorts = [];
428
+ const failedPorts = [];
429
+ const selectionArgs = codespaceSelectionArgs(env);
430
+
431
+ for (const port of uniquePorts) {
432
+ let lastDetail = "";
433
+ for (let attempt = 1; attempt <= 8; attempt += 1) {
434
+ const result = spawnSync(
435
+ "gh",
436
+ ["codespace", "ports", "visibility", `${port}:public`, ...selectionArgs],
437
+ {
438
+ encoding: "utf8",
439
+ env: ghAuthEnv(env),
440
+ stdio: ["ignore", "pipe", "pipe"],
441
+ }
442
+ );
443
+
444
+ if ((result.status ?? 1) === 0) {
445
+ publishedPorts.push(port);
446
+ lastDetail = "";
447
+ break;
448
+ }
449
+
450
+ lastDetail = (result.stderr || result.stdout || "").trim();
451
+ if (attempt < 8) {
452
+ sleepSync(1_500);
453
+ }
432
454
  }
433
- );
434
455
 
435
- if ((result.status ?? 1) !== 0) {
436
- const detail = (result.stderr || result.stdout || "").trim();
437
- const suffix = detail ? ` (${detail.split(/\r?\n/, 1)[0]})` : "";
456
+ if (lastDetail) {
457
+ failedPorts.push({ port, detail: lastDetail });
458
+ }
459
+ }
460
+
461
+ if (publishedPorts.length > 0) {
462
+ updateCodespacesBrowseUrls(env, publishedPorts);
438
463
  notes.push(
439
- `Could not set Codespaces forwarded ports public automatically${suffix}. Run '${formatCodespacesVisibilityCommand(
464
+ `Codespaces forwarded ports are set to public for bridge access: ${publishedPorts.join(", ")}.`
465
+ );
466
+ }
467
+
468
+ for (const failure of failedPorts) {
469
+ const suffix = failure.detail ? ` (${failure.detail.split(/\r?\n/, 1)[0]})` : "";
470
+ notes.push(
471
+ `Could not set Codespaces forwarded port ${failure.port} public automatically${suffix}. Run '${formatCodespacesVisibilityCommand(
440
472
  env,
441
- uniquePorts
473
+ [failure.port]
442
474
  )}' or update the Ports panel manually.`
443
475
  );
444
- return notes;
445
476
  }
446
477
 
447
- updateCodespacesBrowseUrls(env, uniquePorts);
448
- notes.push(
449
- `Codespaces forwarded ports are set to public for bridge access: ${uniquePorts.join(", ")}.`
450
- );
451
478
  return notes;
452
479
  }
453
480
 
@@ -752,6 +779,10 @@ function buildBridgeFromSource(packageDir, env, profile) {
752
779
  }
753
780
 
754
781
  function resolveLaunch(workspaceDir, packageDir, env, { devMode, forceSourceBuild }) {
782
+ const defaultHealthTimeoutMs = isCodespacesMode(env)
783
+ ? CODESPACES_HEALTH_TIMEOUT_MS
784
+ : DEFAULT_HEALTH_TIMEOUT_MS;
785
+
755
786
  if (devMode) {
756
787
  if (!commandExists("cargo")) {
757
788
  console.error("error: missing Rust/Cargo toolchain for dev bridge mode.");
@@ -779,7 +810,7 @@ function resolveLaunch(workspaceDir, packageDir, env, { devMode, forceSourceBuil
779
810
  args: [],
780
811
  cwd: workspaceDir,
781
812
  env,
782
- healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
813
+ healthTimeoutMs: defaultHealthTimeoutMs,
783
814
  };
784
815
  }
785
816
 
@@ -792,7 +823,7 @@ function resolveLaunch(workspaceDir, packageDir, env, { devMode, forceSourceBuil
792
823
  args: [],
793
824
  cwd: workspaceDir,
794
825
  env,
795
- healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
826
+ healthTimeoutMs: defaultHealthTimeoutMs,
796
827
  };
797
828
  }
798
829
 
@@ -804,7 +835,7 @@ function resolveLaunch(workspaceDir, packageDir, env, { devMode, forceSourceBuil
804
835
  args: [],
805
836
  cwd: workspaceDir,
806
837
  env,
807
- healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
838
+ healthTimeoutMs: defaultHealthTimeoutMs,
808
839
  };
809
840
  }
810
841
 
@@ -833,7 +864,7 @@ function resolveLaunch(workspaceDir, packageDir, env, { devMode, forceSourceBuil
833
864
  args: [],
834
865
  cwd: workspaceDir,
835
866
  env,
836
- healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
867
+ healthTimeoutMs: defaultHealthTimeoutMs,
837
868
  };
838
869
  }
839
870
 
@@ -149,7 +149,7 @@ dependencies = [
149
149
 
150
150
  [[package]]
151
151
  name = "codex-rust-bridge"
152
- version = "5.1.3-internal.7"
152
+ version = "5.1.3-internal.9"
153
153
  dependencies = [
154
154
  "axum",
155
155
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "codex-rust-bridge"
3
- version = "5.1.3-internal.7"
3
+ version = "5.1.3-internal.9"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1266,7 +1266,7 @@ impl BrowserPreviewService {
1266
1266
  #[derive(Clone)]
1267
1267
  struct RuntimeBackend {
1268
1268
  preferred_engine: BridgeRuntimeEngine,
1269
- codex: Option<Arc<AppServerBridge>>,
1269
+ codex: Arc<StdRwLock<Option<Arc<AppServerBridge>>>>,
1270
1270
  opencode: Option<Arc<OpencodeBackend>>,
1271
1271
  cursor: Arc<StdRwLock<Option<Arc<AppServerBridge>>>>,
1272
1272
  }
@@ -1281,7 +1281,7 @@ impl RuntimeBackend {
1281
1281
  let cursor_enabled = config
1282
1282
  .enabled_engines
1283
1283
  .contains(&BridgeRuntimeEngine::Cursor);
1284
- let mut codex = None;
1284
+ let codex = Arc::new(StdRwLock::new(None));
1285
1285
  let mut opencode = None;
1286
1286
  let cursor = Arc::new(StdRwLock::new(None));
1287
1287
 
@@ -1291,7 +1291,7 @@ impl RuntimeBackend {
1291
1291
  let app_server =
1292
1292
  AppServerBridge::start_codex(&config.cli_bin, hub.clone()).await?;
1293
1293
  spawn_rollout_live_sync(hub.clone());
1294
- codex = Some(app_server);
1294
+ Self::store_codex_backend(&codex, app_server);
1295
1295
  }
1296
1296
 
1297
1297
  if opencode_enabled {
@@ -1327,7 +1327,7 @@ impl RuntimeBackend {
1327
1327
  {
1328
1328
  Ok(app_server) => {
1329
1329
  spawn_rollout_live_sync(hub.clone());
1330
- codex = Some(app_server);
1330
+ Self::store_codex_backend(&codex, app_server);
1331
1331
  }
1332
1332
  Err(error) => eprintln!(
1333
1333
  "codex backend unavailable; continuing with selected harnesses only: {error}"
@@ -1358,7 +1358,7 @@ impl RuntimeBackend {
1358
1358
  match AppServerBridge::start_codex(&config.cli_bin, hub.clone()).await {
1359
1359
  Ok(app_server) => {
1360
1360
  spawn_rollout_live_sync(hub.clone());
1361
- codex = Some(app_server);
1361
+ Self::store_codex_backend(&codex, app_server);
1362
1362
  }
1363
1363
  Err(error) => eprintln!(
1364
1364
  "codex backend unavailable; continuing with selected harnesses only: {error}"
@@ -1389,6 +1389,19 @@ impl RuntimeBackend {
1389
1389
  self.cursor.read().ok().and_then(|guard| guard.clone())
1390
1390
  }
1391
1391
 
1392
+ fn codex_backend(&self) -> Option<Arc<AppServerBridge>> {
1393
+ self.codex.read().ok().and_then(|guard| guard.clone())
1394
+ }
1395
+
1396
+ fn store_codex_backend(
1397
+ codex_slot: &Arc<StdRwLock<Option<Arc<AppServerBridge>>>>,
1398
+ bridge: Arc<AppServerBridge>,
1399
+ ) {
1400
+ if let Ok(mut guard) = codex_slot.write() {
1401
+ *guard = Some(bridge);
1402
+ }
1403
+ }
1404
+
1392
1405
  fn store_cursor_backend(
1393
1406
  cursor_slot: &Arc<StdRwLock<Option<Arc<AppServerBridge>>>>,
1394
1407
  bridge: Arc<AppServerBridge>,
@@ -1398,8 +1411,31 @@ impl RuntimeBackend {
1398
1411
  }
1399
1412
  }
1400
1413
 
1414
+ async fn restart_codex_app_server(
1415
+ &self,
1416
+ config: &Arc<BridgeConfig>,
1417
+ hub: Arc<ClientHub>,
1418
+ ) -> Result<(), String> {
1419
+ if !config.enabled_engines.contains(&BridgeRuntimeEngine::Codex) {
1420
+ return Err("codex backend is not enabled".to_string());
1421
+ }
1422
+
1423
+ let next_backend = AppServerBridge::start_codex(&config.cli_bin, hub).await?;
1424
+ let previous_backend = self
1425
+ .codex
1426
+ .write()
1427
+ .map(|mut guard| guard.replace(next_backend))
1428
+ .map_err(|_| "codex backend lock is unavailable".to_string())?;
1429
+
1430
+ if let Some(previous_backend) = previous_backend {
1431
+ previous_backend.request_shutdown().await;
1432
+ }
1433
+
1434
+ Ok(())
1435
+ }
1436
+
1401
1437
  async fn shutdown(&self) {
1402
- if let Some(codex) = &self.codex {
1438
+ if let Some(codex) = self.codex_backend() {
1403
1439
  codex.request_shutdown().await;
1404
1440
  }
1405
1441
  if let Some(opencode) = &self.opencode {
@@ -1416,7 +1452,7 @@ impl RuntimeBackend {
1416
1452
 
1417
1453
  fn available_engines(&self) -> Vec<BridgeRuntimeEngine> {
1418
1454
  let mut engines = Vec::new();
1419
- if self.codex.is_some() {
1455
+ if self.codex_backend().is_some() {
1420
1456
  engines.push(BridgeRuntimeEngine::Codex);
1421
1457
  }
1422
1458
  if self.opencode.is_some() {
@@ -1469,8 +1505,7 @@ impl RuntimeBackend {
1469
1505
  ) -> Result<RuntimeBackendRef<'_>, String> {
1470
1506
  match engine {
1471
1507
  BridgeRuntimeEngine::Codex => self
1472
- .codex
1473
- .as_ref()
1508
+ .codex_backend()
1474
1509
  .map(RuntimeBackendRef::Codex)
1475
1510
  .ok_or_else(|| "codex backend is unavailable".to_string()),
1476
1511
  BridgeRuntimeEngine::Opencode => self
@@ -1576,7 +1611,7 @@ impl RuntimeBackend {
1576
1611
  let bridge_cursor = extract_thread_list_cursor(params.as_ref())
1577
1612
  .and_then(|cursor| decode_bridge_thread_list_cursor(&cursor));
1578
1613
 
1579
- if let Some(codex) = &self.codex {
1614
+ if let Some(codex) = self.codex_backend() {
1580
1615
  if let Some(cursor_map) = bridge_cursor.as_ref() {
1581
1616
  if let Some(cursor) = cursor_map.get(&BridgeRuntimeEngine::Codex) {
1582
1617
  results.push((
@@ -1660,7 +1695,7 @@ impl RuntimeBackend {
1660
1695
  async fn aggregate_loaded_thread_ids(&self) -> Result<Value, String> {
1661
1696
  let mut results = Vec::new();
1662
1697
 
1663
- if let Some(codex) = &self.codex {
1698
+ if let Some(codex) = self.codex_backend() {
1664
1699
  results.push((
1665
1700
  BridgeRuntimeEngine::Codex,
1666
1701
  codex.request_internal("thread/loaded/list", None).await?,
@@ -1688,7 +1723,7 @@ impl RuntimeBackend {
1688
1723
 
1689
1724
  async fn list_pending_approvals(&self) -> Vec<PendingApproval> {
1690
1725
  let mut approvals = Vec::new();
1691
- if let Some(codex) = &self.codex {
1726
+ if let Some(codex) = self.codex_backend() {
1692
1727
  approvals.extend(codex.list_pending_approvals().await);
1693
1728
  }
1694
1729
  if let Some(opencode) = &self.opencode {
@@ -1703,7 +1738,7 @@ impl RuntimeBackend {
1703
1738
 
1704
1739
  async fn list_pending_user_inputs(&self) -> Vec<PendingUserInputRequest> {
1705
1740
  let mut requests = Vec::new();
1706
- if let Some(codex) = &self.codex {
1741
+ if let Some(codex) = self.codex_backend() {
1707
1742
  requests.extend(codex.list_pending_user_inputs().await);
1708
1743
  }
1709
1744
  if let Some(opencode) = &self.opencode {
@@ -1721,7 +1756,7 @@ impl RuntimeBackend {
1721
1756
  approval_id: &str,
1722
1757
  decision: &Value,
1723
1758
  ) -> Result<Option<PendingApproval>, String> {
1724
- if let Some(codex) = &self.codex {
1759
+ if let Some(codex) = self.codex_backend() {
1725
1760
  if let Some(approval) = codex.resolve_approval(approval_id, decision).await? {
1726
1761
  return Ok(Some(approval));
1727
1762
  }
@@ -1747,7 +1782,7 @@ impl RuntimeBackend {
1747
1782
  request_id: &str,
1748
1783
  answers: &HashMap<String, UserInputAnswerPayload>,
1749
1784
  ) -> Result<Option<PendingUserInputRequest>, String> {
1750
- if let Some(codex) = &self.codex {
1785
+ if let Some(codex) = self.codex_backend() {
1751
1786
  if let Some(request) = codex.resolve_user_input(request_id, answers).await? {
1752
1787
  return Ok(Some(request));
1753
1788
  }
@@ -1792,7 +1827,7 @@ impl RuntimeBackend {
1792
1827
  }
1793
1828
  }),
1794
1829
  };
1795
- if let Some(codex) = &self.codex {
1830
+ if let Some(codex) = self.codex_backend() {
1796
1831
  codex.hub.send_json(client_id, payload).await;
1797
1832
  } else if let Some(opencode) = &self.opencode {
1798
1833
  opencode.hub.send_json(client_id, payload).await;
@@ -1892,7 +1927,7 @@ async fn terminate_process_tree_windows(pid: u32, label: &str) {
1892
1927
  }
1893
1928
 
1894
1929
  enum RuntimeBackendRef<'a> {
1895
- Codex(&'a Arc<AppServerBridge>),
1930
+ Codex(Arc<AppServerBridge>),
1896
1931
  Opencode(&'a Arc<OpencodeBackend>),
1897
1932
  Cursor(Arc<AppServerBridge>),
1898
1933
  }
@@ -7468,6 +7503,17 @@ async fn handle_bridge_method(
7468
7503
  .map_err(|error| BridgeError::invalid_params(&error.to_string()))?;
7469
7504
  forward_codex_auth_callback(state, &request.callback_url).await
7470
7505
  }
7506
+ "bridge/codex/app-server/restart" => {
7507
+ state
7508
+ .backend
7509
+ .restart_codex_app_server(&state.config, state.hub.clone())
7510
+ .await
7511
+ .map_err(|error| BridgeError::server(&error))?;
7512
+ Ok(json!({
7513
+ "ok": true,
7514
+ "message": "Codex app-server restarted."
7515
+ }))
7516
+ }
7471
7517
  "bridge/update/start" => {
7472
7518
  let request: BridgeUpdateStartRequest =
7473
7519
  serde_json::from_value(params.unwrap_or_else(|| json!({})))
@@ -8156,7 +8202,7 @@ fn thread_list_stream_request_params(include_sub_agents: bool, limit: usize) ->
8156
8202
  json!({
8157
8203
  "cursor": Value::Null,
8158
8204
  "limit": limit,
8159
- "sortKey": Value::Null,
8205
+ "sortKey": "updated_at",
8160
8206
  "modelProviders": Value::Null,
8161
8207
  "sourceKinds": source_kinds,
8162
8208
  "archived": false,
@@ -8212,7 +8258,7 @@ async fn list_workspace_roots(
8212
8258
  Some(json!({
8213
8259
  "cursor": Value::Null,
8214
8260
  "limit": limit,
8215
- "sortKey": Value::Null,
8261
+ "sortKey": "updated_at",
8216
8262
  "modelProviders": Value::Null,
8217
8263
  "sourceKinds": ["cli", "vscode", "exec", "appServer", "unknown"],
8218
8264
  "archived": false,
@@ -13586,16 +13632,15 @@ mod tests {
13586
13632
  let _ = child.wait().await;
13587
13633
  }
13588
13634
 
13589
- fn test_codex_backend(backend: &Arc<RuntimeBackend>) -> &Arc<AppServerBridge> {
13635
+ fn test_codex_backend(backend: &Arc<RuntimeBackend>) -> Arc<AppServerBridge> {
13590
13636
  backend
13591
- .codex
13592
- .as_ref()
13637
+ .codex_backend()
13593
13638
  .expect("expected codex backend in test")
13594
13639
  }
13595
13640
 
13596
13641
  async fn shutdown_test_backend(backend: &Arc<RuntimeBackend>) {
13597
- if let Some(codex) = &backend.codex {
13598
- shutdown_test_bridge(codex).await;
13642
+ if let Some(codex) = backend.codex_backend() {
13643
+ shutdown_test_bridge(&codex).await;
13599
13644
  }
13600
13645
  if let Some(opencode) = &backend.opencode {
13601
13646
  shutdown_test_opencode_backend(opencode).await;
@@ -13610,7 +13655,7 @@ mod tests {
13610
13655
  preferred_engine: BridgeRuntimeEngine,
13611
13656
  include_opencode: bool,
13612
13657
  ) -> Arc<RuntimeBackend> {
13613
- let codex = Some(build_test_bridge(hub.clone()).await);
13658
+ let codex = Arc::new(StdRwLock::new(Some(build_test_bridge(hub.clone()).await)));
13614
13659
  let opencode = if include_opencode {
13615
13660
  Some(build_test_opencode_backend(hub).await)
13616
13661
  } else {