@workermill/agent 0.7.18 → 0.7.20

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.
@@ -90,7 +90,7 @@ export declare function parseCriticResponse(text: string): CriticResult;
90
90
  * Run the critic via Claude CLI (lightweight — no tools, just reasoning).
91
91
  * Returns the raw text output.
92
92
  */
93
- export declare function runCriticCli(claudePath: string, model: string, prompt: string, env: Record<string, string | undefined>): Promise<string>;
93
+ export declare function runCriticCli(claudePath: string, model: string, prompt: string, env: Record<string, string | undefined>, taskId?: string): Promise<string>;
94
94
  /**
95
95
  * Format critic feedback for appending to the planner prompt on re-run.
96
96
  */
@@ -100,5 +100,5 @@ export declare function formatCriticFeedback(critic: CriticResult): string;
100
100
  * Routes to Claude CLI (Anthropic) or HTTP API (other providers).
101
101
  * Returns the critic result, or null if critic fails (non-blocking).
102
102
  */
103
- export declare function runCriticValidation(claudePath: string, model: string, prd: string, plan: ExecutionPlan, env: Record<string, string | undefined>, taskLabel: string, provider?: AIProvider, providerApiKey?: string): Promise<CriticResult | null>;
103
+ export declare function runCriticValidation(claudePath: string, model: string, prd: string, plan: ExecutionPlan, env: Record<string, string | undefined>, taskLabel: string, provider?: AIProvider, providerApiKey?: string, taskId?: string): Promise<CriticResult | null>;
104
104
  export { AUTO_APPROVAL_THRESHOLD };
@@ -12,6 +12,7 @@
12
12
  import { spawn } from "child_process";
13
13
  import chalk from "chalk";
14
14
  import { generateText } from "./providers.js";
15
+ import { api } from "./api.js";
15
16
  // ============================================================================
16
17
  // CONSTANTS
17
18
  // ============================================================================
@@ -277,7 +278,7 @@ export function parseCriticResponse(text) {
277
278
  * Run the critic via Claude CLI (lightweight — no tools, just reasoning).
278
279
  * Returns the raw text output.
279
280
  */
280
- export function runCriticCli(claudePath, model, prompt, env) {
281
+ export function runCriticCli(claudePath, model, prompt, env, taskId) {
281
282
  return new Promise((resolve, reject) => {
282
283
  const proc = spawn(claudePath, [
283
284
  "--print",
@@ -294,7 +295,21 @@ export function runCriticCli(claudePath, model, prompt, env) {
294
295
  let stdout = "";
295
296
  let stderr = "";
296
297
  proc.stdout.on("data", (data) => {
297
- stdout += data.toString();
298
+ const chunk = data.toString();
299
+ stdout += chunk;
300
+ // Stream critic reasoning to dashboard in real-time
301
+ const lines = chunk.split("\n").filter((l) => l.trim());
302
+ for (const line of lines) {
303
+ const trimmed = line.trim().length > 200
304
+ ? line.trim().substring(0, 200) + "…"
305
+ : line.trim();
306
+ if (trimmed) {
307
+ if (taskId) {
308
+ postLog(taskId, `${PREFIX} [critic] ${trimmed}`, "output");
309
+ }
310
+ console.log(`${ts()} ${chalk.dim("🔍")} ${chalk.dim(trimmed)}`);
311
+ }
312
+ }
298
313
  });
299
314
  proc.stderr.on("data", (data) => {
300
315
  stderr += data.toString();
@@ -358,23 +373,44 @@ export function formatCriticFeedback(critic) {
358
373
  lines.push("**You MUST address ALL feedback above.** Each story must target at most 5 files.", "Stories MUST NOT overlap on targetFiles. Generate a revised plan.");
359
374
  return lines.join("\n");
360
375
  }
376
+ /** Consistent prefix matching planner dashboard format */
377
+ const PREFIX = "[🗺️ planning_agent 🤖]";
361
378
  /** Timestamp prefix for console logs */
362
379
  function ts() {
363
380
  return chalk.dim(new Date().toLocaleTimeString());
364
381
  }
382
+ /**
383
+ * Post a log message to the cloud dashboard for real-time visibility.
384
+ */
385
+ async function postLog(taskId, message, type = "system", severity = "info") {
386
+ try {
387
+ await api.post("/api/control-center/logs", {
388
+ taskId,
389
+ type,
390
+ message,
391
+ severity,
392
+ });
393
+ }
394
+ catch {
395
+ // Fire and forget — don't block critic on log failures
396
+ }
397
+ }
365
398
  /**
366
399
  * Run critic validation on a parsed plan.
367
400
  * Routes to Claude CLI (Anthropic) or HTTP API (other providers).
368
401
  * Returns the critic result, or null if critic fails (non-blocking).
369
402
  */
370
- export async function runCriticValidation(claudePath, model, prd, plan, env, taskLabel, provider, providerApiKey) {
403
+ export async function runCriticValidation(claudePath, model, prd, plan, env, taskLabel, provider, providerApiKey, taskId) {
371
404
  const criticPrompt = buildCriticPrompt(prd, plan);
372
405
  const effectiveProvider = provider || "anthropic";
373
406
  console.log(`${ts()} ${taskLabel} ${chalk.dim(`Running critic validation (${effectiveProvider})...`)}`);
407
+ if (taskId) {
408
+ postLog(taskId, `${PREFIX} Running critic validation (${effectiveProvider})...`);
409
+ }
374
410
  try {
375
411
  let rawCriticOutput;
376
412
  if (effectiveProvider === "anthropic") {
377
- rawCriticOutput = await runCriticCli(claudePath, model, criticPrompt, env);
413
+ rawCriticOutput = await runCriticCli(claudePath, model, criticPrompt, env, taskId);
378
414
  }
379
415
  else {
380
416
  if (!providerApiKey) {
package/dist/planner.js CHANGED
@@ -70,20 +70,50 @@ const MAX_ITERATIONS = 3;
70
70
  function ts() {
71
71
  return chalk.dim(new Date().toLocaleTimeString());
72
72
  }
73
+ /**
74
+ * Log queue — sends entries sequentially instead of N concurrent POSTs.
75
+ * During planning, flushTextBuffer() can fire 15-30 postLog() calls in a burst.
76
+ * Without queuing, those concurrent POSTs saturate the API's DB connection pool
77
+ * (max 10), causing poll timeouts, transient 401s, and multi-second stalls.
78
+ */
79
+ const logQueue = [];
80
+ let logDrainPromise = null;
81
+ async function drainLogQueue() {
82
+ while (logQueue.length > 0) {
83
+ const entry = logQueue.shift();
84
+ try {
85
+ await api.post("/api/control-center/logs", entry, { timeout: 5_000 });
86
+ }
87
+ catch {
88
+ // Best-effort — drop on failure
89
+ }
90
+ }
91
+ }
73
92
  /**
74
93
  * Post a log message to the cloud dashboard for real-time visibility.
94
+ * Entries are queued and drained sequentially (max 1 in-flight POST).
75
95
  */
76
96
  async function postLog(taskId, message, type = "system", severity = "info") {
77
- try {
78
- await api.post("/api/control-center/logs", {
79
- taskId,
80
- type,
81
- message,
82
- severity,
97
+ if (logQueue.length >= 200)
98
+ logQueue.shift(); // drop oldest
99
+ logQueue.push({ taskId, message, type, severity });
100
+ if (!logDrainPromise) {
101
+ logDrainPromise = drainLogQueue().finally(() => {
102
+ logDrainPromise = null;
83
103
  });
84
104
  }
85
- catch {
86
- // Fire and forget — don't block planning on log failures
105
+ }
106
+ /**
107
+ * Flush remaining log entries (call before cleanup).
108
+ */
109
+ async function flushLogQueue() {
110
+ if (logDrainPromise)
111
+ await logDrainPromise;
112
+ if (logQueue.length > 0) {
113
+ logDrainPromise = drainLogQueue().finally(() => {
114
+ logDrainPromise = null;
115
+ });
116
+ await logDrainPromise;
87
117
  }
88
118
  }
89
119
  /**
@@ -238,6 +268,7 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime, disable
238
268
  if (block.type === "text" && block.text) {
239
269
  fullText += block.text;
240
270
  charsReceived += block.text.length;
271
+ textBuffer += block.text;
241
272
  if (!firstTextSeen) {
242
273
  firstTextSeen = true;
243
274
  if (toolCallCount > 0 && !milestoneSent.analyzing) {
@@ -263,6 +294,7 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime, disable
263
294
  else if (typeof content === "string" && content) {
264
295
  fullText += content;
265
296
  charsReceived += content.length;
297
+ textBuffer += content;
266
298
  }
267
299
  }
268
300
  else if (event.type === "content_block_delta" && event.delta?.text) {
@@ -919,7 +951,7 @@ export async function planTask(task, config, credentials) {
919
951
  console.log(`${ts()} ${taskLabel} Plan: ${chalk.bold(plan.stories.length)} stories (max ${maxStories})`);
920
952
  await postLog(task.id, `${PREFIX} Plan generated: ${plan.stories.length} stories (${formatElapsed(elapsed)}). Running critic validation...`);
921
953
  // 2d. Run critic validation
922
- const criticResult = await runCriticValidation(claudePath, cliModel, prd, plan, cleanEnv, taskLabel, provider, providerApiKey);
954
+ const criticResult = await runCriticValidation(claudePath, cliModel, prd, plan, cleanEnv, taskLabel, provider, providerApiKey, task.id);
923
955
  // Track best plan across iterations
924
956
  if (criticResult && criticResult.score > bestScore) {
925
957
  bestPlan = plan;
@@ -1036,6 +1068,8 @@ export async function planTask(task, config, credentials) {
1036
1068
  return false;
1037
1069
  }
1038
1070
  finally {
1071
+ // Drain any remaining log entries before cleanup
1072
+ await flushLogQueue();
1039
1073
  // Cleanup temp clone
1040
1074
  if (repoPath) {
1041
1075
  try {
package/dist/poller.js CHANGED
@@ -75,12 +75,17 @@ async function pollOnce(config) {
75
75
  }
76
76
  catch (error) {
77
77
  const err = error;
78
+ const busy = planningInProgress.size > 0 || getActiveCount() > 0 || managerInProgress.size > 0;
78
79
  if (err.response?.status === 401) {
79
- console.error(`${ts()} ${chalk.red("✗")} Authentication failed. Check your API key.`);
80
+ if (!busy) {
81
+ console.error(`${ts()} ${chalk.red("✗")} Authentication failed. Check your API key.`);
82
+ }
83
+ // Silent when busy — transient DB pool exhaustion on server
80
84
  }
81
- else {
82
- console.error(`${ts()} ${chalk.red("")} Poll error: ${err.message || String(error)}`);
85
+ else if (!busy) {
86
+ console.warn(`${ts()} ${chalk.yellow("")} Poll error: ${err.message || String(error)}`);
83
87
  }
88
+ // Silent when busy — expected during heavy planning/execution
84
89
  }
85
90
  }
86
91
  /**
@@ -181,7 +186,7 @@ async function handleQueuedTask(task, config) {
181
186
  }
182
187
  catch (err) {
183
188
  const taskLabel = chalk.cyan(task.id.slice(0, 8));
184
- console.error(`${ts()} ${chalk.red("")} Failed to report started for ${taskLabel}`);
189
+ console.warn(`${ts()} ${chalk.yellow("")} Failed to report started for ${taskLabel}`);
185
190
  }
186
191
  const taskLabel = chalk.cyan(task.id.slice(0, 8));
187
192
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
4
4
  "description": "WorkerMill Remote Agent - Run AI workers locally with your Claude Max subscription",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",