@pushpalsdev/cli 1.0.23 → 1.0.25

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.
@@ -637,7 +637,7 @@ function loadPushPalsConfig(options = {}) {
637
637
  workerpalOnlineTtlMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_ONLINE_TTL_MS") ?? remoteNode.workerpal_online_ttl_ms, 15000)),
638
638
  waitForWorkerpalMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_WAIT_FOR_WORKERPAL_MS") ?? remoteNode.wait_for_workerpal_ms, 15000)),
639
639
  autoSpawnWorkerpals: parseBoolEnv("REMOTEBUDDY_AUTO_SPAWN_WORKERPALS") ?? asBoolean(remoteNode.auto_spawn_workerpals, true),
640
- maxWorkerpals: Math.max(1, asInt(remoteNode.max_workerpals, 20)),
640
+ maxWorkerpals: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals, 20)),
641
641
  workerpalStartupTimeoutMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS") ?? remoteNode.workerpal_startup_timeout_ms, 1e4)),
642
642
  workerpalDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_DOCKER") ?? asBoolean(remoteNode.workerpal_docker, true),
643
643
  workerpalRequireDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_REQUIRE_DOCKER") ?? asBoolean(remoteNode.workerpal_require_docker, true),
@@ -2158,6 +2158,52 @@ function stopRuntimeServices(services) {
2158
2158
  } catch {}
2159
2159
  }
2160
2160
  }
2161
+ function resolveGracefulShutdownPriority(name) {
2162
+ if (name === "source_control_manager")
2163
+ return 0;
2164
+ if (name === "remotebuddy")
2165
+ return 1;
2166
+ if (name === "localbuddy")
2167
+ return 2;
2168
+ return 3;
2169
+ }
2170
+ async function waitForRuntimeServicesExit(services, timeoutMs) {
2171
+ if (services.length === 0)
2172
+ return true;
2173
+ const deadline = Date.now() + Math.max(0, timeoutMs);
2174
+ while (Date.now() < deadline) {
2175
+ if (services.every((service) => service.exited))
2176
+ return true;
2177
+ await Bun.sleep(100);
2178
+ }
2179
+ return services.every((service) => service.exited);
2180
+ }
2181
+ async function stopRuntimeServicesGracefully(services, timeoutMs = 1e4) {
2182
+ if (services.length === 0)
2183
+ return;
2184
+ const running = services.filter((service) => !service.exited);
2185
+ if (running.length === 0)
2186
+ return;
2187
+ const ordered = [...running].sort((a, b) => resolveGracefulShutdownPriority(a.name) - resolveGracefulShutdownPriority(b.name));
2188
+ const nonServer = ordered.filter((service) => service.name !== "server");
2189
+ const server = ordered.filter((service) => service.name === "server");
2190
+ for (const service of nonServer) {
2191
+ try {
2192
+ service.proc.kill("SIGTERM");
2193
+ } catch {}
2194
+ }
2195
+ await waitForRuntimeServicesExit(nonServer, Math.max(1000, timeoutMs - 2000));
2196
+ for (const service of server) {
2197
+ try {
2198
+ service.proc.kill("SIGTERM");
2199
+ } catch {}
2200
+ }
2201
+ await waitForRuntimeServicesExit(server, Math.min(3000, timeoutMs));
2202
+ const remaining = ordered.filter((service) => !service.exited);
2203
+ if (remaining.length > 0) {
2204
+ stopRuntimeServices(remaining);
2205
+ }
2206
+ }
2161
2207
  function prependExecutableDirToPath(env, executablePath, platform = process.platform) {
2162
2208
  const resolvedPath = String(executablePath ?? "").trim();
2163
2209
  if (!resolvedPath)
@@ -2384,10 +2430,54 @@ async function resolveWorkerpalDockerProbe(cwd, env, platform = process.platform
2384
2430
  }
2385
2431
  var WORKERPAL_SANDBOX_RUNTIME_TAG_LABEL = "pushpals.runtime_tag";
2386
2432
  var WORKERPAL_SANDBOX_COMPONENT_LABEL = "pushpals.component=workerpals-sandbox";
2433
+ var WORKERPAL_WARM_COMPONENT_LABEL = "pushpals.component=workerpals-warm";
2387
2434
  function resolveConfiguredDockerExecutable(env, platform = process.platform) {
2388
2435
  const configured = String(env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? env.PUSHPALS_DOCKER_BIN ?? (platform === "win32" ? "docker.exe" : "docker")).trim();
2389
2436
  return configured || (platform === "win32" ? "docker.exe" : "docker");
2390
2437
  }
2438
+ async function cleanupLingeringWorkerpalWarmContainers(opts) {
2439
+ const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
2440
+ const dockerExecutable = resolveConfiguredDockerExecutable(opts.env, opts.platform ?? process.platform);
2441
+ const list = await runCommandWithEnvFn([
2442
+ dockerExecutable,
2443
+ "ps",
2444
+ "-aq",
2445
+ "--filter",
2446
+ `label=${WORKERPAL_WARM_COMPONENT_LABEL}`,
2447
+ "--filter",
2448
+ `label=pushpals.repo=${opts.repoRoot}`
2449
+ ], opts.repoRoot, opts.env);
2450
+ if (!list.ok) {
2451
+ const detail = list.stderr || list.stdout || `exit ${list.exitCode}`;
2452
+ return {
2453
+ ok: false,
2454
+ detail: `failed to inspect lingering WorkerPal warm containers: ${detail}`,
2455
+ removed: 0
2456
+ };
2457
+ }
2458
+ const containerIds = list.stdout.split(/\s+/).map((value) => value.trim()).filter(Boolean);
2459
+ if (containerIds.length === 0) {
2460
+ return {
2461
+ ok: true,
2462
+ detail: "no lingering WorkerPal warm containers found",
2463
+ removed: 0
2464
+ };
2465
+ }
2466
+ const remove = await runCommandWithEnvFn([dockerExecutable, "rm", "-f", ...containerIds], opts.repoRoot, opts.env);
2467
+ if (!remove.ok) {
2468
+ const detail = remove.stderr || remove.stdout || `exit ${remove.exitCode}`;
2469
+ return {
2470
+ ok: false,
2471
+ detail: `failed to remove lingering WorkerPal warm containers: ${detail}`,
2472
+ removed: 0
2473
+ };
2474
+ }
2475
+ return {
2476
+ ok: true,
2477
+ detail: `removed ${containerIds.length} lingering WorkerPal warm container(s)`,
2478
+ removed: containerIds.length
2479
+ };
2480
+ }
2391
2481
  async function inspectDockerImageRuntimeTag(dockerExecutable, imageName, cwd, env) {
2392
2482
  const inspect = await runCommandWithEnv([
2393
2483
  dockerExecutable,
@@ -2704,7 +2794,7 @@ function removeCliClearTarget(target) {
2704
2794
  };
2705
2795
  }
2706
2796
  }
2707
- async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2797
+ async function requestLocalRuntimeShutdown(serverUrl, repoRoot, reason) {
2708
2798
  if (!await probeServer(serverUrl)) {
2709
2799
  return { attempted: false, accepted: false };
2710
2800
  }
@@ -2721,7 +2811,7 @@ async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2721
2811
  const response = await fetchWithTimeout(`${serverUrl}/admin/shutdown`, {
2722
2812
  method: "POST",
2723
2813
  headers: { "Content-Type": "application/json" },
2724
- body: JSON.stringify({ reason: "pushpals --clear" })
2814
+ body: JSON.stringify({ reason })
2725
2815
  }, 5000);
2726
2816
  if (!response.ok) {
2727
2817
  const detail = await response.text().catch(() => "");
@@ -2742,7 +2832,7 @@ async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2742
2832
  }
2743
2833
  async function clearPushpalsState(opts) {
2744
2834
  console.log("[pushpals] Clear requested. Removing repo-local PushPals state.");
2745
- const shutdown = await requestLocalRuntimeShutdownForClear(opts.serverUrl, opts.repoRoot);
2835
+ const shutdown = await requestLocalRuntimeShutdown(opts.serverUrl, opts.repoRoot, "pushpals --clear");
2746
2836
  if (shutdown.attempted && shutdown.accepted) {
2747
2837
  console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
2748
2838
  await Bun.sleep(1500);
@@ -3873,12 +3963,47 @@ async function main() {
3873
3963
  let autoStartedServices = [];
3874
3964
  let pushpalsLogPath;
3875
3965
  let resolvedRuntimeTagForAutoStart = preparedRuntime.runtimeTag || parsed.runtimeTag || "";
3966
+ const cleanupWorkerpalWarmContainersIfNeeded = async (phase) => {
3967
+ if (workerpalDockerPrecheck.status === "failed")
3968
+ return;
3969
+ if (!config.remotebuddy.autoSpawnWorkerpals || !config.remotebuddy.workerpalDocker || !config.remotebuddy.workerpalRequireDocker) {
3970
+ return;
3971
+ }
3972
+ const cleanup = await cleanupLingeringWorkerpalWarmContainers({
3973
+ repoRoot,
3974
+ env: workerpalDockerPrecheck.env
3975
+ });
3976
+ if (!cleanup.ok) {
3977
+ console.warn(`[pushpals] WorkerPal warm-container cleanup warning (${phase}): ${cleanup.detail}`);
3978
+ return;
3979
+ }
3980
+ if (cleanup.removed > 0) {
3981
+ console.log(`[pushpals] ${cleanup.detail} (${phase}).`);
3982
+ }
3983
+ };
3876
3984
  const stopAutoStartedServices = () => {
3877
3985
  if (autoStartedServices.length === 0)
3878
3986
  return;
3879
3987
  stopRuntimeServices(autoStartedServices);
3880
3988
  autoStartedServices = [];
3881
3989
  };
3990
+ const stopAutoStartedServicesGracefully = async (reason) => {
3991
+ if (autoStartedServices.length === 0)
3992
+ return;
3993
+ const services = autoStartedServices;
3994
+ autoStartedServices = [];
3995
+ const shutdown = await requestLocalRuntimeShutdown(serverUrl, repoRoot, reason);
3996
+ if (shutdown.attempted && shutdown.accepted) {
3997
+ console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
3998
+ await Bun.sleep(1500);
3999
+ } else if (shutdown.attempted) {
4000
+ console.warn(`[pushpals] Local runtime shutdown request was not accepted${shutdown.detail ? `: ${shutdown.detail}` : "."}`);
4001
+ } else if (shutdown.detail) {
4002
+ console.warn(`[pushpals] ${shutdown.detail}`);
4003
+ }
4004
+ await stopRuntimeServicesGracefully(services);
4005
+ await cleanupWorkerpalWarmContainersIfNeeded("cli shutdown");
4006
+ };
3882
4007
  let serverHealthy = await probeServer(serverUrl);
3883
4008
  const serverWasAlreadyHealthy = serverHealthy;
3884
4009
  if (!serverHealthy && workerpalDockerPrecheck.status === "failed") {
@@ -3906,6 +4031,7 @@ async function main() {
3906
4031
  detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
3907
4032
  };
3908
4033
  if (!serverHealthy) {
4034
+ await cleanupWorkerpalWarmContainersIfNeeded("startup preflight");
3909
4035
  if (!parsed.noAutoStart) {
3910
4036
  try {
3911
4037
  const startedRuntime = await autoStartRuntimeServices({
@@ -4046,26 +4172,36 @@ ${line}
4046
4172
  console.log(line);
4047
4173
  };
4048
4174
  const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, activeSessionId, cliClient, printIncoming, streamAbort.signal);
4049
- let shuttingDown = false;
4175
+ let stopPromise = null;
4050
4176
  const requestStop = () => {
4051
- if (shuttingDown)
4052
- return;
4053
- shuttingDown = true;
4054
- console.log("[pushpals] Shutting down CLI session...");
4055
- streamAbort.abort();
4056
- if (rl)
4057
- rl.close();
4058
- try {
4059
- monitoringHub?.stop();
4060
- } catch {}
4061
- if (autoStartedServices.length > 0) {
4062
- console.log("[pushpals] Stopping embedded runtime services...");
4063
- }
4064
- stopAutoStartedServices();
4177
+ if (stopPromise)
4178
+ return stopPromise;
4179
+ stopPromise = (async () => {
4180
+ console.log("[pushpals] Shutting down CLI session...");
4181
+ streamAbort.abort();
4182
+ const activeRl = rl;
4183
+ rl = null;
4184
+ if (activeRl)
4185
+ activeRl.close();
4186
+ try {
4187
+ monitoringHub?.stop();
4188
+ } catch {}
4189
+ if (autoStartedServices.length > 0) {
4190
+ console.log("[pushpals] Stopping embedded runtime services...");
4191
+ }
4192
+ await stopAutoStartedServicesGracefully("pushpals CLI exit");
4193
+ })();
4194
+ return stopPromise;
4065
4195
  };
4066
- process.once("SIGINT", requestStop);
4067
- process.once("SIGTERM", requestStop);
4068
- process.once("exit", requestStop);
4196
+ process.once("SIGINT", () => {
4197
+ requestStop();
4198
+ });
4199
+ process.once("SIGTERM", () => {
4200
+ requestStop();
4201
+ });
4202
+ process.once("exit", () => {
4203
+ stopAutoStartedServices();
4204
+ });
4069
4205
  if (parsed.runtimeOnly) {
4070
4206
  console.log("[pushpals] Runtime-only mode is active. Send `exit` on stdin or terminate the process to stop.");
4071
4207
  await new Promise((resolveStop) => {
@@ -4095,7 +4231,7 @@ ${line}
4095
4231
  finish();
4096
4232
  });
4097
4233
  });
4098
- requestStop();
4234
+ await requestStop();
4099
4235
  await Promise.race([streamTask, Bun.sleep(2000)]);
4100
4236
  return;
4101
4237
  }
@@ -4113,7 +4249,7 @@ ${line}
4113
4249
  continue;
4114
4250
  }
4115
4251
  if (isCliExitCommand(text)) {
4116
- requestStop();
4252
+ await requestStop();
4117
4253
  break;
4118
4254
  }
4119
4255
  if (text === "/hub") {
@@ -4153,7 +4289,7 @@ ${line}
4153
4289
  }
4154
4290
  rl.prompt();
4155
4291
  }
4156
- requestStop();
4292
+ await requestStop();
4157
4293
  await Promise.race([streamTask, Bun.sleep(2000)]);
4158
4294
  }
4159
4295
  if (import.meta.main) {
@@ -4192,6 +4328,7 @@ export {
4192
4328
  ensureWorkerpalDockerImageReady,
4193
4329
  downloadRuntimeAssetsFromSourceTag,
4194
4330
  copyTrackedRepoPath,
4331
+ cleanupLingeringWorkerpalWarmContainers,
4195
4332
  bundledMonitoringHubNeedsRefresh,
4196
4333
  buildWorkerpalSandboxPaths,
4197
4334
  buildServiceStopCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,7 @@ status_heartbeat_ms = 120000
40
40
  workerpal_online_ttl_ms = 15000
41
41
  wait_for_workerpal_ms = 15000
42
42
  auto_spawn_workerpals = true
43
- max_workerpals = 10
43
+ max_workerpals = 3
44
44
  workerpal_startup_timeout_ms = 10000
45
45
  workerpal_docker = true
46
46
  workerpal_require_docker = true
@@ -143,8 +143,8 @@ docker_warm_max_attempts = 3
143
143
  docker_warm_retry_backoff_ms = 2000
144
144
  docker_job_max_attempts = 2
145
145
  docker_job_retry_backoff_ms = 3000
146
- docker_warm_memory_mb = 2048
147
- docker_warm_cpus = 2
146
+ docker_warm_memory_mb = 1024
147
+ docker_warm_cpus = 1
148
148
  file_modifying_jobs = ["task.execute"]
149
149
  output_max_chars = 196608
150
150
  output_max_lines = 600
@@ -198,7 +198,7 @@ session_id = "workerpals-dev"
198
198
  [workerpals.openai_codex]
199
199
  timeout_ms = 7200000
200
200
  progress_log_interval_s = 30
201
- reasoning_effort = "xhigh"
201
+ reasoning_effort = "high"
202
202
  approval_policy = "never"
203
203
  sandbox = "workspace-write"
204
204
  color = "never"
@@ -12,7 +12,7 @@ model = "gpt-5.4"
12
12
  codex_auth_mode = "chatgpt"
13
13
  codex_bin = "bun x --yes @openai/codex"
14
14
  codex_timeout_ms = 120000
15
- reasoning_effort = "xhigh"
15
+ reasoning_effort = "high"
16
16
 
17
17
  [remotebuddy.llm]
18
18
  backend = "openai_codex"
@@ -20,10 +20,10 @@ model = "gpt-5.4"
20
20
  codex_auth_mode = "chatgpt"
21
21
  codex_bin = "bun x --yes @openai/codex"
22
22
  codex_timeout_ms = 120000
23
- reasoning_effort = "xhigh"
23
+ reasoning_effort = "high"
24
24
 
25
25
  [remotebuddy]
26
- max_workerpals = 10
26
+ max_workerpals = 3
27
27
  crash_restart_enabled = true
28
28
  crash_restart_max_restarts = 3
29
29
  crash_restart_backoff_ms = 3000
@@ -46,7 +46,7 @@ model = "gpt-5.4"
46
46
  codex_auth_mode = "chatgpt"
47
47
  codex_bin = "bun x --yes @openai/codex"
48
48
  codex_timeout_ms = 120000
49
- reasoning_effort = "xhigh"
49
+ reasoning_effort = "high"
50
50
 
51
51
  [workerpals]
52
52
  executor = "openai_codex"
@@ -59,8 +59,8 @@ openhands_stuck_guard_explore_limit = 18
59
59
  openhands_stuck_guard_min_elapsed_ms = 180000
60
60
  openhands_stuck_guard_broad_scan_limit = 2
61
61
  openhands_stuck_guard_no_progress_max_ms = 300000
62
- docker_warm_memory_mb = 2048
63
- docker_warm_cpus = 2
62
+ docker_warm_memory_mb = 1024
63
+ docker_warm_cpus = 1
64
64
  file_modifying_jobs = ["task.execute"]
65
65
  output_max_chars = 196608
66
66
  output_max_lines = 600
@@ -92,7 +92,7 @@ bin = "bun x --yes @openai/codex"
92
92
  timeout_ms = 7200000
93
93
  progress_log_interval_s = 30
94
94
  # timeout_s = 120 # optional; if set, overrides timeout_ms
95
- reasoning_effort = "xhigh"
95
+ reasoning_effort = "high"
96
96
  approval_policy = "never"
97
97
  sandbox = "workspace-write"
98
98
  color = "never"
@@ -83,6 +83,18 @@ _VALID_AUTH_MODES = {"auto", "api_key", "chatgpt"}
83
83
  _VALID_REASONING_EFFORTS = {"low", "medium", "high", "xhigh"}
84
84
 
85
85
 
86
+ def _model_supports_xhigh_reasoning(model: str) -> bool:
87
+ normalized = str(model or "").strip().lower()
88
+ if not normalized:
89
+ return False
90
+ return not (
91
+ normalized == "gpt-5.4"
92
+ or normalized.startswith("gpt-5.4-")
93
+ or normalized == "codex-1p"
94
+ or normalized.startswith("codex-1p-")
95
+ )
96
+
97
+
86
98
  @dataclass(frozen=True)
87
99
  class OpenAICodexRuntimeConfig:
88
100
  codex_bin_json: str
@@ -152,7 +164,7 @@ class OpenAICodexRuntimeConfig:
152
164
  reasoning_effort=cfg.get_str(
153
165
  env_names=("WORKERPALS_LLM_REASONING_EFFORT", "WORKERPALS_OPENAI_CODEX_REASONING_EFFORT"),
154
166
  config_paths=("workerpals.llm.reasoning_effort", "workerpals.openai_codex.reasoning_effort"),
155
- default="xhigh",
167
+ default="high",
156
168
  ),
157
169
  approval_policy=cfg.get_str(
158
170
  env_names=("WORKERPALS_OPENAI_CODEX_APPROVAL_POLICY",),
@@ -316,18 +328,23 @@ def _resolve_communicate_timeout_seconds(config: OpenAICodexRuntimeConfig) -> Op
316
328
  return max(1, timeout_ms // 1000)
317
329
 
318
330
 
319
- def _resolve_reasoning_effort(config: OpenAICodexRuntimeConfig) -> str:
331
+ def _resolve_reasoning_effort(config: OpenAICodexRuntimeConfig, model: str = DEFAULT_CODEX_MODEL) -> str:
320
332
  raw = config.reasoning_effort
321
333
  normalized = str(raw).strip().lower()
322
334
  if normalized in {"extra high", "extra-high", "extrahigh", "x-high"}:
323
335
  normalized = "xhigh"
336
+ if normalized == "xhigh" and not _model_supports_xhigh_reasoning(model):
337
+ log.info(
338
+ f"Downgrading workerpals.openai_codex.reasoning_effort='xhigh' to 'high' for model {model!r}."
339
+ )
340
+ return "high"
324
341
  if normalized in _VALID_REASONING_EFFORTS:
325
342
  return normalized
326
343
  log.info(
327
344
  "Invalid workerpals.openai_codex.reasoning_effort="
328
- f"{raw!r}; using default 'xhigh'. Allowed: low, medium, high, xhigh."
345
+ f"{raw!r}; using default 'high'. Allowed: low, medium, high, xhigh."
329
346
  )
330
- return "xhigh"
347
+ return "high"
331
348
 
332
349
 
333
350
  def _resolve_progress_log_interval_seconds(config: OpenAICodexRuntimeConfig) -> int:
@@ -1006,7 +1023,7 @@ def _run_codex_task(
1006
1023
  )
1007
1024
  # JSON event output is noisy by default; prefer plain text + output-last-message.
1008
1025
  use_json = runtime_config.json_output
1009
- reasoning_effort = _resolve_reasoning_effort(runtime_config)
1026
+ reasoning_effort = _resolve_reasoning_effort(runtime_config, model)
1010
1027
  communicate_timeout_s = _resolve_communicate_timeout_seconds(runtime_config)
1011
1028
  prompt = _build_instruction(instruction, supplemental_guidance)
1012
1029
  baseline_changes = summarize_git_changes(repo)
@@ -60,17 +60,26 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
60
60
  self.assertEqual(cfg.approval_policy, "never")
61
61
  self.assertEqual(cfg.sandbox, "workspace-write")
62
62
  self.assertEqual(cfg.color, "never")
63
- self.assertEqual(cfg.reasoning_effort, "xhigh")
63
+ self.assertEqual(cfg.reasoning_effort, "high")
64
64
  self.assertFalse(cfg.json_output)
65
65
 
66
- def test_reasoning_effort_accepts_extra_high_alias(self) -> None:
66
+ def test_reasoning_effort_caps_extra_high_for_gpt_5_4(self) -> None:
67
67
  cfg = OpenAICodexRuntimeConfig.from_sources(
68
68
  SettingsResolver(
69
69
  env={"WORKERPALS_OPENAI_CODEX_REASONING_EFFORT": "extra high"},
70
70
  config_loader=lambda: {},
71
71
  ),
72
72
  )
73
- self.assertEqual(_resolve_reasoning_effort(cfg), "xhigh")
73
+ self.assertEqual(_resolve_reasoning_effort(cfg), "high")
74
+
75
+ def test_reasoning_effort_preserves_extra_high_for_future_models(self) -> None:
76
+ cfg = OpenAICodexRuntimeConfig.from_sources(
77
+ SettingsResolver(
78
+ env={"WORKERPALS_OPENAI_CODEX_REASONING_EFFORT": "extra high"},
79
+ config_loader=lambda: {},
80
+ ),
81
+ )
82
+ self.assertEqual(_resolve_reasoning_effort(cfg, model="gpt-6-preview"), "xhigh")
74
83
 
75
84
  def test_runtime_config_prefers_explicit_config_dir_override(self) -> None:
76
85
  import executor_base
@@ -1048,6 +1048,7 @@ export class DockerExecutor {
1048
1048
 
1049
1049
  const worktreeRelPath = relative(this.options.repo, worktreePath).replace(/\\/g, "/");
1050
1050
  const containerWorktreePath = `/repo/${worktreeRelPath}`;
1051
+ await this.waitForWorktreePathInWarmContainer(containerWorktreePath);
1051
1052
 
1052
1053
  const args: string[] = [
1053
1054
  "exec",
@@ -1124,6 +1125,26 @@ export class DockerExecutor {
1124
1125
  return result;
1125
1126
  }
1126
1127
 
1128
+ private async waitForWorktreePathInWarmContainer(
1129
+ containerWorktreePath: string,
1130
+ timeoutMs = 5_000,
1131
+ ): Promise<void> {
1132
+ const deadline = Date.now() + timeoutMs;
1133
+ let lastDetail = "";
1134
+ const command = `test -d ${shellSingleQuote(containerWorktreePath)}`;
1135
+ while (Date.now() < deadline) {
1136
+ const result = await this.runWarmShell(command);
1137
+ if (result.ok) return;
1138
+ lastDetail = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1139
+ await this.sleep(100);
1140
+ }
1141
+ throw new Error(
1142
+ `worktree path not visible inside warm container after ${timeoutMs}ms: ${containerWorktreePath}${
1143
+ lastDetail ? ` (${lastDetail})` : ""
1144
+ }`,
1145
+ );
1146
+ }
1147
+
1127
1148
  private normalizeProvider(raw: string): string {
1128
1149
  const value = raw.trim().toLowerCase();
1129
1150
  if (!value) return "auto";
@@ -1450,6 +1471,9 @@ export class DockerExecutor {
1450
1471
  /\btemporary failure\b/i,
1451
1472
  /\bopenhands wrapper timed out\b/i,
1452
1473
  /\bjob timed out in docker executor\b/i,
1474
+ /\bworktree path not visible inside warm container\b/i,
1475
+ /\bchdir to cwd\b/i,
1476
+ /\bunable to start container process\b/i,
1453
1477
  ];
1454
1478
  return transientPatterns.some((pattern) => pattern.test(text));
1455
1479
  }
@@ -2024,17 +2024,19 @@ export function shouldUseCodexCliForExecutor(executor: string): boolean {
2024
2024
 
2025
2025
  function normalizeCodexReasoningEffort(
2026
2026
  value: unknown,
2027
+ model = "",
2027
2028
  ): "low" | "medium" | "high" | "xhigh" {
2028
2029
  const normalized = String(value ?? "")
2029
2030
  .trim()
2030
2031
  .toLowerCase();
2032
+ const supportsExtraHigh = !/^(gpt-5\.4(?:$|-)|codex-1p(?:$|-))/i.test(String(model ?? "").trim());
2031
2033
  if (
2032
2034
  normalized === "low" ||
2033
2035
  normalized === "medium" ||
2034
2036
  normalized === "high" ||
2035
2037
  normalized === "xhigh"
2036
2038
  ) {
2037
- return normalized;
2039
+ return normalized === "xhigh" && !supportsExtraHigh ? "high" : normalized;
2038
2040
  }
2039
2041
  if (
2040
2042
  normalized === "extra high" ||
@@ -2042,9 +2044,9 @@ function normalizeCodexReasoningEffort(
2042
2044
  normalized === "extrahigh" ||
2043
2045
  normalized === "x-high"
2044
2046
  ) {
2045
- return "xhigh";
2047
+ return supportsExtraHigh ? "xhigh" : "high";
2046
2048
  }
2047
- return "xhigh";
2049
+ return "high";
2048
2050
  }
2049
2051
 
2050
2052
  async function generateCommitMessageFromDiff(
@@ -2105,6 +2107,7 @@ async function generateCommitMessageFromDiffViaCodex(
2105
2107
  })();
2106
2108
  const reasoningEffort = normalizeCodexReasoningEffort(
2107
2109
  runtimeConfig.workerpals.llm.reasoningEffort,
2110
+ model,
2108
2111
  );
2109
2112
  const tmpOutputPath = resolve(
2110
2113
  Bun.env.TEMP || Bun.env.TMP || Bun.env.TMPDIR || "/tmp",
@@ -40,7 +40,7 @@ status_heartbeat_ms = 120000
40
40
  workerpal_online_ttl_ms = 15000
41
41
  wait_for_workerpal_ms = 15000
42
42
  auto_spawn_workerpals = true
43
- max_workerpals = 10
43
+ max_workerpals = 3
44
44
  workerpal_startup_timeout_ms = 10000
45
45
  workerpal_docker = true
46
46
  workerpal_require_docker = true
@@ -143,8 +143,8 @@ docker_warm_max_attempts = 3
143
143
  docker_warm_retry_backoff_ms = 2000
144
144
  docker_job_max_attempts = 2
145
145
  docker_job_retry_backoff_ms = 3000
146
- docker_warm_memory_mb = 2048
147
- docker_warm_cpus = 2
146
+ docker_warm_memory_mb = 1024
147
+ docker_warm_cpus = 1
148
148
  file_modifying_jobs = ["task.execute"]
149
149
  output_max_chars = 196608
150
150
  output_max_lines = 600
@@ -198,7 +198,7 @@ session_id = "workerpals-dev"
198
198
  [workerpals.openai_codex]
199
199
  timeout_ms = 7200000
200
200
  progress_log_interval_s = 30
201
- reasoning_effort = "xhigh"
201
+ reasoning_effort = "high"
202
202
  approval_policy = "never"
203
203
  sandbox = "workspace-write"
204
204
  color = "never"
@@ -12,7 +12,7 @@ model = "gpt-5.4"
12
12
  codex_auth_mode = "chatgpt"
13
13
  codex_bin = "bun x --yes @openai/codex"
14
14
  codex_timeout_ms = 120000
15
- reasoning_effort = "xhigh"
15
+ reasoning_effort = "high"
16
16
 
17
17
  [remotebuddy.llm]
18
18
  backend = "openai_codex"
@@ -20,10 +20,10 @@ model = "gpt-5.4"
20
20
  codex_auth_mode = "chatgpt"
21
21
  codex_bin = "bun x --yes @openai/codex"
22
22
  codex_timeout_ms = 120000
23
- reasoning_effort = "xhigh"
23
+ reasoning_effort = "high"
24
24
 
25
25
  [remotebuddy]
26
- max_workerpals = 10
26
+ max_workerpals = 3
27
27
  crash_restart_enabled = true
28
28
  crash_restart_max_restarts = 3
29
29
  crash_restart_backoff_ms = 3000
@@ -46,7 +46,7 @@ model = "gpt-5.4"
46
46
  codex_auth_mode = "chatgpt"
47
47
  codex_bin = "bun x --yes @openai/codex"
48
48
  codex_timeout_ms = 120000
49
- reasoning_effort = "xhigh"
49
+ reasoning_effort = "high"
50
50
 
51
51
  [workerpals]
52
52
  executor = "openai_codex"
@@ -59,8 +59,8 @@ openhands_stuck_guard_explore_limit = 18
59
59
  openhands_stuck_guard_min_elapsed_ms = 180000
60
60
  openhands_stuck_guard_broad_scan_limit = 2
61
61
  openhands_stuck_guard_no_progress_max_ms = 300000
62
- docker_warm_memory_mb = 2048
63
- docker_warm_cpus = 2
62
+ docker_warm_memory_mb = 1024
63
+ docker_warm_cpus = 1
64
64
  file_modifying_jobs = ["task.execute"]
65
65
  output_max_chars = 196608
66
66
  output_max_lines = 600
@@ -92,7 +92,7 @@ bin = "bun x --yes @openai/codex"
92
92
  timeout_ms = 7200000
93
93
  progress_log_interval_s = 30
94
94
  # timeout_s = 120 # optional; if set, overrides timeout_ms
95
- reasoning_effort = "xhigh"
95
+ reasoning_effort = "high"
96
96
  approval_policy = "never"
97
97
  sandbox = "workspace-write"
98
98
  color = "never"
@@ -1508,7 +1508,10 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
1508
1508
  asBoolean(remoteNode.auto_spawn_workerpals, true),
1509
1509
  maxWorkerpals: Math.max(
1510
1510
  1,
1511
- asInt(remoteNode.max_workerpals, 20),
1511
+ asInt(
1512
+ parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals,
1513
+ 20,
1514
+ ),
1512
1515
  ),
1513
1516
  workerpalStartupTimeoutMs: Math.max(
1514
1517
  1_000,