@pushpalsdev/cli 1.1.39 → 1.1.41
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
CHANGED
|
@@ -58,11 +58,13 @@ function shouldRestartService(attempts, maxAttempts = DEFAULT_SERVICE_MANAGER_MA
|
|
|
58
58
|
return normalizedAttempts < normalizedMax;
|
|
59
59
|
}
|
|
60
60
|
function pipeProcessStreamToLines(stream, onLine) {
|
|
61
|
-
if (!stream || typeof stream === "number" || typeof stream.getReader !== "function")
|
|
62
|
-
return;
|
|
61
|
+
if (!stream || typeof stream === "number" || typeof stream.getReader !== "function") {
|
|
62
|
+
return { cancel: () => {} };
|
|
63
|
+
}
|
|
63
64
|
const reader = stream.getReader();
|
|
64
65
|
const decoder = new TextDecoder;
|
|
65
66
|
let pending = "";
|
|
67
|
+
let cancelled = false;
|
|
66
68
|
(async () => {
|
|
67
69
|
try {
|
|
68
70
|
while (true) {
|
|
@@ -86,9 +88,21 @@ function pipeProcessStreamToLines(stream, onLine) {
|
|
|
86
88
|
if (tail)
|
|
87
89
|
onLine?.(tail);
|
|
88
90
|
} catch {} finally {
|
|
89
|
-
|
|
91
|
+
try {
|
|
92
|
+
reader.releaseLock();
|
|
93
|
+
} catch {}
|
|
90
94
|
}
|
|
91
95
|
})();
|
|
96
|
+
return {
|
|
97
|
+
cancel: () => {
|
|
98
|
+
if (cancelled)
|
|
99
|
+
return;
|
|
100
|
+
cancelled = true;
|
|
101
|
+
try {
|
|
102
|
+
reader.cancel().catch(() => {});
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
92
106
|
}
|
|
93
107
|
function spawnManagedService(spec) {
|
|
94
108
|
const env = { ...spec.env ?? {} };
|
|
@@ -98,8 +112,8 @@ function spawnManagedService(spec) {
|
|
|
98
112
|
stdout: "pipe",
|
|
99
113
|
stderr: "pipe"
|
|
100
114
|
});
|
|
101
|
-
pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
|
|
102
|
-
pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
|
|
115
|
+
const stdoutPipe = pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
|
|
116
|
+
const stderrPipe = pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
|
|
103
117
|
const service = {
|
|
104
118
|
name: spec.name,
|
|
105
119
|
proc,
|
|
@@ -109,7 +123,11 @@ function spawnManagedService(spec) {
|
|
|
109
123
|
exited: false,
|
|
110
124
|
exitCode: null,
|
|
111
125
|
launchedAtMs: Date.now(),
|
|
112
|
-
logPath: spec.logPath
|
|
126
|
+
logPath: spec.logPath,
|
|
127
|
+
stopOutputPipes: () => {
|
|
128
|
+
stdoutPipe.cancel();
|
|
129
|
+
stderrPipe.cancel();
|
|
130
|
+
}
|
|
113
131
|
};
|
|
114
132
|
proc.exited.then((code) => {
|
|
115
133
|
service.exited = true;
|
|
@@ -161,6 +179,7 @@ class ServiceManager {
|
|
|
161
179
|
const existing = this.services.get(spec.name);
|
|
162
180
|
if (existing && !existing.exited) {
|
|
163
181
|
try {
|
|
182
|
+
existing.stopOutputPipes?.();
|
|
164
183
|
const pid = existing.proc.pid;
|
|
165
184
|
if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
|
|
166
185
|
Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
|
|
@@ -224,6 +243,7 @@ class ServiceManager {
|
|
|
224
243
|
this.stopped = true;
|
|
225
244
|
for (const service of this.services.values()) {
|
|
226
245
|
try {
|
|
246
|
+
service.stopOutputPipes?.();
|
|
227
247
|
const pid = service.proc.pid;
|
|
228
248
|
if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
|
|
229
249
|
Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
|
|
@@ -3221,6 +3241,7 @@ function buildServiceStopCommand(pid, platform = process.platform) {
|
|
|
3221
3241
|
function stopRuntimeServices(services) {
|
|
3222
3242
|
for (const service of services) {
|
|
3223
3243
|
try {
|
|
3244
|
+
service.stopOutputPipes?.();
|
|
3224
3245
|
const stopCommand = buildServiceStopCommand(service.proc.pid, process.platform);
|
|
3225
3246
|
if (stopCommand) {
|
|
3226
3247
|
Bun.spawnSync(stopCommand, {
|
|
@@ -3236,6 +3257,52 @@ function stopRuntimeServices(services) {
|
|
|
3236
3257
|
} catch {}
|
|
3237
3258
|
}
|
|
3238
3259
|
}
|
|
3260
|
+
async function runWindowsServiceStopCommand(command, timeoutMs) {
|
|
3261
|
+
const proc = Bun.spawn(command, {
|
|
3262
|
+
stdin: "ignore",
|
|
3263
|
+
stdout: "ignore",
|
|
3264
|
+
stderr: "ignore"
|
|
3265
|
+
});
|
|
3266
|
+
let timeout = null;
|
|
3267
|
+
let timedOut = false;
|
|
3268
|
+
const exitCode = await Promise.race([
|
|
3269
|
+
proc.exited,
|
|
3270
|
+
new Promise((resolveTimeout) => {
|
|
3271
|
+
timeout = setTimeout(() => {
|
|
3272
|
+
timedOut = true;
|
|
3273
|
+
try {
|
|
3274
|
+
proc.kill("SIGKILL");
|
|
3275
|
+
} catch {}
|
|
3276
|
+
resolveTimeout(-1);
|
|
3277
|
+
}, Math.max(250, timeoutMs));
|
|
3278
|
+
})
|
|
3279
|
+
]);
|
|
3280
|
+
if (timeout) {
|
|
3281
|
+
clearTimeout(timeout);
|
|
3282
|
+
timeout = null;
|
|
3283
|
+
}
|
|
3284
|
+
if (timedOut) {
|
|
3285
|
+
await Promise.race([proc.exited.catch(() => -1), Bun.sleep(250)]);
|
|
3286
|
+
return false;
|
|
3287
|
+
}
|
|
3288
|
+
return exitCode === 0;
|
|
3289
|
+
}
|
|
3290
|
+
async function stopRuntimeServicesOnWindows(services, timeoutMs) {
|
|
3291
|
+
const deadline = Date.now() + Math.max(1000, timeoutMs);
|
|
3292
|
+
for (const service of services) {
|
|
3293
|
+
service.stopOutputPipes?.();
|
|
3294
|
+
const stopCommand = buildServiceStopCommand(service.proc.pid, "win32");
|
|
3295
|
+
if (stopCommand) {
|
|
3296
|
+
const remainingMs = Math.max(250, deadline - Date.now());
|
|
3297
|
+
const stopped = await runWindowsServiceStopCommand(stopCommand, Math.min(WINDOWS_TASKKILL_TIMEOUT_MS2, remainingMs));
|
|
3298
|
+
if (stopped)
|
|
3299
|
+
continue;
|
|
3300
|
+
}
|
|
3301
|
+
try {
|
|
3302
|
+
service.proc.kill("SIGKILL");
|
|
3303
|
+
} catch {}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3239
3306
|
function resolveGracefulShutdownPriority(name) {
|
|
3240
3307
|
if (name === "source_control_manager")
|
|
3241
3308
|
return 0;
|
|
@@ -3264,7 +3331,7 @@ async function stopRuntimeServicesGracefully(services, timeoutMs = 1e4) {
|
|
|
3264
3331
|
return;
|
|
3265
3332
|
const ordered = [...running].sort((a, b) => resolveGracefulShutdownPriority(a.name) - resolveGracefulShutdownPriority(b.name));
|
|
3266
3333
|
if (process.platform === "win32") {
|
|
3267
|
-
|
|
3334
|
+
await stopRuntimeServicesOnWindows(ordered, timeoutMs);
|
|
3268
3335
|
await waitForRuntimeServicesExit(ordered, Math.min(1000, timeoutMs));
|
|
3269
3336
|
return;
|
|
3270
3337
|
}
|
|
@@ -3295,6 +3362,7 @@ async function shutdownEmbeddedServiceManagerGracefully(options) {
|
|
|
3295
3362
|
reason,
|
|
3296
3363
|
requestShutdown = requestLocalRuntimeShutdown,
|
|
3297
3364
|
shutdownAcceptedDelayMs = 1500,
|
|
3365
|
+
serviceStopTimeoutMs = 1e4,
|
|
3298
3366
|
onLog = (line) => console.log(line),
|
|
3299
3367
|
onWarn = (line) => console.warn(line),
|
|
3300
3368
|
cleanupTasks = []
|
|
@@ -3310,7 +3378,7 @@ async function shutdownEmbeddedServiceManagerGracefully(options) {
|
|
|
3310
3378
|
} else if (shutdown.detail) {
|
|
3311
3379
|
onWarn(`[pushpals] ${shutdown.detail}`);
|
|
3312
3380
|
}
|
|
3313
|
-
await stopRuntimeServicesGracefully(services);
|
|
3381
|
+
await stopRuntimeServicesGracefully(services, serviceStopTimeoutMs);
|
|
3314
3382
|
for (const task of cleanupTasks) {
|
|
3315
3383
|
await task();
|
|
3316
3384
|
}
|
package/package.json
CHANGED
|
@@ -119,6 +119,7 @@ _DEFAULT_NO_EDIT_RECHECK_S = 120
|
|
|
119
119
|
_NO_EDIT_RECOVERY_RECHECK_S = 30
|
|
120
120
|
_DEFAULT_NO_EDIT_COMMAND_GRACE_S = 240
|
|
121
121
|
_DEFAULT_NO_EDIT_COMMAND_PROGRESS_CAP_S = 360
|
|
122
|
+
_BACKGROUND_NO_EDIT_COMMAND_PROGRESS_CAP_S = 120
|
|
122
123
|
_NO_EDIT_RECOVERY_COMMAND_PROGRESS_CAP_S = 120
|
|
123
124
|
_DEFAULT_STARTUP_STALL_WATCHDOG_S = 210
|
|
124
125
|
_RECOVERY_STARTUP_STALL_WATCHDOG_S = 150
|
|
@@ -817,6 +818,7 @@ def _resolve_no_edit_command_progress_cap_seconds(
|
|
|
817
818
|
communicate_timeout_s: Optional[int],
|
|
818
819
|
no_edit_command_grace_s: Optional[int],
|
|
819
820
|
recovery_attempt: int = 0,
|
|
821
|
+
prompt: str = "",
|
|
820
822
|
) -> Optional[int]:
|
|
821
823
|
if not communicate_timeout_s or no_edit_command_grace_s is None:
|
|
822
824
|
return None
|
|
@@ -834,11 +836,12 @@ def _resolve_no_edit_command_progress_cap_seconds(
|
|
|
834
836
|
else:
|
|
835
837
|
return max(1, min(parsed, max(1, communicate_timeout_s - 1)))
|
|
836
838
|
|
|
837
|
-
|
|
838
|
-
_NO_EDIT_RECOVERY_COMMAND_PROGRESS_CAP_S
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
839
|
+
if recovery_attempt > 0:
|
|
840
|
+
default_s = _NO_EDIT_RECOVERY_COMMAND_PROGRESS_CAP_S
|
|
841
|
+
elif _looks_like_background_autonomy_prompt(prompt):
|
|
842
|
+
default_s = _BACKGROUND_NO_EDIT_COMMAND_PROGRESS_CAP_S
|
|
843
|
+
else:
|
|
844
|
+
default_s = _DEFAULT_NO_EDIT_COMMAND_PROGRESS_CAP_S
|
|
842
845
|
upper = max(1, communicate_timeout_s - 1)
|
|
843
846
|
return max(1, min(default_s, upper))
|
|
844
847
|
|
|
@@ -2713,6 +2716,7 @@ def _run_codex_task(
|
|
|
2713
2716
|
communicate_timeout_s,
|
|
2714
2717
|
no_edit_command_grace_s,
|
|
2715
2718
|
recovery_attempt=recovery_depth,
|
|
2719
|
+
prompt=prompt,
|
|
2716
2720
|
)
|
|
2717
2721
|
startup_stall_watchdog_s = _resolve_startup_stall_watchdog_seconds(
|
|
2718
2722
|
communicate_timeout_s,
|
|
@@ -45,6 +45,7 @@ from openai_codex_executor import (
|
|
|
45
45
|
_has_credible_shell_wrapper_progress,
|
|
46
46
|
_load_prompt_template,
|
|
47
47
|
_mask_repo_local_codex_files,
|
|
48
|
+
_minimum_recovery_attempt_seconds,
|
|
48
49
|
_repo_root_for_prompt_loading,
|
|
49
50
|
_restore_repo_local_codex_files,
|
|
50
51
|
_resolve_codex_command_prefix,
|
|
@@ -249,6 +250,41 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
|
|
|
249
250
|
)
|
|
250
251
|
self.assertEqual(_resolve_rollout_watchdog_seconds(prompt, 1200, no_edit), 90)
|
|
251
252
|
|
|
253
|
+
def test_background_autonomy_caps_patchless_command_progress_before_recovery_reserve(self) -> None:
|
|
254
|
+
prompt = (
|
|
255
|
+
"Task planning contract from PushPals:\n"
|
|
256
|
+
"- Planning summary: intent=code_change, risk=low, priority=background\n"
|
|
257
|
+
"- Origin=autonomy targetPaths=[app/__tests__/opportunity-graph.contract.test.ts]\n"
|
|
258
|
+
"Add focused contract coverage without broad discovery.\n"
|
|
259
|
+
)
|
|
260
|
+
child_budget_s = 570
|
|
261
|
+
|
|
262
|
+
with mock.patch.dict(
|
|
263
|
+
os.environ,
|
|
264
|
+
{
|
|
265
|
+
"WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_GRACE_S": "",
|
|
266
|
+
"WORKERPALS_OPENAI_CODEX_NO_EDIT_COMMAND_PROGRESS_CAP_S": "",
|
|
267
|
+
"WORKERPALS_OPENAI_CODEX_STARTUP_STALL_WATCHDOG_S": "",
|
|
268
|
+
},
|
|
269
|
+
clear=False,
|
|
270
|
+
):
|
|
271
|
+
command_grace_s = _resolve_no_edit_command_grace_seconds(child_budget_s)
|
|
272
|
+
command_cap_s = _resolve_no_edit_command_progress_cap_seconds(
|
|
273
|
+
child_budget_s,
|
|
274
|
+
command_grace_s,
|
|
275
|
+
prompt=prompt,
|
|
276
|
+
)
|
|
277
|
+
startup_stall_s = _resolve_startup_stall_watchdog_seconds(child_budget_s)
|
|
278
|
+
|
|
279
|
+
self.assertEqual(command_grace_s, 240)
|
|
280
|
+
self.assertEqual(command_cap_s, 120)
|
|
281
|
+
self.assertEqual(startup_stall_s, 210)
|
|
282
|
+
first_attempt_patchless_ceiling_s = startup_stall_s + command_cap_s
|
|
283
|
+
self.assertGreaterEqual(
|
|
284
|
+
child_budget_s - first_attempt_patchless_ceiling_s,
|
|
285
|
+
2 * _minimum_recovery_attempt_seconds(child_budget_s),
|
|
286
|
+
)
|
|
287
|
+
|
|
252
288
|
def test_runtime_config_prefers_explicit_config_dir_override(self) -> None:
|
|
253
289
|
import executor_base
|
|
254
290
|
|