clementine-agent 1.18.19 → 1.18.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.
@@ -31,6 +31,8 @@ export declare function looksLikeContextThrashText(value: unknown): boolean;
31
31
  export declare function contextThrashRecoveryNotice(): string;
32
32
  export declare function buildContextThrashRecoveryPrompt(userRequest: string, priorFailureText?: string): string;
33
33
  export declare function looksLikeOneMillionContextError(value: unknown): boolean;
34
+ export declare function oneMillionContextRecoveryMessage(): string;
35
+ export declare function looksLikeProviderApiErrorResponse(value: unknown): boolean;
34
36
  export declare function looksLikeNoResponseRequested(value: unknown): boolean;
35
37
  /** Autonomous jobs use this sentinel to mean "completed, but do not notify the owner." */
36
38
  export declare function isAutonomousNothingOutput(response: string): boolean;
@@ -343,6 +343,15 @@ function resultInputTokens(result) {
343
343
  export function looksLikeOneMillionContextError(value) {
344
344
  return looksLikeClaudeOneMillionContextError(value);
345
345
  }
346
+ export function oneMillionContextRecoveryMessage() {
347
+ return "Claude rejected 1M context for this account. I've switched Clementine to persistent 200K recovery mode and reset the session. Restart Clementine once so every background worker starts with the same safe setting.";
348
+ }
349
+ export function looksLikeProviderApiErrorResponse(value) {
350
+ const text = String(value ?? '').trim();
351
+ return /^api error:/i.test(text)
352
+ || /^error:\s*api error:/i.test(text)
353
+ || looksLikeOneMillionContextError(text);
354
+ }
346
355
  export function looksLikeNoResponseRequested(value) {
347
356
  const text = String(value ?? '').trim();
348
357
  return /^no response requested\.?$/i.test(text);
@@ -2992,7 +3001,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2992
3001
  // Track exchange count, timestamp, and last exchange.
2993
3002
  // Never store API error responses — they poison session history and create
2994
3003
  // a self-reinforcing loop where every subsequent request replays the errors.
2995
- const isApiError = responseText.startsWith('Error:') && responseText.includes('API Error:');
3004
+ const isApiError = looksLikeProviderApiErrorResponse(responseText);
2996
3005
  if (key && !isApiError) {
2997
3006
  this.exchangeCounts.set(key, (this.exchangeCounts.get(key) ?? 0) + 1);
2998
3007
  this.sessionTimestamps.set(key, new Date());
@@ -3432,7 +3441,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3432
3441
  this.exchangeCounts.set(sessionKey, 0);
3433
3442
  this._compactedSessions.delete(sessionKey);
3434
3443
  }
3435
- responseText = responseText || ("Claude rejected 1M context for this account. I've switched Clementine to persistent 200K recovery mode and reset the session. Restart Clementine once so every background worker starts with the same safe setting.");
3444
+ responseText = responseText || (oneMillionContextRecoveryMessage());
3436
3445
  }
3437
3446
  else if (lower.includes('rate') && lower.includes('limit')) {
3438
3447
  hitRateLimit = true;
@@ -3485,7 +3494,19 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3485
3494
  else if ('result' in result && result.result) {
3486
3495
  // Success: use SDK result text if streaming didn't capture a substantive response
3487
3496
  const sdkResult = result.result;
3488
- if (looksLikeContextThrashText(sdkResult)) {
3497
+ if (looksLikeOneMillionContextError(sdkResult)) {
3498
+ logger.warn({ sessionKey }, '1M context error surfaced as SDK result text — forcing recovery');
3499
+ applyOneMillionContextRecovery();
3500
+ if (sessionKey) {
3501
+ this.sessions.delete(sessionKey);
3502
+ this.exchangeCounts.set(sessionKey, 0);
3503
+ this._compactedSessions.delete(sessionKey);
3504
+ }
3505
+ responseText = oneMillionContextRecoveryMessage();
3506
+ if (onText)
3507
+ await onText(responseText);
3508
+ }
3509
+ else if (looksLikeContextThrashText(sdkResult)) {
3489
3510
  logger.warn({ sessionKey }, 'Autocompact thrashing surfaced as SDK result text — rotating session');
3490
3511
  preRotationSnapshot = {
3491
3512
  toolCalls: stallGuard?.getToolCalls() ?? [],
@@ -3563,7 +3584,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3563
3584
  this.exchangeCounts.set(sessionKey, 0);
3564
3585
  this._compactedSessions.delete(sessionKey);
3565
3586
  }
3566
- responseText = responseText || ("Claude rejected 1M context for this account. I've switched Clementine to persistent 200K recovery mode and reset the session. Restart Clementine once so every background worker starts with the same safe setting.");
3587
+ responseText = responseText || (oneMillionContextRecoveryMessage());
3567
3588
  }
3568
3589
  else if (errStr.includes('rate') && (errStr.includes('limit') || errStr.includes('rate_limit'))) {
3569
3590
  hitRateLimit = true;
@@ -39,6 +39,7 @@ const HEARTBEAT_WORK_QUEUE_FILE = path.join(BASE_DIR, 'heartbeat', 'work-queue.j
39
39
  const MEMORY_DB_PATH = path.join(VAULT_DIR, '.memory.db');
40
40
  const PROJECTS_META_FILE = path.join(BASE_DIR, 'projects.json');
41
41
  const DASHBOARD_PID_FILE = path.join(BASE_DIR, '.dashboard.pid');
42
+ const INTERACTIVE_FAILURE_LOG = path.join(BASE_DIR, 'self-improve', 'interactive-failures.jsonl');
42
43
  /**
43
44
  * Kill all existing dashboard processes before starting a new one.
44
45
  * Uses both the PID file and a process sweep to catch orphans.
@@ -5679,6 +5680,43 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
5679
5680
  process.env[key] = normalized;
5680
5681
  return { ok: true, value: normalized };
5681
5682
  }
5683
+ function readRecentDashboardChatFailures(limit = 5) {
5684
+ try {
5685
+ if (!existsSync(INTERACTIVE_FAILURE_LOG))
5686
+ return [];
5687
+ const lines = readFileSync(INTERACTIVE_FAILURE_LOG, 'utf-8')
5688
+ .trim()
5689
+ .split('\n')
5690
+ .filter(Boolean)
5691
+ .slice(-80)
5692
+ .reverse();
5693
+ const out = [];
5694
+ for (const line of lines) {
5695
+ try {
5696
+ const item = JSON.parse(line);
5697
+ const error = String(item.error ?? '');
5698
+ const stage = String(item.stage ?? '');
5699
+ const haystack = `${stage} ${error}`;
5700
+ if (!/1m|context|budget|credit|api error|rate.?limit/i.test(haystack))
5701
+ continue;
5702
+ out.push({
5703
+ createdAt: String(item.createdAt ?? ''),
5704
+ stage,
5705
+ sessionKey: String(item.sessionKey ?? ''),
5706
+ textPreview: String(item.textPreview ?? '').slice(0, 220),
5707
+ error: error.slice(0, 500),
5708
+ });
5709
+ if (out.length >= limit)
5710
+ break;
5711
+ }
5712
+ catch { /* skip malformed lines */ }
5713
+ }
5714
+ return out;
5715
+ }
5716
+ catch {
5717
+ return [];
5718
+ }
5719
+ }
5682
5720
  const ASSISTANT_PREF_OPTIONS = {
5683
5721
  proactivity: ['quiet', 'balanced', 'proactive', 'operator'],
5684
5722
  responseStyle: ['concise', 'balanced', 'detailed'],
@@ -5797,6 +5835,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
5797
5835
  legacyMode,
5798
5836
  },
5799
5837
  findings,
5838
+ recentFailures: readRecentDashboardChatFailures(),
5800
5839
  counts: doctor.counts,
5801
5840
  });
5802
5841
  }
@@ -20083,6 +20122,7 @@ async function refreshBudgetHealth() {
20083
20122
  var modeClass = mode === 'off' ? 'badge-green' : mode === 'on' ? 'badge-yellow' : 'badge-blue';
20084
20123
  var rows = d.budgets || [];
20085
20124
  var findings = d.findings || [];
20125
+ var recentFailures = d.recentFailures || [];
20086
20126
  var html = '<div class="card">'
20087
20127
  + '<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:12px">'
20088
20128
  + '<div style="display:flex;align-items:center;gap:8px"><span>Spend Guards &amp; Context Health</span><span class="badge ' + modeClass + '" style="font-size:10px">1M ' + esc(mode) + '</span></div>'
@@ -20131,6 +20171,22 @@ async function refreshBudgetHealth() {
20131
20171
  + '<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">Safe Recovery lowers autonomous spend and disables 1M context for accounts seeing credit or entitlement errors.</div>'
20132
20172
  + '<div style="font-size:11px;color:var(--text-muted);margin-top:6px">Restart the daemon after changing budgets or context mode.</div>'
20133
20173
  + '</div></div>';
20174
+ if (recentFailures.length) {
20175
+ html += '<div style="border-top:1px solid var(--border);padding-top:10px;margin-bottom:10px">'
20176
+ + '<div style="font-weight:600;font-size:13px;margin-bottom:6px">Recent chat failures</div>';
20177
+ for (var rf = 0; rf < recentFailures.length; rf++) {
20178
+ var fail = recentFailures[rf] || {};
20179
+ html += '<div style="padding:8px 0;border-bottom:1px solid rgba(127,127,127,0.12)">'
20180
+ + '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
20181
+ + '<span class="badge badge-yellow" style="font-size:10px">' + esc(fail.stage || 'failure') + '</span>'
20182
+ + '<span style="font-size:11px;color:var(--text-muted)">' + esc(fail.createdAt || '') + '</span>'
20183
+ + '</div>'
20184
+ + '<div style="font-size:12px;color:var(--text-secondary);margin-top:4px">' + esc(fail.error || '') + '</div>'
20185
+ + (fail.textPreview ? '<div style="font-size:11px;color:var(--text-muted);margin-top:3px">Prompt: ' + esc(fail.textPreview) + '</div>' : '')
20186
+ + '</div>';
20187
+ }
20188
+ html += '</div>';
20189
+ }
20134
20190
  if (findings.length) {
20135
20191
  html += '<div style="border-top:1px solid var(--border);padding-top:10px">'
20136
20192
  + '<div style="font-weight:600;font-size:13px;margin-bottom:6px">Potential causes</div>';
@@ -11,7 +11,7 @@ import { TeamRouter } from '../agent/team-router.js';
11
11
  import { TeamBus } from '../agent/team-bus.js';
12
12
  import type { NotificationDispatcher } from './notifications.js';
13
13
  import { type ProactiveNotificationInput } from './notification-context.js';
14
- export type ChatErrorKind = 'rate_limit' | 'context_overflow' | 'auth' | 'billing' | 'transient' | 'unknown';
14
+ export type ChatErrorKind = 'rate_limit' | 'one_million_context' | 'context_overflow' | 'auth' | 'billing' | 'transient' | 'unknown';
15
15
  export declare function classifyChatError(err: unknown): ChatErrorKind;
16
16
  /** Detect auth-like errors in response text that the SDK returned as "successful" results. */
17
17
  export declare function looksLikeAuthError(text: string): boolean;
@@ -7,10 +7,10 @@
7
7
  import path from 'node:path';
8
8
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
9
  import pino from 'pino';
10
- import { buildContextThrashRecoveryPrompt, contextThrashRecoveryNotice, isAutonomousNothingOutput, looksLikeContextThrashText, PersonalAssistant, } from '../agent/assistant.js';
10
+ import { buildContextThrashRecoveryPrompt, contextThrashRecoveryNotice, isAutonomousNothingOutput, looksLikeContextThrashText, looksLikeProviderApiErrorResponse, oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
11
11
  import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
12
12
  import { SelfImproveLoop } from '../agent/self-improve.js';
13
- import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED } from '../config.js';
13
+ import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
14
14
  import { scanner } from '../security/scanner.js';
15
15
  import { lanes } from './lanes.js';
16
16
  import { AgentManager } from '../agent/agent-manager.js';
@@ -42,7 +42,9 @@ export function classifyChatError(err) {
42
42
  return 'billing';
43
43
  if (/rate.?limit|\b429\b|too many requests|quota.?exceeded/i.test(msg))
44
44
  return 'rate_limit';
45
- if (looksLikeContextThrashText(msg) || /extra usage.*1m context|1m context.*extra usage|context-1m|context.?length|token.?limit|maximum.?context|prompt.?too.?long/i.test(msg))
45
+ if (looksLikeClaudeOneMillionContextError(msg))
46
+ return 'one_million_context';
47
+ if (looksLikeContextThrashText(msg) || /context.?length|token.?limit|maximum.?context|prompt.?too.?long/i.test(msg))
46
48
  return 'context_overflow';
47
49
  if (/\b401\b|\b403\b|auth|forbidden|invalid.?api.?key|permission|does not have access|please run \/login/i.test(msg))
48
50
  return 'auth';
@@ -1500,6 +1502,19 @@ export class Gateway {
1500
1502
  }, 'chat:latency');
1501
1503
  // Re-baseline integrity checksums after chat (auto-memory may write to vault)
1502
1504
  scanner.refreshIntegrity();
1505
+ if (response && looksLikeClaudeOneMillionContextError(response)) {
1506
+ logger.warn({ sessionKey, responsePreview: response.slice(0, 200) }, '1M context error returned as assistant text — forcing recovery');
1507
+ this.recordInteractiveFailure(sessionKey, text, response, 'one_million_context_result_text', { effectiveSessionKey });
1508
+ applyOneMillionContextRecovery();
1509
+ this.clearSession(effectiveSessionKey);
1510
+ return oneMillionContextRecoveryMessage();
1511
+ }
1512
+ if (response && looksLikeProviderApiErrorResponse(response)) {
1513
+ logger.warn({ sessionKey, responsePreview: response.slice(0, 200) }, 'Provider API error returned as assistant text — clearing session');
1514
+ this.recordInteractiveFailure(sessionKey, text, response, 'provider_api_result_text', { effectiveSessionKey });
1515
+ this.clearSession(effectiveSessionKey);
1516
+ return "Claude returned a provider API error instead of a normal answer. I've reset this session so the error does not get replayed into future context. Please try that question again.";
1517
+ }
1503
1518
  if (response && looksLikeContextThrashText(response)) {
1504
1519
  logger.warn({ sessionKey, responsePreview: response.slice(0, 200) }, 'Context-thrash text returned from assistant — starting recovery pass');
1505
1520
  return this.startContextThrashRecovery(sessionKey, text, response, {
@@ -1668,6 +1683,11 @@ export class Gateway {
1668
1683
  switch (errKind) {
1669
1684
  case 'rate_limit':
1670
1685
  return "I'm being rate-limited by the API right now. Please wait a minute and try again.";
1686
+ case 'one_million_context':
1687
+ this.recordInteractiveFailure(sessionKey, text, err, 'one_million_context_exception', { effectiveSessionKey });
1688
+ applyOneMillionContextRecovery();
1689
+ this.clearSession(effectiveSessionKey);
1690
+ return oneMillionContextRecoveryMessage();
1671
1691
  case 'context_overflow':
1672
1692
  logger.info({ sessionKey }, 'Context overflow — rotating session');
1673
1693
  this.assistant.clearSession(effectiveSessionKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.19",
3
+ "version": "1.18.20",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",