@pushpalsdev/cli 1.0.23 → 1.0.24

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.
@@ -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)
@@ -2704,7 +2750,7 @@ function removeCliClearTarget(target) {
2704
2750
  };
2705
2751
  }
2706
2752
  }
2707
- async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2753
+ async function requestLocalRuntimeShutdown(serverUrl, repoRoot, reason) {
2708
2754
  if (!await probeServer(serverUrl)) {
2709
2755
  return { attempted: false, accepted: false };
2710
2756
  }
@@ -2721,7 +2767,7 @@ async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2721
2767
  const response = await fetchWithTimeout(`${serverUrl}/admin/shutdown`, {
2722
2768
  method: "POST",
2723
2769
  headers: { "Content-Type": "application/json" },
2724
- body: JSON.stringify({ reason: "pushpals --clear" })
2770
+ body: JSON.stringify({ reason })
2725
2771
  }, 5000);
2726
2772
  if (!response.ok) {
2727
2773
  const detail = await response.text().catch(() => "");
@@ -2742,7 +2788,7 @@ async function requestLocalRuntimeShutdownForClear(serverUrl, repoRoot) {
2742
2788
  }
2743
2789
  async function clearPushpalsState(opts) {
2744
2790
  console.log("[pushpals] Clear requested. Removing repo-local PushPals state.");
2745
- const shutdown = await requestLocalRuntimeShutdownForClear(opts.serverUrl, opts.repoRoot);
2791
+ const shutdown = await requestLocalRuntimeShutdown(opts.serverUrl, opts.repoRoot, "pushpals --clear");
2746
2792
  if (shutdown.attempted && shutdown.accepted) {
2747
2793
  console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
2748
2794
  await Bun.sleep(1500);
@@ -3879,6 +3925,22 @@ async function main() {
3879
3925
  stopRuntimeServices(autoStartedServices);
3880
3926
  autoStartedServices = [];
3881
3927
  };
3928
+ const stopAutoStartedServicesGracefully = async (reason) => {
3929
+ if (autoStartedServices.length === 0)
3930
+ return;
3931
+ const services = autoStartedServices;
3932
+ autoStartedServices = [];
3933
+ const shutdown = await requestLocalRuntimeShutdown(serverUrl, repoRoot, reason);
3934
+ if (shutdown.attempted && shutdown.accepted) {
3935
+ console.log("[pushpals] Local runtime shutdown accepted; waiting for services to exit...");
3936
+ await Bun.sleep(1500);
3937
+ } else if (shutdown.attempted) {
3938
+ console.warn(`[pushpals] Local runtime shutdown request was not accepted${shutdown.detail ? `: ${shutdown.detail}` : "."}`);
3939
+ } else if (shutdown.detail) {
3940
+ console.warn(`[pushpals] ${shutdown.detail}`);
3941
+ }
3942
+ await stopRuntimeServicesGracefully(services);
3943
+ };
3882
3944
  let serverHealthy = await probeServer(serverUrl);
3883
3945
  const serverWasAlreadyHealthy = serverHealthy;
3884
3946
  if (!serverHealthy && workerpalDockerPrecheck.status === "failed") {
@@ -4046,26 +4108,36 @@ ${line}
4046
4108
  console.log(line);
4047
4109
  };
4048
4110
  const streamTask = parsed.noStream ? Promise.resolve() : parsed.runtimeOnly ? Promise.resolve() : runSessionStream(serverUrl, activeSessionId, cliClient, printIncoming, streamAbort.signal);
4049
- let shuttingDown = false;
4111
+ let stopPromise = null;
4050
4112
  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();
4113
+ if (stopPromise)
4114
+ return stopPromise;
4115
+ stopPromise = (async () => {
4116
+ console.log("[pushpals] Shutting down CLI session...");
4117
+ streamAbort.abort();
4118
+ const activeRl = rl;
4119
+ rl = null;
4120
+ if (activeRl)
4121
+ activeRl.close();
4122
+ try {
4123
+ monitoringHub?.stop();
4124
+ } catch {}
4125
+ if (autoStartedServices.length > 0) {
4126
+ console.log("[pushpals] Stopping embedded runtime services...");
4127
+ }
4128
+ await stopAutoStartedServicesGracefully("pushpals CLI exit");
4129
+ })();
4130
+ return stopPromise;
4065
4131
  };
4066
- process.once("SIGINT", requestStop);
4067
- process.once("SIGTERM", requestStop);
4068
- process.once("exit", requestStop);
4132
+ process.once("SIGINT", () => {
4133
+ requestStop();
4134
+ });
4135
+ process.once("SIGTERM", () => {
4136
+ requestStop();
4137
+ });
4138
+ process.once("exit", () => {
4139
+ stopAutoStartedServices();
4140
+ });
4069
4141
  if (parsed.runtimeOnly) {
4070
4142
  console.log("[pushpals] Runtime-only mode is active. Send `exit` on stdin or terminate the process to stop.");
4071
4143
  await new Promise((resolveStop) => {
@@ -4095,7 +4167,7 @@ ${line}
4095
4167
  finish();
4096
4168
  });
4097
4169
  });
4098
- requestStop();
4170
+ await requestStop();
4099
4171
  await Promise.race([streamTask, Bun.sleep(2000)]);
4100
4172
  return;
4101
4173
  }
@@ -4113,7 +4185,7 @@ ${line}
4113
4185
  continue;
4114
4186
  }
4115
4187
  if (isCliExitCommand(text)) {
4116
- requestStop();
4188
+ await requestStop();
4117
4189
  break;
4118
4190
  }
4119
4191
  if (text === "/hub") {
@@ -4153,7 +4225,7 @@ ${line}
4153
4225
  }
4154
4226
  rl.prompt();
4155
4227
  }
4156
- requestStop();
4228
+ await requestStop();
4157
4229
  await Promise.race([streamTask, Bun.sleep(2000)]);
4158
4230
  }
4159
4231
  if (import.meta.main) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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,7 +20,7 @@ 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
26
  max_workerpals = 10
@@ -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"
@@ -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",
@@ -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,7 +20,7 @@ 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
26
  max_workerpals = 10
@@ -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"
@@ -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"