@pushpalsdev/cli 1.1.11 → 1.1.13

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.
@@ -136,6 +136,29 @@ export function resolveGenericPythonExecutorTimeoutMs(params: {
136
136
  return configuredTimeoutMs;
137
137
  }
138
138
 
139
+ function toSnakeConfigKey(key: string): string {
140
+ return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
141
+ }
142
+
143
+ function formatGenericPythonExecutorTimeoutDetail(
144
+ config: GenericPythonExecutorConfig,
145
+ configuredTimeoutMs: number,
146
+ executionBudgetMs: number | null,
147
+ timeoutMs: number,
148
+ ): string {
149
+ const configPath = `workerpals.${toSnakeConfigKey(config.timeoutConfigKey)}`;
150
+ if (executionBudgetMs == null) {
151
+ return `${configPath}=${configuredTimeoutMs}ms`;
152
+ }
153
+ if (config.capTimeoutToExecutionBudget === false) {
154
+ return `${configPath}=${configuredTimeoutMs}ms; planning executionBudgetMs=${executionBudgetMs}ms ignored by backend opt-out`;
155
+ }
156
+ if (timeoutMs < configuredTimeoutMs) {
157
+ return `${configPath}=${configuredTimeoutMs}ms capped by planning executionBudgetMs=${executionBudgetMs}ms`;
158
+ }
159
+ return `${configPath}=${configuredTimeoutMs}ms within planning executionBudgetMs=${executionBudgetMs}ms`;
160
+ }
161
+
139
162
  export function createGenericPythonExecutor(
140
163
  config: GenericPythonExecutorConfig,
141
164
  ): BackendTaskExecutor {
@@ -172,6 +195,12 @@ export function createGenericPythonExecutor(
172
195
  executionBudgetMs,
173
196
  capTimeoutToExecutionBudget: config.capTimeoutToExecutionBudget,
174
197
  });
198
+ const timeoutDetail = formatGenericPythonExecutorTimeoutDetail(
199
+ config,
200
+ configuredTimeoutMs,
201
+ executionBudgetMs,
202
+ timeoutMs,
203
+ );
175
204
  const payloadBase64 = Buffer.from(
176
205
  JSON.stringify({
177
206
  kind,
@@ -184,7 +213,7 @@ export function createGenericPythonExecutor(
184
213
 
185
214
  onLog?.(
186
215
  "stdout",
187
- `[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms)`,
216
+ `[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail})`,
188
217
  );
189
218
 
190
219
  try {
@@ -207,6 +236,7 @@ export function createGenericPythonExecutor(
207
236
  });
208
237
 
209
238
  let timedOut = false;
239
+ let hardKillTimer: ReturnType<typeof setTimeout> | null = null;
210
240
  const timeoutTimer = setTimeout(() => {
211
241
  timedOut = true;
212
242
  onLog?.(
@@ -214,6 +244,13 @@ export function createGenericPythonExecutor(
214
244
  `[${backendLabel}Executor] Timeout reached after ${timeoutMs}ms; terminating process.`,
215
245
  );
216
246
  proc.kill();
247
+ hardKillTimer = setTimeout(() => {
248
+ onLog?.(
249
+ "stdout",
250
+ `[${backendLabel}Executor] Process did not exit after graceful timeout termination; forcing kill.`,
251
+ );
252
+ proc.kill("SIGKILL");
253
+ }, 5_000);
217
254
  }, timeoutMs);
218
255
 
219
256
  const progressIntervalMs = 15_000;
@@ -246,6 +283,7 @@ export function createGenericPythonExecutor(
246
283
  ]);
247
284
 
248
285
  clearTimeout(timeoutTimer);
286
+ if (hardKillTimer) clearTimeout(hardKillTimer);
249
287
  clearInterval(progressTimer);
250
288
 
251
289
  const stdout = rawStdout ?? "";
@@ -303,7 +303,7 @@ export function resolveDockerJobTimeoutMs(
303
303
  if (!hasBrowserValidationCommand(job)) return baseTimeoutMs;
304
304
 
305
305
  const planning = maybeRecord(job.params.planning);
306
- const executionBudgetMs = readPositiveNumber(planning?.executionBudgetMs) ?? 1_800_000;
306
+ const executionBudgetMs = readPositiveNumber(planning?.executionBudgetMs) ?? 1_200_000;
307
307
  const finalizationBudgetMs = readPositiveNumber(planning?.finalizationBudgetMs) ?? 120_000;
308
308
  const attempts = BROWSER_VALIDATION_JOB_REPAIR_ATTEMPTS + 1; // initial attempt plus repairs
309
309
  const estimatedTimeoutMs =
@@ -1145,6 +1145,21 @@ export function isLongRunningBrowserValidationCommand(command: string): boolean
1145
1145
  );
1146
1146
  }
1147
1147
 
1148
+ export function isParallelSafeFastValidationCommand(repo: string, command: string): boolean {
1149
+ if (isLongRunningBrowserValidationCommand(command)) return false;
1150
+ if (shouldEnsurePlaywrightBrowserRuntime(repo, command)) return false;
1151
+ const tokens = tokenizeValidationCommandArgv(command);
1152
+ if (!tokens || tokens.length === 0) return false;
1153
+ const lower = tokens.map((token) => token.toLowerCase());
1154
+ if (lower[0] !== "bun") return false;
1155
+ if (lower[1] === "test") return true;
1156
+ if (lower[1] === "x" && lower[2] === "tsc") return true;
1157
+ if (lower[1] === "run" && ["lint", "typecheck", "test", "test:unit"].includes(lower[2] ?? "")) {
1158
+ return true;
1159
+ }
1160
+ return false;
1161
+ }
1162
+
1148
1163
  function readPackageJson(repo: string): {
1149
1164
  scripts?: Record<string, unknown>;
1150
1165
  dependencies?: Record<string, unknown>;
@@ -3116,7 +3131,71 @@ async function runDeterministicQualityGate(
3116
3131
  );
3117
3132
  }
3118
3133
  const playwrightBrowserRuntimeReadyTargets = new Set<string>();
3119
- for (const command of commandsToRun) {
3134
+ for (let commandIndex = 0; commandIndex < commandsToRun.length; ) {
3135
+ const parallelBatch: string[] = [];
3136
+ while (
3137
+ commandIndex + parallelBatch.length < commandsToRun.length &&
3138
+ parallelBatch.length < 3
3139
+ ) {
3140
+ const candidate = commandsToRun[commandIndex + parallelBatch.length];
3141
+ if (!isParallelSafeFastValidationCommand(repo, candidate)) break;
3142
+ parallelBatch.push(candidate);
3143
+ }
3144
+ if (parallelBatch.length > 1) {
3145
+ onLog?.(
3146
+ "stdout",
3147
+ `[ValidationGate] Running fast validation batch in parallel: ${parallelBatch.join(" | ")}`,
3148
+ );
3149
+ const batchRuns = await Promise.all(
3150
+ parallelBatch.map(async (command) => {
3151
+ const commandMissingTools = requirementsForValidationCommand(
3152
+ toolchainPlan,
3153
+ command,
3154
+ ).filter((requirement) =>
3155
+ missingToolRequirements.some((missing) => missing.tool === requirement.tool),
3156
+ );
3157
+ if (commandMissingTools.length > 0) {
3158
+ const stderr = `Validation skipped before execution because required tool(s) are missing: ${formatMissingToolRequirements(
3159
+ commandMissingTools,
3160
+ )}.`;
3161
+ return {
3162
+ run: {
3163
+ step: command,
3164
+ command,
3165
+ ok: false,
3166
+ exitCode: 127,
3167
+ stdout: "",
3168
+ stderr,
3169
+ elapsedMs: 1,
3170
+ } satisfies ValidationExecutionResult,
3171
+ stream: "stderr" as const,
3172
+ summary: `[ValidationGate] Validation skipped (missing toolchain): ${command}`,
3173
+ };
3174
+ }
3175
+ const run = await runValidationCommand(
3176
+ repo,
3177
+ command,
3178
+ resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
3179
+ outputPolicy,
3180
+ );
3181
+ const digest = run.ok ? "" : extractValidationFailureDigest(run);
3182
+ return {
3183
+ run,
3184
+ stream: (run.ok ? "stdout" : "stderr") as "stdout" | "stderr",
3185
+ summary: `[ValidationGate] ${run.ok ? "Passed" : "Failed"} (${run.elapsedMs}ms, exit ${run.exitCode}): ${command}${digest ? ` - ${digest}` : ""}`,
3186
+ };
3187
+ }),
3188
+ );
3189
+ for (const { run, stream, summary } of batchRuns) {
3190
+ validationRuns.push(run);
3191
+ onLog?.(stream, summary);
3192
+ }
3193
+ commandIndex += parallelBatch.length;
3194
+ continue;
3195
+ }
3196
+
3197
+ const command = commandsToRun[commandIndex];
3198
+ commandIndex += 1;
3120
3199
  const commandMissingTools = requirementsForValidationCommand(toolchainPlan, command).filter(
3121
3200
  (requirement) =>
3122
3201
  missingToolRequirements.some((missing) => missing.tool === requirement.tool),
@@ -6665,6 +6744,7 @@ export async function executeJob(
6665
6744
  const previousValidationFailureDigests = new Map<string, string>();
6666
6745
  const failureJobFamily = buildTaskFailureJobFamily(normalizedParams);
6667
6746
  while (revisionAttempt <= qualityRevisionLoopMax) {
6747
+ const attemptStartedAt = Date.now();
6668
6748
  const attemptParams: Record<string, unknown> = { ...normalizedParams };
6669
6749
  if (revisionHint) {
6670
6750
  attemptParams.qualityRevisionHint = revisionHint;
@@ -6683,6 +6763,7 @@ export async function executeJob(
6683
6763
  }
6684
6764
  let result: Awaited<ReturnType<typeof runExecutor>> | null = null;
6685
6765
  let mergeConflictPass = 0;
6766
+ let executorElapsedMs = 0;
6686
6767
  while (true) {
6687
6768
  const currentResult = await runExecutor(
6688
6769
  kind,
@@ -6751,6 +6832,7 @@ export async function executeJob(
6751
6832
  exitCode: 4,
6752
6833
  };
6753
6834
  }
6835
+ executorElapsedMs = Date.now() - attemptStartedAt;
6754
6836
 
6755
6837
  const preQualityStatus = await git(repo, ["status", "--porcelain"]);
6756
6838
  const preQualityChangedPaths = preQualityStatus.ok
@@ -6799,6 +6881,7 @@ export async function executeJob(
6799
6881
  };
6800
6882
  }
6801
6883
 
6884
+ const qualityStartedAt = Date.now();
6802
6885
  const quality = await runDeterministicQualityGate(
6803
6886
  repo,
6804
6887
  attemptParams,
@@ -6810,6 +6893,15 @@ export async function executeJob(
6810
6893
  revisionAttempt,
6811
6894
  },
6812
6895
  );
6896
+ const qualityElapsedMs = Date.now() - qualityStartedAt;
6897
+ const validationCommandElapsedMs = quality.validationRuns.reduce(
6898
+ (total, run) => total + Math.max(0, Number(run.elapsedMs) || 0),
6899
+ 0,
6900
+ );
6901
+ onLog?.(
6902
+ "stdout",
6903
+ `[JobRunner] Performance summary: attempt=${revisionAttempt}, executor=${executorElapsedMs}ms, quality=${qualityElapsedMs}ms, validation_commands=${quality.validationRuns.length}, validation_command_time=${validationCommandElapsedMs}ms, changed_files=${quality.changedPaths.length}`,
6904
+ );
6813
6905
  let browserRepairPacket = buildBrowserValidationRepairPacket(
6814
6906
  quality.validationRuns,
6815
6907
  previousValidationFailureDigests,
@@ -51,7 +51,7 @@ workerpal_heartbeat_ms = 0
51
51
  workerpal_labels = []
52
52
  execution_budget_interactive_ms = 600000
53
53
  execution_budget_normal_ms = 1500000
54
- execution_budget_background_ms = 1800000
54
+ execution_budget_background_ms = 1200000
55
55
  finalization_budget_ms = 120000
56
56
  crash_restart_enabled = true
57
57
  crash_restart_max_restarts = 3
@@ -515,7 +515,8 @@
515
515
  "stream": { "type": "string", "enum": ["stdout", "stderr"] },
516
516
  "seq": { "type": "integer", "minimum": 1 },
517
517
  "line": { "type": "string" },
518
- "ts": { "type": "string" }
518
+ "ts": { "type": "string" },
519
+ "phase": { "type": ["string", "null"] }
519
520
  },
520
521
  "additionalProperties": false
521
522
  }
@@ -125,6 +125,7 @@ export interface EventTypePayloadMap {
125
125
  seq: number;
126
126
  line: string;
127
127
  ts?: string;
128
+ phase?: string | null;
128
129
  };
129
130
 
130
131
  /** System heartbeat / status beacon */
@@ -1619,7 +1619,7 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
1619
1619
  asInt(
1620
1620
  parseIntEnv("REMOTEBUDDY_EXECUTION_BUDGET_BACKGROUND_MS") ??
1621
1621
  remoteNode.execution_budget_background_ms,
1622
- 1_800_000,
1622
+ 1_200_000,
1623
1623
  ),
1624
1624
  ),
1625
1625
  finalizationBudgetMs: Math.max(
@@ -515,7 +515,8 @@
515
515
  "stream": { "type": "string", "enum": ["stdout", "stderr"] },
516
516
  "seq": { "type": "integer", "minimum": 1 },
517
517
  "line": { "type": "string" },
518
- "ts": { "type": "string" }
518
+ "ts": { "type": "string" },
519
+ "phase": { "type": ["string", "null"] }
519
520
  },
520
521
  "additionalProperties": false
521
522
  }