@pushpalsdev/cli 1.1.19 → 1.1.21
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 +16 -2
- package/package.json +1 -1
- package/runtime/configs/default.toml +1 -0
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +40 -8
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +45 -3
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +42 -1
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +226 -41
- package/runtime/sandbox/apps/workerpals/src/merge_conflict_job.ts +9 -0
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +13 -26
- package/runtime/sandbox/apps/workerpals/src/worktree_base_ref.ts +141 -0
- package/runtime/sandbox/configs/default.toml +1 -0
- package/runtime/sandbox/packages/shared/src/config.ts +9 -0
package/dist/pushpals-cli.js
CHANGED
|
@@ -1040,6 +1040,7 @@ function loadPushPalsConfig(options = {}) {
|
|
|
1040
1040
|
enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, true),
|
|
1041
1041
|
killSwitchEnabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ?? asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
|
|
1042
1042
|
tickIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ?? remoteAutonomyNode.tick_interval_ms, 120000)),
|
|
1043
|
+
startupGraceMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_STARTUP_GRACE_MS") ?? remoteAutonomyNode.startup_grace_ms, 120000)),
|
|
1043
1044
|
heartbeatLogMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ?? remoteAutonomyNode.heartbeat_log_ms, 30000)),
|
|
1044
1045
|
visionContextMaxChars: Math.max(1000, Math.min(1e6, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_VISION_CONTEXT_MAX_CHARS") ?? remoteAutonomyNode.vision_context_max_chars, 65536))),
|
|
1045
1046
|
ideationBudgetMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_BUDGET_MS") ?? remoteAutonomyNode.ideation_budget_ms, 20000)),
|
|
@@ -1648,7 +1649,8 @@ var DEFAULT_STARTUP_GIT_PROBE_TIMEOUT_MS = 5000;
|
|
|
1648
1649
|
var DEFAULT_STARTUP_GIT_REMOTE_TIMEOUT_MS = 1e4;
|
|
1649
1650
|
var DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS = 5000;
|
|
1650
1651
|
var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
|
|
1651
|
-
var
|
|
1652
|
+
var DEFAULT_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 5000;
|
|
1653
|
+
var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS_ENV = "PUSHPALS_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS";
|
|
1652
1654
|
var BLOCKING_WORKERPAL_IMAGE_BUILD_ENV = "PUSHPALS_BLOCKING_WORKERPAL_IMAGE_BUILD";
|
|
1653
1655
|
var CLI_SESSION_JOB_LOG_MAX_CHARS = 700;
|
|
1654
1656
|
var CLI_SESSION_SHOW_JOB_EVENTS_ENV = "PUSHPALS_CLI_SHOW_JOB_EVENTS";
|
|
@@ -1681,6 +1683,13 @@ function formatTimestampedCliLine(line, at = new Date) {
|
|
|
1681
1683
|
function isTruthyCliEnvValue(value) {
|
|
1682
1684
|
return /^(1|true|yes|on)$/i.test(String(value ?? "").trim());
|
|
1683
1685
|
}
|
|
1686
|
+
function parseCliIntEnv(name, env = process.env) {
|
|
1687
|
+
const raw = env[name];
|
|
1688
|
+
if (raw == null || String(raw).trim() === "")
|
|
1689
|
+
return null;
|
|
1690
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
1691
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1692
|
+
}
|
|
1684
1693
|
function shouldShowCliSessionOperationalEvents(env = process.env) {
|
|
1685
1694
|
return isTruthyCliEnvValue(env[CLI_SESSION_SHOW_JOB_EVENTS_ENV]);
|
|
1686
1695
|
}
|
|
@@ -3899,8 +3908,12 @@ async function precheckWorkerpalDockerAvailability(opts) {
|
|
|
3899
3908
|
function resolveWorkerpalCapacityTimeoutMs(config) {
|
|
3900
3909
|
return Math.max(config.remotebuddy.waitForWorkerpalMs, config.remotebuddy.workerpalStartupTimeoutMs, config.remotebuddy.workerpalDocker ? config.workerpals.dockerAgentStartupTimeoutMs + 15000 : 0, 1e4);
|
|
3901
3910
|
}
|
|
3911
|
+
function resolveWorkerpalStartupReadinessProbeMaxMs(env = process.env) {
|
|
3912
|
+
const configured = parseCliIntEnv(WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS_ENV, env);
|
|
3913
|
+
return Math.max(1000, configured ?? DEFAULT_WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS);
|
|
3914
|
+
}
|
|
3902
3915
|
function resolveWorkerpalStartupReadinessProbeTimeoutMs(config) {
|
|
3903
|
-
return Math.max(
|
|
3916
|
+
return Math.max(1000, Math.min(resolveWorkerpalCapacityTimeoutMs(config), resolveWorkerpalStartupReadinessProbeMaxMs()));
|
|
3904
3917
|
}
|
|
3905
3918
|
function shouldPrepareEmbeddedWorkerpalDockerImageBlocking(opts = {}) {
|
|
3906
3919
|
const env = opts.env ?? process.env;
|
|
@@ -6082,6 +6095,7 @@ export {
|
|
|
6082
6095
|
shouldPrepareEmbeddedWorkerpalDockerImageBlocking,
|
|
6083
6096
|
shouldDeferRemoteBuddySessionConsumerReadiness,
|
|
6084
6097
|
runCommandWithEnv,
|
|
6098
|
+
resolveWorkerpalStartupReadinessProbeMaxMs,
|
|
6085
6099
|
resolveWorkerpalDockerProbe,
|
|
6086
6100
|
resolveWorkerExecutionReadiness,
|
|
6087
6101
|
resolveWindowsWhereExecutableCandidatesForEnv,
|
package/package.json
CHANGED
|
@@ -1323,6 +1323,7 @@ function loadPushPalsConfig(options = {}) {
|
|
|
1323
1323
|
enabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_ENABLED") ?? asBoolean(remoteAutonomyNode.enabled, true),
|
|
1324
1324
|
killSwitchEnabled: parseBoolEnv("REMOTEBUDDY_AUTONOMY_KILL_SWITCH_ENABLED") ?? asBoolean(remoteAutonomyNode.kill_switch_enabled, false),
|
|
1325
1325
|
tickIntervalMs: Math.max(5000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_TICK_INTERVAL_MS") ?? remoteAutonomyNode.tick_interval_ms, 120000)),
|
|
1326
|
+
startupGraceMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_STARTUP_GRACE_MS") ?? remoteAutonomyNode.startup_grace_ms, 120000)),
|
|
1326
1327
|
heartbeatLogMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_HEARTBEAT_LOG_MS") ?? remoteAutonomyNode.heartbeat_log_ms, 30000)),
|
|
1327
1328
|
visionContextMaxChars: Math.max(1000, Math.min(1e6, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_VISION_CONTEXT_MAX_CHARS") ?? remoteAutonomyNode.vision_context_max_chars, 65536))),
|
|
1328
1329
|
ideationBudgetMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_AUTONOMY_IDEATION_BUDGET_MS") ?? remoteAutonomyNode.ideation_budget_ms, 20000)),
|
|
@@ -6569,6 +6570,7 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6569
6570
|
cfg;
|
|
6570
6571
|
runtimeEnabled = true;
|
|
6571
6572
|
timer = null;
|
|
6573
|
+
startupGraceTimer = null;
|
|
6572
6574
|
startupFastTickTimer = null;
|
|
6573
6575
|
heartbeatTimer = null;
|
|
6574
6576
|
inFlight = false;
|
|
@@ -6641,7 +6643,8 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6641
6643
|
console.log(`[RemoteBuddyAutonomousEngine] heartbeat: status=running run=${this.currentRunId} phase=${this.currentPhase} run_elapsed_ms=${runElapsedMs} phase_elapsed_ms=${phaseElapsedMs}`);
|
|
6642
6644
|
return;
|
|
6643
6645
|
}
|
|
6644
|
-
const
|
|
6646
|
+
const hasScheduledTick = Boolean(this.timer || this.startupGraceTimer || this.startupFastTickTimer);
|
|
6647
|
+
const nextTickInMs = hasScheduledTick && this.nextTickAtMs > 0 ? Math.max(0, this.nextTickAtMs - now) : 0;
|
|
6645
6648
|
const lastAgeMs = this.lastCompletedAtMs > 0 ? Math.max(0, now - this.lastCompletedAtMs) : -1;
|
|
6646
6649
|
console.log(`[RemoteBuddyAutonomousEngine] heartbeat: status=idle last_outcome=${this.lastOutcome} detail=${this.lastDetail} last_tick_age_ms=${lastAgeMs} next_tick_in_ms=${nextTickInMs}`);
|
|
6647
6650
|
}
|
|
@@ -6667,6 +6670,15 @@ class RemoteBuddyAutonomousEngine {
|
|
|
6667
6670
|
startupFastTickDelayMs() {
|
|
6668
6671
|
return Math.max(1000, Math.min(STARTUP_FAST_TICK_MAX_DELAY_MS, Math.floor(this.cfg.tickIntervalMs / 10)));
|
|
6669
6672
|
}
|
|
6673
|
+
startupGraceMs() {
|
|
6674
|
+
return Math.max(0, this.cfg.startupGraceMs ?? 0);
|
|
6675
|
+
}
|
|
6676
|
+
clearStartupGraceTimer() {
|
|
6677
|
+
if (this.startupGraceTimer) {
|
|
6678
|
+
clearTimeout(this.startupGraceTimer);
|
|
6679
|
+
this.startupGraceTimer = null;
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6670
6682
|
clearStartupFastTickTimer() {
|
|
6671
6683
|
if (this.startupFastTickTimer) {
|
|
6672
6684
|
clearTimeout(this.startupFastTickTimer);
|
|
@@ -8160,22 +8172,42 @@ Scope:
|
|
|
8160
8172
|
});
|
|
8161
8173
|
}
|
|
8162
8174
|
start() {
|
|
8163
|
-
if (!this.runtimeEnabled || this.timer)
|
|
8175
|
+
if (!this.runtimeEnabled || this.timer || this.startupGraceTimer)
|
|
8164
8176
|
return;
|
|
8165
8177
|
console.log(`[RemoteBuddyAutonomousEngine] Using dedicated autonomy worktree ${this.autonomyRepo} (remote=${this.gitRemote} integration=${this.integrationBranch} base=${this.baseBranch}).`);
|
|
8166
8178
|
this.startupFastTickAttemptsRemaining = STARTUP_FAST_TICK_MAX_ATTEMPTS;
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
this.
|
|
8171
|
-
|
|
8179
|
+
const startInterval = () => {
|
|
8180
|
+
if (this.timer)
|
|
8181
|
+
return;
|
|
8182
|
+
this.timer = setInterval(() => {
|
|
8183
|
+
this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
|
|
8184
|
+
this.tick();
|
|
8185
|
+
}, this.cfg.tickIntervalMs);
|
|
8186
|
+
};
|
|
8187
|
+
const firstTickDelayMs = this.startupGraceMs();
|
|
8188
|
+
this.nextTickAtMs = Date.now() + firstTickDelayMs;
|
|
8172
8189
|
this.heartbeatTimer = setInterval(() => {
|
|
8173
8190
|
this.logHeartbeat();
|
|
8174
8191
|
}, this.cfg.heartbeatLogMs);
|
|
8175
8192
|
this.logHeartbeat();
|
|
8193
|
+
if (firstTickDelayMs > 0) {
|
|
8194
|
+
console.log(`[RemoteBuddyAutonomousEngine] startup autonomy tick delayed by ${firstTickDelayMs}ms to leave cold-start capacity available for user work.`);
|
|
8195
|
+
this.startupGraceTimer = setTimeout(() => {
|
|
8196
|
+
this.startupGraceTimer = null;
|
|
8197
|
+
if (!this.runtimeEnabled)
|
|
8198
|
+
return;
|
|
8199
|
+
startInterval();
|
|
8200
|
+
this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
|
|
8201
|
+
this.tick();
|
|
8202
|
+
}, firstTickDelayMs);
|
|
8203
|
+
return;
|
|
8204
|
+
}
|
|
8205
|
+
startInterval();
|
|
8206
|
+
this.nextTickAtMs = Date.now() + this.cfg.tickIntervalMs;
|
|
8176
8207
|
this.tick();
|
|
8177
8208
|
}
|
|
8178
8209
|
stop() {
|
|
8210
|
+
this.clearStartupGraceTimer();
|
|
8179
8211
|
this.clearStartupFastTickTimer();
|
|
8180
8212
|
if (this.timer) {
|
|
8181
8213
|
clearInterval(this.timer);
|
|
@@ -9014,7 +9046,7 @@ class RemoteBuddyOrchestrator {
|
|
|
9014
9046
|
console.log(`[RemoteBuddy] Budgets: interactive=${this.executionBudgetInteractiveMs}ms normal=${this.executionBudgetNormalMs}ms background=${this.executionBudgetBackgroundMs}ms finalization=${this.finalizationBudgetMs}ms`);
|
|
9015
9047
|
console.log(`[RemoteBuddy] Failure log fetch on job failures: ${this.fetchFailureLogsOnJobFailure ? "on" : "off"}`);
|
|
9016
9048
|
console.log(`[RemoteBuddy] Persistent memory: ${this.memoryEnabled ? "on" : "off"} crossSession=${this.memoryIncludeCrossSession ? "on" : "off"} recallItems=${this.memoryMaxRecallItems} recallChars=${this.memoryMaxRecallChars} retentionDays=${this.memoryRetentionDays}`);
|
|
9017
|
-
console.log(`[RemoteBuddy] Autonomous engine: ${CONFIG.remotebuddy.autonomy.enabled ? "enabled" : "disabled"} tick=${CONFIG.remotebuddy.autonomy.tickIntervalMs}ms maxConcurrentObjectives=${CONFIG.remotebuddy.autonomy.maxConcurrentObjectives} maxDispatchPerHour=${CONFIG.remotebuddy.autonomy.maxDispatchPerHour} exploreRate=${CONFIG.remotebuddy.autonomy.exploreRate.toFixed(2)} allowDirtyWorktree=${CONFIG.remotebuddy.autonomy.allowDirtyWorktree ? "on" : "off"}`);
|
|
9049
|
+
console.log(`[RemoteBuddy] Autonomous engine: ${CONFIG.remotebuddy.autonomy.enabled ? "enabled" : "disabled"} tick=${CONFIG.remotebuddy.autonomy.tickIntervalMs}ms startupGrace=${CONFIG.remotebuddy.autonomy.startupGraceMs}ms maxConcurrentObjectives=${CONFIG.remotebuddy.autonomy.maxConcurrentObjectives} maxDispatchPerHour=${CONFIG.remotebuddy.autonomy.maxDispatchPerHour} exploreRate=${CONFIG.remotebuddy.autonomy.exploreRate.toFixed(2)} allowDirtyWorktree=${CONFIG.remotebuddy.autonomy.allowDirtyWorktree ? "on" : "off"}`);
|
|
9018
9050
|
console.log(`[RemoteBuddy] Autonomy runtime-config polling: every ${this.autonomyConfigPollMs}ms`);
|
|
9019
9051
|
}
|
|
9020
9052
|
async emitStartupStatus() {
|
|
@@ -29,6 +29,9 @@ interface GenericPythonExecutorConfig {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const BACKEND_TIMEOUT_RESULT_GRACE_MS = 30_000;
|
|
32
|
+
const OPENAI_CODEX_MIN_VALIDATION_RESERVE_MS = 180_000;
|
|
33
|
+
const OPENAI_CODEX_MAX_VALIDATION_RESERVE_MS = 600_000;
|
|
34
|
+
const OPENAI_CODEX_MIN_PRIMARY_TURN_BUDGET_MS = 600_000;
|
|
32
35
|
|
|
33
36
|
function estimateTokensFromText(text: string): number {
|
|
34
37
|
return Math.max(0, Math.ceil(String(text ?? "").length / 3));
|
|
@@ -143,6 +146,27 @@ export function resolveGenericPythonExecutorTimeoutMs(params: {
|
|
|
143
146
|
return configuredTimeoutMs;
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
export function resolveOpenAICodexValidationReserveMs(
|
|
150
|
+
executionBudgetMs: number | null | undefined,
|
|
151
|
+
): number {
|
|
152
|
+
if (typeof executionBudgetMs !== "number" || !Number.isFinite(executionBudgetMs)) return 0;
|
|
153
|
+
const budgetMs = Math.max(10_000, Math.floor(executionBudgetMs));
|
|
154
|
+
const targetReserveMs = Math.floor(
|
|
155
|
+
Math.min(
|
|
156
|
+
budgetMs,
|
|
157
|
+
Math.max(
|
|
158
|
+
OPENAI_CODEX_MIN_VALIDATION_RESERVE_MS,
|
|
159
|
+
Math.min(OPENAI_CODEX_MAX_VALIDATION_RESERVE_MS, budgetMs * 0.35),
|
|
160
|
+
),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
const maxReserveAfterPrimaryTurn = Math.max(
|
|
164
|
+
0,
|
|
165
|
+
budgetMs - OPENAI_CODEX_MIN_PRIMARY_TURN_BUDGET_MS,
|
|
166
|
+
);
|
|
167
|
+
return Math.max(0, Math.min(targetReserveMs, maxReserveAfterPrimaryTurn));
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
export function resolveGenericPythonExecutorChildTimeoutMs(params: {
|
|
147
171
|
backendName: string;
|
|
148
172
|
hostTimeoutMs: number;
|
|
@@ -154,8 +178,11 @@ export function resolveGenericPythonExecutorChildTimeoutMs(params: {
|
|
|
154
178
|
typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
|
|
155
179
|
? Math.max(10_000, Math.floor(params.executionBudgetMs))
|
|
156
180
|
: null;
|
|
181
|
+
const validationReserveMs = resolveOpenAICodexValidationReserveMs(executionBudgetMs);
|
|
157
182
|
const childBudgetMs =
|
|
158
|
-
executionBudgetMs == null
|
|
183
|
+
executionBudgetMs == null
|
|
184
|
+
? hostTimeoutMs
|
|
185
|
+
: Math.min(hostTimeoutMs, Math.max(1_000, executionBudgetMs - validationReserveMs));
|
|
159
186
|
const graceMs = Math.min(
|
|
160
187
|
BACKEND_TIMEOUT_RESULT_GRACE_MS,
|
|
161
188
|
Math.max(2_000, Math.floor(childBudgetMs / 10)),
|
|
@@ -305,15 +332,30 @@ export function createGenericPythonExecutor(
|
|
|
305
332
|
"utf-8",
|
|
306
333
|
).toString("base64");
|
|
307
334
|
const args = [pythonBin, scriptPath, payloadBase64];
|
|
308
|
-
const
|
|
335
|
+
const childTimeoutMs = resolveGenericPythonExecutorChildTimeoutMs({
|
|
309
336
|
backendName,
|
|
310
337
|
hostTimeoutMs: timeoutMs,
|
|
311
338
|
executionBudgetMs,
|
|
312
339
|
});
|
|
340
|
+
const childTimeoutEnv =
|
|
341
|
+
childTimeoutMs == null
|
|
342
|
+
? {}
|
|
343
|
+
: {
|
|
344
|
+
WORKERPALS_OPENAI_CODEX_TIMEOUT_MS: String(childTimeoutMs),
|
|
345
|
+
WORKERPALS_OPENAI_CODEX_TIMEOUT_S: String(
|
|
346
|
+
Math.max(1, Math.floor(childTimeoutMs / 1000)),
|
|
347
|
+
),
|
|
348
|
+
};
|
|
349
|
+
const childTimeoutDetail =
|
|
350
|
+
childTimeoutMs != null
|
|
351
|
+
? `; codex_child_timeout=${childTimeoutMs}ms; reserved_validation_budget=${resolveOpenAICodexValidationReserveMs(
|
|
352
|
+
executionBudgetMs,
|
|
353
|
+
)}ms`
|
|
354
|
+
: "";
|
|
313
355
|
|
|
314
356
|
onLog?.(
|
|
315
357
|
"stdout",
|
|
316
|
-
`[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail})`,
|
|
358
|
+
`[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail}${childTimeoutDetail})`,
|
|
317
359
|
);
|
|
318
360
|
|
|
319
361
|
try {
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
DockerWarmShellResult,
|
|
35
35
|
DockerWarmStartupContext,
|
|
36
36
|
} from "./backends/types.js";
|
|
37
|
+
import { resolveFreshWorktreeBaseRef } from "./worktree_base_ref.js";
|
|
37
38
|
|
|
38
39
|
const DEFAULT_OPENHANDS_MODEL = "local-model";
|
|
39
40
|
const DEFAULT_CONFIG = loadPushPalsConfig();
|
|
@@ -2106,7 +2107,27 @@ export class DockerExecutor {
|
|
|
2106
2107
|
reviewAgent && typeof reviewAgent.resolutionType === "string"
|
|
2107
2108
|
? reviewAgent.resolutionType.trim().toLowerCase()
|
|
2108
2109
|
: "";
|
|
2109
|
-
if (resolutionType !== "merge_conflict")
|
|
2110
|
+
if (resolutionType !== "merge_conflict") {
|
|
2111
|
+
return resolveFreshWorktreeBaseRef({
|
|
2112
|
+
requestedRef: this.options.baseRef,
|
|
2113
|
+
integrationBranch:
|
|
2114
|
+
this.config.sourceControlManager.mainBranch ||
|
|
2115
|
+
this.config.workerpals.baseRef ||
|
|
2116
|
+
this.options.baseRef,
|
|
2117
|
+
sourceBaseBranch: this.config.sourceControlManager.baseBranch,
|
|
2118
|
+
git: (args) => this.runGitBaseRefCommand(args),
|
|
2119
|
+
log: (level, message) => {
|
|
2120
|
+
const line = `[DockerExecutor] ${message}`;
|
|
2121
|
+
if (level === "warn") {
|
|
2122
|
+
console.warn(line);
|
|
2123
|
+
onLog?.("stderr", line);
|
|
2124
|
+
} else {
|
|
2125
|
+
console.log(line);
|
|
2126
|
+
onLog?.("stdout", line);
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2110
2131
|
|
|
2111
2132
|
const normalizedHeadRef = normalizeMergeConflictHeadRef(reviewAgent?.prHeadRef);
|
|
2112
2133
|
if (!normalizedHeadRef) {
|
|
@@ -2150,6 +2171,26 @@ export class DockerExecutor {
|
|
|
2150
2171
|
return remoteRef;
|
|
2151
2172
|
}
|
|
2152
2173
|
|
|
2174
|
+
private async runGitBaseRefCommand(
|
|
2175
|
+
args: string[],
|
|
2176
|
+
): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
2177
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
2178
|
+
cwd: this.options.repo,
|
|
2179
|
+
stdout: "pipe",
|
|
2180
|
+
stderr: "pipe",
|
|
2181
|
+
});
|
|
2182
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
2183
|
+
proc.exited,
|
|
2184
|
+
new Response(proc.stdout).text(),
|
|
2185
|
+
new Response(proc.stderr).text(),
|
|
2186
|
+
]);
|
|
2187
|
+
return {
|
|
2188
|
+
ok: exitCode === 0,
|
|
2189
|
+
stdout,
|
|
2190
|
+
stderr,
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2153
2194
|
/**
|
|
2154
2195
|
* Pull the Docker image
|
|
2155
2196
|
*/
|
|
@@ -190,6 +190,7 @@ export interface QualityGatePolicy {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
const BROWSER_VALIDATION_MAX_AUTO_REVISIONS = 3;
|
|
193
|
+
const CRITIC_COMPACT_RETRY_MIN_REDUCTION_RATIO = 0.25;
|
|
193
194
|
|
|
194
195
|
export function qualityRevisionLoopUpperBound(policy: {
|
|
195
196
|
maxAutoRevisions: number;
|
|
@@ -232,6 +233,37 @@ export function qualityRevisionBudgetDecision(opts: {
|
|
|
232
233
|
};
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
export function shouldRetryCriticTimeoutWithCompact(opts: {
|
|
237
|
+
timeoutBehavior: string;
|
|
238
|
+
qualityOk: boolean;
|
|
239
|
+
validationPassed: boolean;
|
|
240
|
+
initialPromptChars: number;
|
|
241
|
+
compactPromptChars: number;
|
|
242
|
+
}): boolean {
|
|
243
|
+
if (opts.timeoutBehavior !== "retry_once") return false;
|
|
244
|
+
if (!opts.qualityOk || !opts.validationPassed) return true;
|
|
245
|
+
const initialPromptChars = Math.max(1, Math.floor(opts.initialPromptChars));
|
|
246
|
+
const compactPromptChars = Math.max(0, Math.floor(opts.compactPromptChars));
|
|
247
|
+
const reductionRatio = 1 - compactPromptChars / initialPromptChars;
|
|
248
|
+
return reductionRatio >= CRITIC_COMPACT_RETRY_MIN_REDUCTION_RATIO;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function shouldSkipCriticAfterExecutorTimeout(opts: {
|
|
252
|
+
executor: string;
|
|
253
|
+
policyMode: string;
|
|
254
|
+
executorText: string;
|
|
255
|
+
qualityOk: boolean;
|
|
256
|
+
validationPassed: boolean;
|
|
257
|
+
qualityIssues: string[];
|
|
258
|
+
changedPaths: string[];
|
|
259
|
+
}): boolean {
|
|
260
|
+
if (opts.executor !== "openai_codex") return false;
|
|
261
|
+
if (opts.policyMode !== "default") return false;
|
|
262
|
+
if (!opts.qualityOk || !opts.validationPassed) return false;
|
|
263
|
+
if (opts.qualityIssues.length > 0 || opts.changedPaths.length === 0) return false;
|
|
264
|
+
return /\b(openai_codex|codex(?: exec)?)\b[^\r\n]*\btimed out\b/i.test(opts.executorText);
|
|
265
|
+
}
|
|
266
|
+
|
|
235
267
|
export function workerAttemptRolloutScore(params: {
|
|
236
268
|
executorElapsedMs: number;
|
|
237
269
|
qualityElapsedMs: number;
|
|
@@ -879,6 +911,10 @@ function parseJsonObjectLoose(text: string): Record<string, unknown> | null {
|
|
|
879
911
|
}
|
|
880
912
|
|
|
881
913
|
const COMMIT_MSG_MAX_DIFF_CHARS = 120_000;
|
|
914
|
+
const COMMIT_MSG_LLM_MAX_CHANGED_PATHS = 20;
|
|
915
|
+
const COMMIT_MSG_GENERATOR_DEFAULT_TIMEOUT_MS = 15_000;
|
|
916
|
+
const COMMIT_MSG_GENERATOR_MIN_TIMEOUT_MS = 3_000;
|
|
917
|
+
const COMMIT_MSG_GENERATOR_MAX_TIMEOUT_MS = 30_000;
|
|
882
918
|
|
|
883
919
|
const SHELL_CONTROL_TOKENS = new Set(["&&", "||", ";", "|"]);
|
|
884
920
|
|
|
@@ -2096,6 +2132,18 @@ function classifyBrowserValidationFailureKindFromText(text: string): BrowserVali
|
|
|
2096
2132
|
return "unknown";
|
|
2097
2133
|
}
|
|
2098
2134
|
|
|
2135
|
+
export function shouldRetryBrowserValidationRunOnce(run: ValidationExecutionResult): boolean {
|
|
2136
|
+
if (run.ok || !isLongRunningBrowserValidationCommand(run.command)) return false;
|
|
2137
|
+
const combined = stripAnsiControlSequences([run.stderr, run.stdout].filter(Boolean).join("\n"));
|
|
2138
|
+
const digest = extractValidationFailureDigest(run);
|
|
2139
|
+
const failureKind = classifyBrowserValidationFailureKindFromText(`${digest}\n${combined}`);
|
|
2140
|
+
if (failureKind === "runtime" || failureKind === "network") return true;
|
|
2141
|
+
if (failureKind === "startup") return true;
|
|
2142
|
+
return /\b(Route\/startup smoke failure|startup smoke failure|home route startup)\b/i.test(
|
|
2143
|
+
`${digest}\n${combined}`,
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2099
2147
|
function extractBrowserValidationStage(text: string): string | null {
|
|
2100
2148
|
const patterns = [
|
|
2101
2149
|
/\bBrowser validation failed during\s+([^:.\r\n|]+?)\s+stage\b/i,
|
|
@@ -3662,12 +3710,34 @@ async function runDeterministicQualityGate(
|
|
|
3662
3710
|
continue;
|
|
3663
3711
|
}
|
|
3664
3712
|
onLog?.("stdout", `[ValidationGate] Running "${command}"`);
|
|
3665
|
-
|
|
3713
|
+
let run = await runValidationCommand(
|
|
3666
3714
|
repo,
|
|
3667
3715
|
command,
|
|
3668
3716
|
resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
|
|
3669
3717
|
outputPolicy,
|
|
3670
3718
|
);
|
|
3719
|
+
const firstDigest = run.ok ? "" : extractValidationFailureDigest(run);
|
|
3720
|
+
if (shouldRetryBrowserValidationRunOnce(run)) {
|
|
3721
|
+
onLog?.(
|
|
3722
|
+
"stderr",
|
|
3723
|
+
`[ValidationGate] Retrying browser validation once after retryable startup/runtime failure: ${command}${firstDigest ? ` - ${firstDigest}` : ""}`,
|
|
3724
|
+
);
|
|
3725
|
+
const retryRun = await runValidationCommand(
|
|
3726
|
+
repo,
|
|
3727
|
+
command,
|
|
3728
|
+
resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
|
|
3729
|
+
outputPolicy,
|
|
3730
|
+
);
|
|
3731
|
+
if (!retryRun.ok && firstDigest) {
|
|
3732
|
+
retryRun.stderr = [
|
|
3733
|
+
`Previous browser validation attempt failed before retry: ${firstDigest}`,
|
|
3734
|
+
retryRun.stderr,
|
|
3735
|
+
]
|
|
3736
|
+
.filter(Boolean)
|
|
3737
|
+
.join("\n");
|
|
3738
|
+
}
|
|
3739
|
+
run = retryRun;
|
|
3740
|
+
}
|
|
3671
3741
|
validationRuns.push(run);
|
|
3672
3742
|
const digest = run.ok ? "" : extractValidationFailureDigest(run);
|
|
3673
3743
|
const runSummary = `[ValidationGate] ${run.ok ? "Passed" : "Failed"} (${run.elapsedMs}ms, exit ${run.exitCode}): ${command}${digest ? ` - ${digest}` : ""}`;
|
|
@@ -4768,17 +4838,19 @@ export async function createJobCommit(
|
|
|
4768
4838
|
...toNonEmptyStringArray(jobPlanning?.requiredValidationSteps),
|
|
4769
4839
|
...loadRequiredValidationStepsFromVision(repo),
|
|
4770
4840
|
];
|
|
4771
|
-
const llmCommitMsg =
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4841
|
+
const llmCommitMsg = shouldUseLlmCommitMessageForStagedDiff({ changedPaths, diff })
|
|
4842
|
+
? await generateCommitMessageFromDiff(
|
|
4843
|
+
diff,
|
|
4844
|
+
{
|
|
4845
|
+
instruction: String(job.params?.instruction ?? ""),
|
|
4846
|
+
type: normalizeCommitType(job.kind, job.params),
|
|
4847
|
+
area: inferCommitArea(job.kind, job.params, changedPaths),
|
|
4848
|
+
validationSteps: jobValidationSteps,
|
|
4849
|
+
},
|
|
4850
|
+
repo,
|
|
4851
|
+
runtimeConfig,
|
|
4852
|
+
).catch(() => null)
|
|
4853
|
+
: null;
|
|
4782
4854
|
if (!llmCommitMsg) {
|
|
4783
4855
|
console.warn(
|
|
4784
4856
|
`[WorkerPals] Commit message generator unavailable for job ${job.id}; using deterministic fallback.`,
|
|
@@ -5745,17 +5817,19 @@ async function createMergeConflictJobCommit(
|
|
|
5745
5817
|
...toNonEmptyStringArray(jobPlanning?.requiredValidationSteps),
|
|
5746
5818
|
...loadRequiredValidationStepsFromVision(repo),
|
|
5747
5819
|
];
|
|
5748
|
-
const llmCommitMsg =
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5820
|
+
const llmCommitMsg = shouldUseLlmCommitMessageForStagedDiff({ changedPaths, diff })
|
|
5821
|
+
? await generateCommitMessageFromDiff(
|
|
5822
|
+
diff,
|
|
5823
|
+
{
|
|
5824
|
+
instruction: String(job.params?.instruction ?? ""),
|
|
5825
|
+
type: normalizeCommitType(job.kind, job.params),
|
|
5826
|
+
area: inferCommitArea(job.kind, job.params, changedPaths),
|
|
5827
|
+
validationSteps: jobValidationSteps,
|
|
5828
|
+
},
|
|
5829
|
+
repo,
|
|
5830
|
+
runtimeConfig,
|
|
5831
|
+
).catch(() => null)
|
|
5832
|
+
: null;
|
|
5759
5833
|
if (!llmCommitMsg) {
|
|
5760
5834
|
console.warn(
|
|
5761
5835
|
`[WorkerPals] Commit message generator unavailable for merge-conflict job ${job.id}; using deterministic fallback.`,
|
|
@@ -6206,6 +6280,38 @@ async function generateCommitMessageFromDiff(
|
|
|
6206
6280
|
return generateCommitMessageFromDiffViaHttp(prompt, opts, runtimeConfig);
|
|
6207
6281
|
}
|
|
6208
6282
|
|
|
6283
|
+
export function resolveCommitMessageGeneratorTimeoutMs(
|
|
6284
|
+
runtimeConfig: WorkerpalsRuntimeConfig = DEFAULT_CONFIG,
|
|
6285
|
+
): number {
|
|
6286
|
+
const workerpalsConfig = runtimeConfig.workerpals as Record<string, unknown>;
|
|
6287
|
+
const llmConfig =
|
|
6288
|
+
workerpalsConfig.llm && typeof workerpalsConfig.llm === "object"
|
|
6289
|
+
? (workerpalsConfig.llm as Record<string, unknown>)
|
|
6290
|
+
: {};
|
|
6291
|
+
const configuredRaw =
|
|
6292
|
+
workerpalsConfig.commitMessageTimeoutMs ??
|
|
6293
|
+
workerpalsConfig.commit_message_timeout_ms ??
|
|
6294
|
+
llmConfig.commitMessageTimeoutMs ??
|
|
6295
|
+
llmConfig.commit_message_timeout_ms ??
|
|
6296
|
+
Bun.env.WORKERPALS_COMMIT_MESSAGE_TIMEOUT_MS;
|
|
6297
|
+
const configured = Number(configuredRaw);
|
|
6298
|
+
const value = Number.isFinite(configured)
|
|
6299
|
+
? configured
|
|
6300
|
+
: COMMIT_MSG_GENERATOR_DEFAULT_TIMEOUT_MS;
|
|
6301
|
+
return Math.max(
|
|
6302
|
+
COMMIT_MSG_GENERATOR_MIN_TIMEOUT_MS,
|
|
6303
|
+
Math.min(COMMIT_MSG_GENERATOR_MAX_TIMEOUT_MS, Math.floor(value)),
|
|
6304
|
+
);
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
export function shouldUseLlmCommitMessageForStagedDiff(params: {
|
|
6308
|
+
changedPaths: string[];
|
|
6309
|
+
diff: string;
|
|
6310
|
+
}): boolean {
|
|
6311
|
+
if (!String(params.diff ?? "").trim()) return false;
|
|
6312
|
+
return params.changedPaths.length <= COMMIT_MSG_LLM_MAX_CHANGED_PATHS;
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6209
6315
|
type CommitMessagePrompt = {
|
|
6210
6316
|
systemPrompt: string;
|
|
6211
6317
|
userMessage: string;
|
|
@@ -6244,11 +6350,7 @@ async function generateCommitMessageFromDiffViaCodex(
|
|
|
6244
6350
|
if (!model) return null;
|
|
6245
6351
|
const codexPrefix = await resolveCodexCommandPrefix(repo, runtimeConfig.workerpals.llm.codexBin);
|
|
6246
6352
|
if (!codexPrefix) return null;
|
|
6247
|
-
const timeoutMs = (
|
|
6248
|
-
const value = Number(runtimeConfig.workerpals.llm.codexTimeoutMs);
|
|
6249
|
-
if (!Number.isFinite(value)) return 120_000;
|
|
6250
|
-
return Math.max(10_000, Math.min(600_000, Math.floor(value)));
|
|
6251
|
-
})();
|
|
6353
|
+
const timeoutMs = resolveCommitMessageGeneratorTimeoutMs(runtimeConfig);
|
|
6252
6354
|
const reasoningEffort = normalizeCodexReasoningEffort(
|
|
6253
6355
|
runtimeConfig.workerpals.llm.reasoningEffort,
|
|
6254
6356
|
model,
|
|
@@ -6338,7 +6440,7 @@ async function generateCommitMessageFromDiffViaHttp(
|
|
|
6338
6440
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
6339
6441
|
|
|
6340
6442
|
const controller = new AbortController();
|
|
6341
|
-
const timer = setTimeout(() => controller.abort(),
|
|
6443
|
+
const timer = setTimeout(() => controller.abort(), resolveCommitMessageGeneratorTimeoutMs(runtimeConfig));
|
|
6342
6444
|
try {
|
|
6343
6445
|
const response = await fetch(endpoint, {
|
|
6344
6446
|
method: "POST",
|
|
@@ -6952,6 +7054,7 @@ async function runCodexCriticReview(
|
|
|
6952
7054
|
validationChars: validationSummary.length,
|
|
6953
7055
|
};
|
|
6954
7056
|
};
|
|
7057
|
+
type CodexCriticPayload = Awaited<ReturnType<typeof buildCriticInstruction>>;
|
|
6955
7058
|
|
|
6956
7059
|
const tmpOutputPath = `/tmp/pushpals-critic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`;
|
|
6957
7060
|
const buildCmd = () => {
|
|
@@ -6980,13 +7083,17 @@ async function runCodexCriticReview(
|
|
|
6980
7083
|
const runAttempt = async (
|
|
6981
7084
|
attempt: number,
|
|
6982
7085
|
compact: boolean,
|
|
6983
|
-
|
|
7086
|
+
payloadOverride?: CodexCriticPayload,
|
|
7087
|
+
): Promise<
|
|
7088
|
+
| { status: "timeout"; payload: CodexCriticPayload }
|
|
7089
|
+
| { status: "done"; review: CriticReview | null; payload: CodexCriticPayload }
|
|
7090
|
+
> => {
|
|
6984
7091
|
try {
|
|
6985
7092
|
unlinkSync(tmpOutputPath);
|
|
6986
7093
|
} catch {
|
|
6987
7094
|
/* ignore stale/missing critic output */
|
|
6988
7095
|
}
|
|
6989
|
-
const payload = await buildCriticInstruction(compact);
|
|
7096
|
+
const payload = payloadOverride ?? (await buildCriticInstruction(compact));
|
|
6990
7097
|
const startedAt = Date.now();
|
|
6991
7098
|
onLog?.(
|
|
6992
7099
|
"stdout",
|
|
@@ -7014,7 +7121,7 @@ async function runCodexCriticReview(
|
|
|
7014
7121
|
clearTimeout(timer);
|
|
7015
7122
|
|
|
7016
7123
|
if (timedOut) {
|
|
7017
|
-
return { status: "timeout" };
|
|
7124
|
+
return { status: "timeout", payload };
|
|
7018
7125
|
}
|
|
7019
7126
|
if (exitCode !== 0) {
|
|
7020
7127
|
const stderrText = await new Response(proc.stderr).text();
|
|
@@ -7022,7 +7129,7 @@ async function runCodexCriticReview(
|
|
|
7022
7129
|
"stderr",
|
|
7023
7130
|
`[CriticGate] Codex exited ${exitCode}: ${toSingleLine(stderrText, 220)}`,
|
|
7024
7131
|
);
|
|
7025
|
-
return { status: "done", review: null };
|
|
7132
|
+
return { status: "done", review: null, payload };
|
|
7026
7133
|
}
|
|
7027
7134
|
|
|
7028
7135
|
let lastMessage = "";
|
|
@@ -7039,7 +7146,7 @@ async function runCodexCriticReview(
|
|
|
7039
7146
|
|
|
7040
7147
|
if (!lastMessage) {
|
|
7041
7148
|
onLog?.("stderr", "[CriticGate] Codex: no output message captured; skipping.");
|
|
7042
|
-
return { status: "done", review: null };
|
|
7149
|
+
return { status: "done", review: null, payload };
|
|
7043
7150
|
}
|
|
7044
7151
|
|
|
7045
7152
|
const reviewObj = parseJsonObjectLoose(lastMessage);
|
|
@@ -7048,7 +7155,7 @@ async function runCodexCriticReview(
|
|
|
7048
7155
|
"stderr",
|
|
7049
7156
|
`[CriticGate] Codex returned non-JSON: ${toSingleLine(lastMessage, 220)}`,
|
|
7050
7157
|
);
|
|
7051
|
-
return { status: "done", review: null };
|
|
7158
|
+
return { status: "done", review: null, payload };
|
|
7052
7159
|
}
|
|
7053
7160
|
|
|
7054
7161
|
const scoreRaw = Number(reviewObj.score);
|
|
@@ -7068,6 +7175,7 @@ async function runCodexCriticReview(
|
|
|
7068
7175
|
);
|
|
7069
7176
|
return {
|
|
7070
7177
|
status: "done",
|
|
7178
|
+
payload,
|
|
7071
7179
|
review: {
|
|
7072
7180
|
score,
|
|
7073
7181
|
findings,
|
|
@@ -7081,11 +7189,36 @@ async function runCodexCriticReview(
|
|
|
7081
7189
|
try {
|
|
7082
7190
|
let attempt = await runAttempt(1, false);
|
|
7083
7191
|
if (attempt.status === "timeout" && timeoutBehavior === "retry_once") {
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7192
|
+
const compactPayload = await buildCriticInstruction(true);
|
|
7193
|
+
const validationPassed =
|
|
7194
|
+
quality.validationRuns.length > 0 && quality.validationRuns.every((run) => run.ok);
|
|
7195
|
+
if (
|
|
7196
|
+
shouldRetryCriticTimeoutWithCompact({
|
|
7197
|
+
timeoutBehavior,
|
|
7198
|
+
qualityOk: quality.ok,
|
|
7199
|
+
validationPassed,
|
|
7200
|
+
initialPromptChars: attempt.payload.promptChars,
|
|
7201
|
+
compactPromptChars: compactPayload.promptChars,
|
|
7202
|
+
})
|
|
7203
|
+
) {
|
|
7204
|
+
onLog?.(
|
|
7205
|
+
"stderr",
|
|
7206
|
+
`[CriticGate] Codex timed out after ${qualityCriticTimeoutMs}ms; retrying once with compact critic input.`,
|
|
7207
|
+
);
|
|
7208
|
+
attempt = await runAttempt(2, true, compactPayload);
|
|
7209
|
+
} else {
|
|
7210
|
+
const reductionPct = Math.max(
|
|
7211
|
+
0,
|
|
7212
|
+
Math.round(
|
|
7213
|
+
(1 - compactPayload.promptChars / Math.max(1, attempt.payload.promptChars)) * 100,
|
|
7214
|
+
),
|
|
7215
|
+
);
|
|
7216
|
+
onLog?.(
|
|
7217
|
+
"stderr",
|
|
7218
|
+
`[CriticGate] Codex timed out after ${qualityCriticTimeoutMs}ms; compact critic input only reduced prompt by ${reductionPct}% after clean validation; skipping retry.`,
|
|
7219
|
+
);
|
|
7220
|
+
return null;
|
|
7221
|
+
}
|
|
7089
7222
|
}
|
|
7090
7223
|
if (attempt.status === "timeout") {
|
|
7091
7224
|
if (timeoutBehavior === "block") {
|
|
@@ -7316,6 +7449,42 @@ export async function executeJob(
|
|
|
7316
7449
|
);
|
|
7317
7450
|
continue;
|
|
7318
7451
|
}
|
|
7452
|
+
if (sequencer === "rebase" && !resume.resumed) {
|
|
7453
|
+
mergeConflictPass += 1;
|
|
7454
|
+
const budget = qualityRevisionBudgetDecision({
|
|
7455
|
+
jobElapsedMs: Date.now() - attemptStartedAt,
|
|
7456
|
+
executionBudgetMs,
|
|
7457
|
+
});
|
|
7458
|
+
if (mergeConflictPass < MAX_MERGE_CONFLICT_RESOLUTION_PASSES && budget.shouldStart) {
|
|
7459
|
+
const retryDetail =
|
|
7460
|
+
resume.detail ??
|
|
7461
|
+
"the previous resolver pass returned before the prepared rebase completed";
|
|
7462
|
+
const previousHint = String(attemptParams.qualityRevisionHint ?? "").trim();
|
|
7463
|
+
attemptParams.qualityRevisionHint = [
|
|
7464
|
+
previousHint,
|
|
7465
|
+
[
|
|
7466
|
+
`Merge-conflict resolver pass ${mergeConflictPass} left the rebase unfinished: ${retryDetail}.`,
|
|
7467
|
+
"Focus only on completing the active rebase. Inspect unresolved files with `git diff --name-only --diff-filter=U`, remove remaining conflict markers, stage resolved files, and run `git -c core.editor=true rebase --continue` until no rebase remains.",
|
|
7468
|
+
"Do not broaden the patch or run full validation before the rebase is complete.",
|
|
7469
|
+
].join("\n"),
|
|
7470
|
+
]
|
|
7471
|
+
.filter(Boolean)
|
|
7472
|
+
.join("\n\n");
|
|
7473
|
+
onLog?.(
|
|
7474
|
+
"stdout",
|
|
7475
|
+
`[MergeConflict] ${retryDetail}; rerunning resolver pass ${
|
|
7476
|
+
mergeConflictPass + 1
|
|
7477
|
+
} with focused rebase-completion guidance.`,
|
|
7478
|
+
);
|
|
7479
|
+
continue;
|
|
7480
|
+
}
|
|
7481
|
+
if (!budget.shouldStart) {
|
|
7482
|
+
onLog?.(
|
|
7483
|
+
"stderr",
|
|
7484
|
+
`[MergeConflict] Not rerunning unfinished rebase resolver: remaining execution budget is ${budget.remainingBudgetMs}ms (< ${budget.minimumRevisionBudgetMs}ms).`,
|
|
7485
|
+
);
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7319
7488
|
const detail =
|
|
7320
7489
|
`Merge-conflict job returned with git ${sequencer} still in progress. ` +
|
|
7321
7490
|
`Finish the ${sequencer} before returning control to WorkerPals.`;
|
|
@@ -7440,14 +7609,30 @@ export async function executeJob(
|
|
|
7440
7609
|
blocker: null,
|
|
7441
7610
|
}
|
|
7442
7611
|
: quality;
|
|
7612
|
+
const validationPassed =
|
|
7613
|
+
quality.validationRuns.length > 0 && quality.validationRuns.every((run) => run.ok);
|
|
7614
|
+
const skipCriticAfterExecutorTimeout = shouldSkipCriticAfterExecutorTimeout({
|
|
7615
|
+
executor,
|
|
7616
|
+
policyMode: qualityGatePolicy.mode,
|
|
7617
|
+
executorText,
|
|
7618
|
+
qualityOk: quality.ok,
|
|
7619
|
+
validationPassed,
|
|
7620
|
+
qualityIssues: qualityForCritic.issues,
|
|
7621
|
+
changedPaths: quality.changedPaths,
|
|
7622
|
+
});
|
|
7443
7623
|
const critic =
|
|
7444
|
-
quality.skipped || !qualityGatePolicy.criticGateEnabled
|
|
7624
|
+
quality.skipped || !qualityGatePolicy.criticGateEnabled || skipCriticAfterExecutorTimeout
|
|
7445
7625
|
? null
|
|
7446
7626
|
: executor === "openai_codex"
|
|
7447
7627
|
? await runCodexCriticReview(repo, attemptParams, qualityForCritic, runtimeConfig, onLog)
|
|
7448
7628
|
: await runTaskCriticReview(repo, attemptParams, qualityForCritic, runtimeConfig, onLog);
|
|
7449
7629
|
if (!qualityGatePolicy.criticGateEnabled) {
|
|
7450
7630
|
onLog?.("stdout", "[CriticGate] Disabled by workerpals.quality_critic_gate_enabled=false.");
|
|
7631
|
+
} else if (skipCriticAfterExecutorTimeout) {
|
|
7632
|
+
onLog?.(
|
|
7633
|
+
"stdout",
|
|
7634
|
+
"[CriticGate] Skipping Codex critic after primary Codex executor timeout because deterministic quality and validation are clean.",
|
|
7635
|
+
);
|
|
7451
7636
|
}
|
|
7452
7637
|
const rolloutScore = workerAttemptRolloutScore({
|
|
7453
7638
|
executorElapsedMs,
|
|
@@ -197,6 +197,15 @@ function buildPlannerGuidance(
|
|
|
197
197
|
lines.push(
|
|
198
198
|
"- Use direct commands only while resolving this rebase. Prefer `git diff -- <path>`, `git add <path>`, and `git -c core.editor=true rebase --continue` instead of `/bin/bash -lc`, `sh -lc`, `awk`, or chained shell snippets.",
|
|
199
199
|
);
|
|
200
|
+
lines.push(
|
|
201
|
+
"- Primary success condition: finish the git rebase and leave no active rebase/merge/cherry-pick state. Do not spend budget polishing, broadening, or refactoring tests beyond what is required to remove conflict markers and keep both sides' intended behavior.",
|
|
202
|
+
);
|
|
203
|
+
lines.push(
|
|
204
|
+
"- Rebase convergence rule: after resolving each conflicted file, run `git diff --name-only --diff-filter=U`. If no unresolved paths remain, stage the resolved files and continue the rebase immediately before doing broader validation.",
|
|
205
|
+
);
|
|
206
|
+
lines.push(
|
|
207
|
+
"- Budget rule: if conflict resolution is running long, choose the smallest side-preserving resolution, stage it, and continue the rebase. A clean rebased branch with focused follow-up validation is better than a richer partial patch left mid-rebase.",
|
|
208
|
+
);
|
|
200
209
|
lines.push(
|
|
201
210
|
"- After editing, run `git add <files>` and `git -c core.editor=true rebase --continue` until the rebase completes.",
|
|
202
211
|
);
|
|
@@ -47,6 +47,7 @@ import { DockerExecutionExhaustedError, DockerExecutor } from "./docker_executor
|
|
|
47
47
|
import { forceDeleteWorktreePath } from "./common/worktree_cleanup.js";
|
|
48
48
|
import { WorkerServerTransport, type WorkerHeartbeatPayload } from "./common/server_transport.js";
|
|
49
49
|
import { DEFAULT_DOCKER_TIMEOUT_MS, parseDockerTimeoutMs } from "./timeout_policy.js";
|
|
50
|
+
import { resolveFreshWorktreeBaseRef } from "./worktree_base_ref.js";
|
|
50
51
|
|
|
51
52
|
type CommitRef = {
|
|
52
53
|
branch: string;
|
|
@@ -314,6 +315,8 @@ async function reportWorkerLlmUsage(
|
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
function integrationBranchName(): string {
|
|
318
|
+
const configuredIntegrationBranch = CONFIG.sourceControlManager.mainBranch.trim();
|
|
319
|
+
if (configuredIntegrationBranch) return configuredIntegrationBranch;
|
|
317
320
|
const configuredBaseRef = CONFIG.workerpals.baseRef.trim();
|
|
318
321
|
if (!configuredBaseRef) return "main_agents";
|
|
319
322
|
return configuredBaseRef.replace(/^origin\//, "").trim() || "main_agents";
|
|
@@ -665,33 +668,17 @@ async function runJob(
|
|
|
665
668
|
}
|
|
666
669
|
|
|
667
670
|
async function resolveWorktreeBaseRef(repo: string, requestedRef: string): Promise<string> {
|
|
668
|
-
|
|
669
|
-
const integrationRemoteRef = `origin/${integrationBranch}`;
|
|
670
|
-
const candidates = new Set<string>([
|
|
671
|
+
return resolveFreshWorktreeBaseRef({
|
|
671
672
|
requestedRef,
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
`[WorkerPals] Could not refresh ${requestedRef}; continuing with local refs (${fetchResult.stderr || fetchResult.stdout})`,
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
candidates.add(branch);
|
|
685
|
-
} else if (requestedRef !== "HEAD") {
|
|
686
|
-
candidates.add(`origin/${requestedRef}`);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
for (const ref of candidates) {
|
|
690
|
-
const parsed = await git(repo, ["rev-parse", "--verify", "--quiet", ref]);
|
|
691
|
-
if (parsed.ok) return ref;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return "HEAD";
|
|
673
|
+
integrationBranch: integrationBranchName(),
|
|
674
|
+
sourceBaseBranch: CONFIG.sourceControlManager.baseBranch,
|
|
675
|
+
git: (args) => git(repo, args),
|
|
676
|
+
log: (level, message) => {
|
|
677
|
+
const line = `[WorkerPals] ${message}`;
|
|
678
|
+
if (level === "warn") console.warn(line);
|
|
679
|
+
else console.log(line);
|
|
680
|
+
},
|
|
681
|
+
});
|
|
695
682
|
}
|
|
696
683
|
|
|
697
684
|
async function createIsolatedWorktree(
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
export type GitBaseRefCommandResult = {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
stdout?: string;
|
|
4
|
+
stderr?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type GitBaseRefCommand = (args: string[]) => Promise<GitBaseRefCommandResult>;
|
|
8
|
+
|
|
9
|
+
export type WorktreeBaseRefLogLevel = "info" | "warn";
|
|
10
|
+
|
|
11
|
+
export type ResolveFreshWorktreeBaseRefOptions = {
|
|
12
|
+
requestedRef: string;
|
|
13
|
+
integrationBranch: string;
|
|
14
|
+
sourceBaseBranch: string;
|
|
15
|
+
remote?: string;
|
|
16
|
+
git: GitBaseRefCommand;
|
|
17
|
+
log?: (level: WorktreeBaseRefLogLevel, message: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function normalizeBranchName(value: string): string {
|
|
21
|
+
return value
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^refs\/heads\//, "")
|
|
24
|
+
.replace(/^origin\//, "")
|
|
25
|
+
.replace(/^\/+|\/+$/g, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeRequestedRef(value: string): string {
|
|
29
|
+
return value.trim() || "HEAD";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function remoteRef(remote: string, branch: string): string {
|
|
33
|
+
return `${remote}/${branch}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isIntegrationBaseRequest(ref: string, integrationBranch: string, remote: string): boolean {
|
|
37
|
+
const normalized = normalizeRequestedRef(ref);
|
|
38
|
+
const branch = normalizeBranchName(normalized);
|
|
39
|
+
return (
|
|
40
|
+
branch === integrationBranch ||
|
|
41
|
+
normalized === remoteRef(remote, integrationBranch) ||
|
|
42
|
+
normalized === `refs/remotes/${remote}/${integrationBranch}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchRemoteBranch(
|
|
47
|
+
git: GitBaseRefCommand,
|
|
48
|
+
remote: string,
|
|
49
|
+
branch: string,
|
|
50
|
+
): Promise<GitBaseRefCommandResult> {
|
|
51
|
+
if (!remote || !branch || branch === "HEAD") return { ok: true };
|
|
52
|
+
return git(["fetch", remote, branch, "--quiet"]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function refExists(git: GitBaseRefCommand, ref: string): Promise<boolean> {
|
|
56
|
+
const result = await git(["rev-parse", "--verify", "--quiet", ref]);
|
|
57
|
+
return result.ok;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function isAncestor(
|
|
61
|
+
git: GitBaseRefCommand,
|
|
62
|
+
ancestorRef: string,
|
|
63
|
+
descendantRef: string,
|
|
64
|
+
): Promise<boolean> {
|
|
65
|
+
const result = await git(["merge-base", "--is-ancestor", ancestorRef, descendantRef]);
|
|
66
|
+
return result.ok;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function resolveExistingWorktreeBaseRef(
|
|
70
|
+
options: Omit<ResolveFreshWorktreeBaseRefOptions, "sourceBaseBranch" | "log">,
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const remote = (options.remote ?? "origin").trim() || "origin";
|
|
73
|
+
const requestedRef = normalizeRequestedRef(options.requestedRef);
|
|
74
|
+
const integrationBranch = normalizeBranchName(options.integrationBranch) || "main_agents";
|
|
75
|
+
const integrationRemoteRef = remoteRef(remote, integrationBranch);
|
|
76
|
+
const candidates = new Set<string>([
|
|
77
|
+
requestedRef,
|
|
78
|
+
integrationRemoteRef,
|
|
79
|
+
integrationBranch,
|
|
80
|
+
"HEAD",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
if (requestedRef.startsWith(`${remote}/`)) {
|
|
84
|
+
const branch = requestedRef.slice(`${remote}/`.length);
|
|
85
|
+
await fetchRemoteBranch(options.git, remote, branch);
|
|
86
|
+
candidates.add(branch);
|
|
87
|
+
} else if (requestedRef !== "HEAD") {
|
|
88
|
+
candidates.add(remoteRef(remote, requestedRef));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const ref of candidates) {
|
|
92
|
+
if (await refExists(options.git, ref)) return ref;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return "HEAD";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function resolveFreshWorktreeBaseRef(
|
|
99
|
+
options: ResolveFreshWorktreeBaseRefOptions,
|
|
100
|
+
): Promise<string> {
|
|
101
|
+
const remote = (options.remote ?? "origin").trim() || "origin";
|
|
102
|
+
const requestedRef = normalizeRequestedRef(options.requestedRef);
|
|
103
|
+
const integrationBranch = normalizeBranchName(options.integrationBranch) || "main_agents";
|
|
104
|
+
const sourceBaseBranch = normalizeBranchName(options.sourceBaseBranch) || "main";
|
|
105
|
+
const resolvedRef = await resolveExistingWorktreeBaseRef({
|
|
106
|
+
requestedRef,
|
|
107
|
+
integrationBranch,
|
|
108
|
+
remote,
|
|
109
|
+
git: options.git,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!sourceBaseBranch ||
|
|
114
|
+
sourceBaseBranch === integrationBranch ||
|
|
115
|
+
!isIntegrationBaseRequest(requestedRef, integrationBranch, remote)
|
|
116
|
+
) {
|
|
117
|
+
return resolvedRef;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const sourceBaseRef = remoteRef(remote, sourceBaseBranch);
|
|
121
|
+
const fetchSource = await fetchRemoteBranch(options.git, remote, sourceBaseBranch);
|
|
122
|
+
if (!fetchSource.ok) {
|
|
123
|
+
options.log?.(
|
|
124
|
+
"warn",
|
|
125
|
+
`Could not refresh ${sourceBaseRef}; checking local ref before keeping ${resolvedRef} (${fetchSource.stderr || fetchSource.stdout || "fetch failed"}).`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!(await refExists(options.git, sourceBaseRef))) return resolvedRef;
|
|
130
|
+
|
|
131
|
+
if (resolvedRef !== "HEAD" && (await refExists(options.git, resolvedRef))) {
|
|
132
|
+
const sourceAlreadyIncluded = await isAncestor(options.git, sourceBaseRef, resolvedRef);
|
|
133
|
+
if (sourceAlreadyIncluded) return resolvedRef;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
options.log?.(
|
|
137
|
+
"warn",
|
|
138
|
+
`Worktree base ${resolvedRef} does not contain ${sourceBaseRef}; using ${sourceBaseRef} for new WorkerPal jobs to avoid stale integration-branch checkouts.`,
|
|
139
|
+
);
|
|
140
|
+
return sourceBaseRef;
|
|
141
|
+
}
|
|
@@ -128,6 +128,7 @@ export interface PushPalsConfig {
|
|
|
128
128
|
enabled: boolean;
|
|
129
129
|
killSwitchEnabled: boolean;
|
|
130
130
|
tickIntervalMs: number;
|
|
131
|
+
startupGraceMs: number;
|
|
131
132
|
heartbeatLogMs: number;
|
|
132
133
|
visionContextMaxChars: number;
|
|
133
134
|
ideationBudgetMs: number;
|
|
@@ -1671,6 +1672,14 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
|
|
|
1671
1672
|
120_000,
|
|
1672
1673
|
),
|
|
1673
1674
|
),
|
|
1675
|
+
startupGraceMs: Math.max(
|
|
1676
|
+
0,
|
|
1677
|
+
asInt(
|
|
1678
|
+
parseIntEnv("REMOTEBUDDY_AUTONOMY_STARTUP_GRACE_MS") ??
|
|
1679
|
+
remoteAutonomyNode.startup_grace_ms,
|
|
1680
|
+
120_000,
|
|
1681
|
+
),
|
|
1682
|
+
),
|
|
1674
1683
|
heartbeatLogMs: Math.max(
|
|
1675
1684
|
1_000,
|
|
1676
1685
|
asInt(
|