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.
- package/dist/agent/assistant.d.ts +2 -0
- package/dist/agent/assistant.js +25 -4
- package/dist/cli/dashboard.js +56 -0
- package/dist/gateway/router.d.ts +1 -1
- package/dist/gateway/router.js +23 -3
- package/package.json +1 -1
|
@@ -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;
|
package/dist/agent/assistant.js
CHANGED
|
@@ -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 =
|
|
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 || (
|
|
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 (
|
|
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 || (
|
|
3587
|
+
responseText = responseText || (oneMillionContextRecoveryMessage());
|
|
3567
3588
|
}
|
|
3568
3589
|
else if (errStr.includes('rate') && (errStr.includes('limit') || errStr.includes('rate_limit'))) {
|
|
3569
3590
|
hitRateLimit = true;
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -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 & 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>';
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -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;
|
package/dist/gateway/router.js
CHANGED
|
@@ -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 (
|
|
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);
|