@yeaft/webchat-agent 1.0.411 → 1.0.412

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.
@@ -1 +1 @@
1
- {"version":"1.0.411"}
1
+ {"version":"1.0.412"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.411",
3
+ "version": "1.0.412",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/engine.js CHANGED
@@ -22,7 +22,7 @@ import { promises as fsp } from 'fs';
22
22
  import { join, resolve as resolvePath } from 'path';
23
23
  import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
24
24
  import { getRuntimePlatformInfo } from './runtime-platform.js';
25
- import { LLMContextError, LLMAbortError, LLMAuthError, LLMRateLimitError, LLMServerError, LLMStreamIdleTimeoutError } from './llm/adapter.js';
25
+ import { LLMContextError, LLMAbortError, LLMAuthError, LLMPolicyError, LLMRateLimitError, LLMServerError, LLMStreamIdleTimeoutError } from './llm/adapter.js';
26
26
  import { runMemoryPreflow, buildRelevantScopes, memoryScopeLabel } from './sessions/pre-flow.js';
27
27
  import {
28
28
  readProjectDoc,
@@ -155,6 +155,8 @@ const RETRY_DEFAULTS = Object.freeze({
155
155
 
156
156
  const RETRY_CONTINUATION_PROMPT =
157
157
  'Continue from the exact point where the previous response stopped. Do not repeat text already produced.';
158
+ const POLICY_RECOVERY_PROMPT =
159
+ 'Continue this authorized code review, but describe security findings abstractly. Do not repeat credential-like or exploit payloads, secrets, tokens, or step-by-step misuse instructions. Preserve the technical conclusion, evidence location, severity, and remediation.';
158
160
 
159
161
  // Accept legacy namespaced commands and Claude Code-style bare skill commands.
160
162
  // Project-tier skills are shown as /<skill-name>; /yeaft-skills:<name> and
@@ -2791,6 +2793,7 @@ export class Engine {
2791
2793
  let retryPolicy = resolveRetryPolicy(this.#config);
2792
2794
  let consecutiveRetryableErrors = 0;
2793
2795
  let consecutiveForbiddenErrors = 0;
2796
+ let contentPolicyRecoveryAttempts = 0;
2794
2797
 
2795
2798
  while (true) {
2796
2799
  turnNumber++;
@@ -3442,17 +3445,42 @@ export class Engine {
3442
3445
  errorName: err?.name || null,
3443
3446
  statusCode: err?.statusCode ?? null,
3444
3447
  retryable: err instanceof LLMRateLimitError || err instanceof LLMServerError,
3448
+ reasonCode: err?.reasonCode || null,
3445
3449
  message: String(err?.message || '').slice(0, 200),
3446
3450
  },
3447
3451
  });
3448
3452
 
3449
3453
  const earlyIsRateLimit = err instanceof LLMRateLimitError;
3450
3454
  const earlyIsTransient = err instanceof LLMServerError;
3455
+ const earlyIsContentPolicy = err instanceof LLMPolicyError;
3451
3456
  // A completed tool_call has already crossed the streaming boundary to
3452
3457
  // the caller. Replaying that request would publish a duplicate call and
3453
3458
  // leave ambiguous execution ownership, so only pre-tool failures are
3454
3459
  // eligible for transparent retry or model fallback.
3455
3460
  const canReplayProviderRequest = toolCalls.length === 0;
3461
+ if (earlyIsContentPolicy && canReplayProviderRequest && contentPolicyRecoveryAttempts === 0) {
3462
+ contentPolicyRecoveryAttempts = 1;
3463
+ endAttemptTrace('llm_retry');
3464
+ if (responseText) prepareRetryContinuation();
3465
+ retryLifecycle.pendingContinuation = {
3466
+ role: 'user',
3467
+ content: POLICY_RECOVERY_PROMPT,
3468
+ userAuthored: false,
3469
+ };
3470
+ yield {
3471
+ type: 'llm_retry',
3472
+ attempt: 1,
3473
+ maxRetries: 1,
3474
+ delayMs: 0,
3475
+ reason: 'content_policy_recovery',
3476
+ recoveryMode: 'continue',
3477
+ errorName: err.name,
3478
+ statusCode: err.statusCode ?? 422,
3479
+ message: 'Provider content-safety rejection; retrying once with sensitive examples abstracted.',
3480
+ };
3481
+ yield { type: 'turn_end', turnNumber, stopReason: 'llm_retry', threadId };
3482
+ continue;
3483
+ }
3456
3484
  const earlyIsTemporaryForbidden = err instanceof LLMAuthError
3457
3485
  && err.statusCode === 403
3458
3486
  && err.temporary === true;
@@ -3640,9 +3668,25 @@ export class Engine {
3640
3668
  && consecutiveRetryableErrors >= retryPolicy.maxRetries;
3641
3669
  errorEvent.retryAttempts = consecutiveRetryableErrors;
3642
3670
  errorEvent.maxRetries = retryPolicy.maxRetries;
3671
+ } else if (err instanceof LLMPolicyError) {
3672
+ errorEvent.reason = 'content_policy_denied';
3673
+ errorEvent.retryExhausted = contentPolicyRecoveryAttempts >= 1;
3674
+ errorEvent.retryAttempts = contentPolicyRecoveryAttempts;
3675
+ errorEvent.maxRetries = 1;
3643
3676
  }
3644
3677
  yield errorEvent;
3645
- yield { type: 'turn_end', turnNumber, stopReason: 'error', threadId, terminal: true };
3678
+ yield {
3679
+ type: 'turn_end',
3680
+ turnNumber,
3681
+ stopReason: 'error',
3682
+ threadId,
3683
+ terminal: true,
3684
+ detail: {
3685
+ errorName: err?.name || 'Error',
3686
+ statusCode: err?.statusCode ?? null,
3687
+ reason: errorEvent.reason || err?.reasonCode || null,
3688
+ },
3689
+ };
3646
3690
  break;
3647
3691
  }
3648
3692
 
@@ -140,6 +140,29 @@ export function classifyAuthError(statusCode, responseBody = '', details = {}) {
140
140
  });
141
141
  }
142
142
 
143
+ /** Provider content-safety rejection (commonly 422) — eligible for one sanitized recovery. */
144
+ export class LLMPolicyError extends Error {
145
+ constructor(_providerMessage, statusCode = 422, details = {}) {
146
+ super('The LLM provider blocked this request under its content-safety policy. Continue and avoid repeating sensitive payloads or credential-like examples.');
147
+ this.name = 'LLMPolicyError';
148
+ this.statusCode = statusCode;
149
+ this.reasonCode = 'content_policy_denied';
150
+ this.provider = details.provider || null;
151
+ this.model = details.model || null;
152
+ }
153
+ }
154
+
155
+ const CONTENT_POLICY_RE = /(?:content (?:was )?flagged|content[-_ ]?safety|cybersecurity risk|safety policy|safety system|content[_ -]?filter|policy[_ -]?violation)/i;
156
+
157
+ export function classifyPolicyError(statusCode, responseBody = '', details = {}) {
158
+ const status = Number(statusCode) || 0;
159
+ const signals = providerErrorSignals(responseBody);
160
+ if (status !== 422 || (!CONTENT_POLICY_RE.test(signals.code) && !CONTENT_POLICY_RE.test(signals.message))) {
161
+ return null;
162
+ }
163
+ return new LLMPolicyError(signals.message, status, details);
164
+ }
165
+
143
166
  /** Context too long error (413 or API-specific) — need compaction. */
144
167
  export class LLMContextError extends Error {
145
168
  constructor(message) {
@@ -13,6 +13,7 @@ import {
13
13
  retryAfterFromResponse,
14
14
  LLMAuthError,
15
15
  classifyAuthError,
16
+ classifyPolicyError,
16
17
  LLMContextError,
17
18
  LLMServerError,
18
19
  LLMAbortError,
@@ -230,6 +231,8 @@ export class AnthropicAdapter extends LLMAdapter {
230
231
  const retryAfter = retryAfterFromResponse(response);
231
232
  return new LLMRateLimitError(`Anthropic overloaded (${authHint}): ${body}`, status, retryAfter);
232
233
  }
234
+ const policyError = classifyPolicyError(status, body);
235
+ if (policyError) return policyError;
233
236
  if (body.includes('prompt is too long') || body.includes('max_tokens')) {
234
237
  return new LLMContextError(`Anthropic context error (${authHint}): ${body}`);
235
238
  }
@@ -30,6 +30,7 @@ import {
30
30
  LLMRateLimitError,
31
31
  LLMAuthError,
32
32
  classifyAuthError,
33
+ classifyPolicyError,
33
34
  LLMContextError,
34
35
  LLMServerError,
35
36
  LLMAbortError,
@@ -219,6 +220,8 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
219
220
  const retryAfter = retryAfterFromResponse(response);
220
221
  return new LLMRateLimitError(`Overloaded: ${body}`, status, retryAfter);
221
222
  }
223
+ const policyError = classifyPolicyError(status, body);
224
+ if (policyError) return policyError;
222
225
  if (status === 413 || body.includes('context_length_exceeded') || body.includes('maximum context length')) {
223
226
  return new LLMContextError(`Context too long: ${body}`);
224
227
  }
@@ -4464,9 +4464,13 @@ function handleEngineEvent(event, hctx) {
4464
4464
  const errMsg = event.error?.message || 'Unknown error';
4465
4465
  const retryAttempts = Number.isFinite(event.retryAttempts) ? event.retryAttempts : 0;
4466
4466
  const exhaustedIdle = event.reason === 'stream_idle_timeout' && event.retryExhausted;
4467
- const visibleErrMsg = exhaustedIdle && retryAttempts > 0
4468
- ? `${errMsg} after ${retryAttempts} fresh request retries`
4469
- : errMsg;
4467
+ const contentPolicyDenied = event.reason === 'content_policy_denied'
4468
+ || event.error?.reasonCode === 'content_policy_denied';
4469
+ const visibleErrMsg = contentPolicyDenied
4470
+ ? 'Provider blocked this request for content-safety reasons after one safe recovery attempt. Continue and ask the VP to avoid repeating sensitive payloads, credentials, tokens, or exploit samples.'
4471
+ : exhaustedIdle && retryAttempts > 0
4472
+ ? `${errMsg} after ${retryAttempts} fresh request retries`
4473
+ : errMsg;
4470
4474
  hctx.lastEngineErrorDetail = {
4471
4475
  message: visibleErrMsg,
4472
4476
  ...(event.reason ? { reason: event.reason } : {}),