@pushpalsdev/cli 1.1.13 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pushpals-cli.js +2 -1
- package/package.json +1 -1
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1 -1
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +63 -0
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +59 -2
package/dist/pushpals-cli.js
CHANGED
|
@@ -5315,8 +5315,9 @@ function shouldSuppressCliSessionJobLogLine(line) {
|
|
|
5315
5315
|
return true;
|
|
5316
5316
|
if (/^\[QualityGate\]\s+(?:Policy:|Gates:)/i.test(text))
|
|
5317
5317
|
return true;
|
|
5318
|
-
if (/^\[
|
|
5318
|
+
if (/^\[(?:Openai_codex|OpenHands|Miniswe)Executor\]\s+(?:Spawning\b|Timeout reached\b|Still running\b|Process did not exit after graceful timeout termination\b)/i.test(text)) {
|
|
5319
5319
|
return true;
|
|
5320
|
+
}
|
|
5320
5321
|
if (/^\[OpenAICodexExecutor\]\s+(?:Planner guidance|Codex auth mode|ChatGPT auth mode|Starting codex exec|codex exec finished|Codex JSON stream captured|Codex stdout captured|No reasoning-like|Reasoning-like event|Usage observed|Temporarily masked repo-local|Timeout reached after|Process did not exit after graceful timeout termination)/i.test(text)) {
|
|
5321
5322
|
return true;
|
|
5322
5323
|
}
|
package/package.json
CHANGED
|
@@ -1892,7 +1892,7 @@ def _run_codex_task(
|
|
|
1892
1892
|
command_policy_rejection_loop = False
|
|
1893
1893
|
no_edit_watchdog_s = (
|
|
1894
1894
|
_resolve_no_edit_watchdog_seconds(prompt, communicate_timeout_s)
|
|
1895
|
-
if no_edit_recovery_attempt
|
|
1895
|
+
if no_edit_recovery_attempt <= _MAX_NO_EDIT_RECOVERY_ATTEMPTS
|
|
1896
1896
|
else None
|
|
1897
1897
|
)
|
|
1898
1898
|
no_edit_deadline = (
|
|
@@ -823,6 +823,69 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
|
|
|
823
823
|
self.assertIn("Patched immediately after no-edit recovery", str(result.get("stdout") or ""))
|
|
824
824
|
self.assertIn("src/", str(result.get("stdout") or ""))
|
|
825
825
|
|
|
826
|
+
def test_run_codex_task_recovery_attempt_is_still_guarded_by_no_edit_watchdog(self) -> None:
|
|
827
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-no-edit-watchdog-fail-") as temp_dir:
|
|
828
|
+
repo = Path(temp_dir) / "repo"
|
|
829
|
+
repo.mkdir(parents=True, exist_ok=True)
|
|
830
|
+
(repo / "README.md").write_text("# no edit watchdog failure repo\n", encoding="utf-8")
|
|
831
|
+
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True, text=True)
|
|
832
|
+
subprocess.run(
|
|
833
|
+
["git", "config", "user.name", "PushPals Test"],
|
|
834
|
+
cwd=repo,
|
|
835
|
+
check=True,
|
|
836
|
+
capture_output=True,
|
|
837
|
+
text=True,
|
|
838
|
+
)
|
|
839
|
+
subprocess.run(
|
|
840
|
+
["git", "config", "user.email", "pushpals-tests@example.com"],
|
|
841
|
+
cwd=repo,
|
|
842
|
+
check=True,
|
|
843
|
+
capture_output=True,
|
|
844
|
+
text=True,
|
|
845
|
+
)
|
|
846
|
+
subprocess.run(["git", "add", "README.md"], cwd=repo, check=True, capture_output=True, text=True)
|
|
847
|
+
subprocess.run(
|
|
848
|
+
["git", "commit", "-m", "chore: seed no-edit watchdog failure repo"],
|
|
849
|
+
cwd=repo,
|
|
850
|
+
check=True,
|
|
851
|
+
capture_output=True,
|
|
852
|
+
text=True,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
stub_path = Path(temp_dir) / "fake_codex_no_edit_watchdog_fail.py"
|
|
856
|
+
stub_path.write_text(
|
|
857
|
+
"\n".join(
|
|
858
|
+
[
|
|
859
|
+
"import sys",
|
|
860
|
+
"import time",
|
|
861
|
+
"",
|
|
862
|
+
"sys.stdin.read()",
|
|
863
|
+
"print('item.completed | Still inspecting, no patch yet.', flush=True)",
|
|
864
|
+
"time.sleep(10)",
|
|
865
|
+
]
|
|
866
|
+
),
|
|
867
|
+
encoding="utf-8",
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
env_overrides = {
|
|
871
|
+
"PUSHPALS_OPENAI_CODEX_BIN_JSON": json.dumps([sys.executable, str(stub_path)]),
|
|
872
|
+
"PUSHPALS_OPENAI_CODEX_AUTH_MODE": "api_key",
|
|
873
|
+
"OPENAI_API_KEY": "pushpals-no-edit-watchdog-fail-test-key",
|
|
874
|
+
"WORKERPALS_OPENAI_CODEX_TIMEOUT_S": "20",
|
|
875
|
+
"WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "1",
|
|
876
|
+
"WORKERPALS_OPENAI_CODEX_PROGRESS_LOG_INTERVAL_S": "1",
|
|
877
|
+
}
|
|
878
|
+
with mock.patch.dict(os.environ, env_overrides, clear=False):
|
|
879
|
+
result = _run_codex_task(
|
|
880
|
+
str(repo),
|
|
881
|
+
"Polish the first-entry home shell with a compact visual patch.",
|
|
882
|
+
[],
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
self.assertFalse(result.get("ok"), result)
|
|
886
|
+
self.assertEqual(result.get("exitCode"), 124)
|
|
887
|
+
self.assertIn("no publishable changes", str(result.get("summary") or ""))
|
|
888
|
+
|
|
826
889
|
def test_codex_changed_paths_filters_dependency_artifacts_from_publishable_delta(self) -> None:
|
|
827
890
|
with tempfile.TemporaryDirectory(prefix="pushpals-codex-artifact-delta-") as temp_dir:
|
|
828
891
|
repo = Path(temp_dir) / "repo"
|
|
@@ -28,6 +28,8 @@ interface GenericPythonExecutorConfig {
|
|
|
28
28
|
capTimeoutToExecutionBudget?: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const BACKEND_TIMEOUT_RESULT_GRACE_MS = 30_000;
|
|
32
|
+
|
|
31
33
|
function estimateTokensFromText(text: string): number {
|
|
32
34
|
return Math.max(0, Math.ceil(String(text ?? "").length / 3));
|
|
33
35
|
}
|
|
@@ -123,6 +125,7 @@ function resolveRuntimeSettings(
|
|
|
123
125
|
export function resolveGenericPythonExecutorTimeoutMs(params: {
|
|
124
126
|
configuredTimeoutMs: number;
|
|
125
127
|
executionBudgetMs?: number | null;
|
|
128
|
+
finalizationBudgetMs?: number | null;
|
|
126
129
|
capTimeoutToExecutionBudget?: boolean;
|
|
127
130
|
}): number {
|
|
128
131
|
const configuredTimeoutMs = Math.max(10_000, Math.floor(params.configuredTimeoutMs));
|
|
@@ -130,12 +133,49 @@ export function resolveGenericPythonExecutorTimeoutMs(params: {
|
|
|
130
133
|
typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
|
|
131
134
|
? Math.max(10_000, Math.floor(params.executionBudgetMs))
|
|
132
135
|
: null;
|
|
136
|
+
const finalizationBudgetMs =
|
|
137
|
+
typeof params.finalizationBudgetMs === "number" && Number.isFinite(params.finalizationBudgetMs)
|
|
138
|
+
? Math.max(0, Math.floor(params.finalizationBudgetMs))
|
|
139
|
+
: 0;
|
|
133
140
|
if (executionBudgetMs != null && params.capTimeoutToExecutionBudget !== false) {
|
|
134
|
-
return Math.min(configuredTimeoutMs, executionBudgetMs);
|
|
141
|
+
return Math.min(configuredTimeoutMs, executionBudgetMs + finalizationBudgetMs);
|
|
135
142
|
}
|
|
136
143
|
return configuredTimeoutMs;
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
export function resolveGenericPythonExecutorChildTimeoutMs(params: {
|
|
147
|
+
backendName: string;
|
|
148
|
+
hostTimeoutMs: number;
|
|
149
|
+
executionBudgetMs?: number | null;
|
|
150
|
+
}): number | null {
|
|
151
|
+
const hostTimeoutMs = Math.max(10_000, Math.floor(params.hostTimeoutMs));
|
|
152
|
+
if (params.backendName !== "openai_codex") return null;
|
|
153
|
+
const executionBudgetMs =
|
|
154
|
+
typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
|
|
155
|
+
? Math.max(10_000, Math.floor(params.executionBudgetMs))
|
|
156
|
+
: null;
|
|
157
|
+
const childBudgetMs =
|
|
158
|
+
executionBudgetMs == null ? hostTimeoutMs : Math.min(hostTimeoutMs, executionBudgetMs);
|
|
159
|
+
const graceMs = Math.min(
|
|
160
|
+
BACKEND_TIMEOUT_RESULT_GRACE_MS,
|
|
161
|
+
Math.max(2_000, Math.floor(childBudgetMs / 10)),
|
|
162
|
+
);
|
|
163
|
+
return Math.max(1_000, childBudgetMs - graceMs);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function resolveGenericPythonExecutorChildTimeoutEnv(params: {
|
|
167
|
+
backendName: string;
|
|
168
|
+
hostTimeoutMs: number;
|
|
169
|
+
executionBudgetMs?: number | null;
|
|
170
|
+
}): Record<string, string> {
|
|
171
|
+
const childTimeoutMs = resolveGenericPythonExecutorChildTimeoutMs(params);
|
|
172
|
+
if (childTimeoutMs == null) return {};
|
|
173
|
+
return {
|
|
174
|
+
WORKERPALS_OPENAI_CODEX_TIMEOUT_MS: String(childTimeoutMs),
|
|
175
|
+
WORKERPALS_OPENAI_CODEX_TIMEOUT_S: String(Math.max(1, Math.floor(childTimeoutMs / 1000))),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
139
179
|
function toSnakeConfigKey(key: string): string {
|
|
140
180
|
return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
141
181
|
}
|
|
@@ -144,6 +184,7 @@ function formatGenericPythonExecutorTimeoutDetail(
|
|
|
144
184
|
config: GenericPythonExecutorConfig,
|
|
145
185
|
configuredTimeoutMs: number,
|
|
146
186
|
executionBudgetMs: number | null,
|
|
187
|
+
finalizationBudgetMs: number | null,
|
|
147
188
|
timeoutMs: number,
|
|
148
189
|
): string {
|
|
149
190
|
const configPath = `workerpals.${toSnakeConfigKey(config.timeoutConfigKey)}`;
|
|
@@ -154,7 +195,11 @@ function formatGenericPythonExecutorTimeoutDetail(
|
|
|
154
195
|
return `${configPath}=${configuredTimeoutMs}ms; planning executionBudgetMs=${executionBudgetMs}ms ignored by backend opt-out`;
|
|
155
196
|
}
|
|
156
197
|
if (timeoutMs < configuredTimeoutMs) {
|
|
157
|
-
|
|
198
|
+
const finalizationDetail =
|
|
199
|
+
finalizationBudgetMs && finalizationBudgetMs > 0
|
|
200
|
+
? ` + finalizationBudgetMs=${finalizationBudgetMs}ms`
|
|
201
|
+
: "";
|
|
202
|
+
return `${configPath}=${configuredTimeoutMs}ms capped by planning executionBudgetMs=${executionBudgetMs}ms${finalizationDetail}`;
|
|
158
203
|
}
|
|
159
204
|
return `${configPath}=${configuredTimeoutMs}ms within planning executionBudgetMs=${executionBudgetMs}ms`;
|
|
160
205
|
}
|
|
@@ -190,15 +235,21 @@ export function createGenericPythonExecutor(
|
|
|
190
235
|
typeof budgets?.executionBudgetMs === "number" && Number.isFinite(budgets.executionBudgetMs)
|
|
191
236
|
? Math.max(10_000, Math.floor(budgets.executionBudgetMs))
|
|
192
237
|
: null;
|
|
238
|
+
const finalizationBudgetMs =
|
|
239
|
+
typeof budgets?.finalizationBudgetMs === "number" && Number.isFinite(budgets.finalizationBudgetMs)
|
|
240
|
+
? Math.max(0, Math.floor(budgets.finalizationBudgetMs))
|
|
241
|
+
: null;
|
|
193
242
|
const timeoutMs = resolveGenericPythonExecutorTimeoutMs({
|
|
194
243
|
configuredTimeoutMs,
|
|
195
244
|
executionBudgetMs,
|
|
245
|
+
finalizationBudgetMs,
|
|
196
246
|
capTimeoutToExecutionBudget: config.capTimeoutToExecutionBudget,
|
|
197
247
|
});
|
|
198
248
|
const timeoutDetail = formatGenericPythonExecutorTimeoutDetail(
|
|
199
249
|
config,
|
|
200
250
|
configuredTimeoutMs,
|
|
201
251
|
executionBudgetMs,
|
|
252
|
+
finalizationBudgetMs,
|
|
202
253
|
timeoutMs,
|
|
203
254
|
);
|
|
204
255
|
const payloadBase64 = Buffer.from(
|
|
@@ -210,6 +261,11 @@ export function createGenericPythonExecutor(
|
|
|
210
261
|
"utf-8",
|
|
211
262
|
).toString("base64");
|
|
212
263
|
const args = [pythonBin, scriptPath, payloadBase64];
|
|
264
|
+
const childTimeoutEnv = resolveGenericPythonExecutorChildTimeoutEnv({
|
|
265
|
+
backendName,
|
|
266
|
+
hostTimeoutMs: timeoutMs,
|
|
267
|
+
executionBudgetMs,
|
|
268
|
+
});
|
|
213
269
|
|
|
214
270
|
onLog?.(
|
|
215
271
|
"stdout",
|
|
@@ -229,6 +285,7 @@ export function createGenericPythonExecutor(
|
|
|
229
285
|
stderr: "pipe",
|
|
230
286
|
env: {
|
|
231
287
|
...buildWorkerSandboxWritableEnv(repo),
|
|
288
|
+
...childTimeoutEnv,
|
|
232
289
|
PUSHPALS_REPO_PATH: repo,
|
|
233
290
|
PUSHPALS_ASSIGNED_REPO_ROOT: repo,
|
|
234
291
|
PYTHONIOENCODING: "utf-8",
|