clementine-agent 1.18.17 → 1.18.18
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.js +9 -14
- package/dist/agent/daily-planner.js +15 -4
- package/dist/agent/mcp-bridge.js +12 -4
- package/dist/agent/strategic-planner.js +13 -4
- package/dist/brain/adapters/pdf.js +13 -4
- package/dist/brain/llm-client.js +12 -4
- package/dist/cli/dashboard.js +33 -7
- package/dist/config.d.ts +12 -0
- package/dist/config.js +76 -0
- package/dist/index.js +21 -3
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -13,7 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { query as rawQuery, listSubagents, getSubagentMessages, SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import pino from 'pino';
|
|
16
|
-
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, usesOneMillionContext, envSnapshot, } from '../config.js';
|
|
16
|
+
import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE, MEMORY_FILE, AGENTS_DIR, ASSISTANT_NAME, OWNER_NAME, MODEL, MODELS, HEARTBEAT_MAX_TURNS, SEARCH_CONTEXT_LIMIT, SEARCH_RECENCY_LIMIT, SYSTEM_PROMPT_MAX_CONTEXT_CHARS, SESSION_EXCHANGE_HISTORY_SIZE, SESSION_EXCHANGE_MAX_CHARS, INJECTED_CONTEXT_MAX_CHARS, UNLEASHED_PHASE_TURNS, UNLEASHED_DEFAULT_MAX_HOURS, UNLEASHED_MAX_PHASES, PROJECTS_META_FILE, CRON_PROGRESS_DIR, CRON_REFLECTIONS_DIR, HANDOFFS_DIR, BUDGET, TASK_BUDGET_TOKENS, IDENTITY_FILE, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, claudeCodeDisableOneMillionForModel, currentOneMillionContextMode, normalizeClaudeModelForOneMillionContext, normalizeClaudeSdkOptionsForOneMillionContext, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, usesOneMillionContext, envSnapshot, } from '../config.js';
|
|
17
17
|
import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
|
|
18
18
|
import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, } from '../integrations/tool-preferences.js';
|
|
19
19
|
import { loadClaudeIntegrations } from './mcp-bridge.js';
|
|
@@ -288,7 +288,7 @@ const query = ((args) => {
|
|
|
288
288
|
if (typeof opts.appendSystemPrompt === 'string') {
|
|
289
289
|
newOpts.appendSystemPrompt = stripLoneSurrogates(opts.appendSystemPrompt);
|
|
290
290
|
}
|
|
291
|
-
cleaned.options = newOpts;
|
|
291
|
+
cleaned.options = normalizeClaudeSdkOptionsForOneMillionContext(newOpts);
|
|
292
292
|
}
|
|
293
293
|
return rawQuery(cleaned);
|
|
294
294
|
}
|
|
@@ -341,8 +341,7 @@ function resultInputTokens(result) {
|
|
|
341
341
|
return total;
|
|
342
342
|
}
|
|
343
343
|
export function looksLikeOneMillionContextError(value) {
|
|
344
|
-
|
|
345
|
-
return /extra usage.*1m context|1m context.*extra usage|context-1m/i.test(text);
|
|
344
|
+
return looksLikeClaudeOneMillionContextError(value);
|
|
346
345
|
}
|
|
347
346
|
export function looksLikeNoResponseRequested(value) {
|
|
348
347
|
const text = String(value ?? '').trim();
|
|
@@ -3427,14 +3426,13 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3427
3426
|
responseText = responseText || ('Claude says the account credit balance is too low. I paused background jobs for a few hours so they stop draining/retrying, but interactive chat will also fail until credits are available again.');
|
|
3428
3427
|
}
|
|
3429
3428
|
else if (looksLikeOneMillionContextError(errorText)) {
|
|
3430
|
-
|
|
3431
|
-
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
3429
|
+
applyOneMillionContextRecovery();
|
|
3432
3430
|
if (sessionKey) {
|
|
3433
3431
|
this.sessions.delete(sessionKey);
|
|
3434
3432
|
this.exchangeCounts.set(sessionKey, 0);
|
|
3435
3433
|
this._compactedSessions.delete(sessionKey);
|
|
3436
3434
|
}
|
|
3437
|
-
responseText = responseText || ("Claude rejected 1M context for this account. I've switched
|
|
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.");
|
|
3438
3436
|
}
|
|
3439
3437
|
else if (lower.includes('rate') && lower.includes('limit')) {
|
|
3440
3438
|
hitRateLimit = true;
|
|
@@ -3559,14 +3557,13 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
3559
3557
|
responseText = responseText || ('Claude says the account credit balance is too low. I paused background jobs for a few hours so they stop draining/retrying, but interactive chat will also fail until credits are available again.');
|
|
3560
3558
|
}
|
|
3561
3559
|
else if (looksLikeOneMillionContextError(e)) {
|
|
3562
|
-
|
|
3563
|
-
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
3560
|
+
applyOneMillionContextRecovery();
|
|
3564
3561
|
if (sessionKey) {
|
|
3565
3562
|
this.sessions.delete(sessionKey);
|
|
3566
3563
|
this.exchangeCounts.set(sessionKey, 0);
|
|
3567
3564
|
this._compactedSessions.delete(sessionKey);
|
|
3568
3565
|
}
|
|
3569
|
-
responseText = responseText || ("Claude rejected 1M context for this account. I've switched
|
|
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.");
|
|
3570
3567
|
}
|
|
3571
3568
|
else if (errStr.includes('rate') && (errStr.includes('limit') || errStr.includes('rate_limit'))) {
|
|
3572
3569
|
hitRateLimit = true;
|
|
@@ -4822,8 +4819,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
4822
4819
|
throw new Error(errText);
|
|
4823
4820
|
}
|
|
4824
4821
|
if (looksLikeOneMillionContextError(errText)) {
|
|
4825
|
-
|
|
4826
|
-
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
4822
|
+
applyOneMillionContextRecovery();
|
|
4827
4823
|
throw new Error(errText);
|
|
4828
4824
|
}
|
|
4829
4825
|
}
|
|
@@ -5171,8 +5167,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
|
|
|
5171
5167
|
throw new Error(exitText);
|
|
5172
5168
|
}
|
|
5173
5169
|
if (looksLikeOneMillionContextError(exitText)) {
|
|
5174
|
-
|
|
5175
|
-
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
5170
|
+
applyOneMillionContextRecovery();
|
|
5176
5171
|
throw new Error(exitText);
|
|
5177
5172
|
}
|
|
5178
5173
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import pino from 'pino';
|
|
11
|
-
import { BASE_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, } from '../config.js';
|
|
11
|
+
import { BASE_DIR, CRON_REFLECTIONS_DIR, TASKS_FILE, INBOX_DIR, MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
12
12
|
import { listAllGoals } from '../tools/shared.js';
|
|
13
13
|
const logger = pino({ name: 'clementine.daily-planner' });
|
|
14
14
|
const PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
@@ -253,15 +253,24 @@ Rules:
|
|
|
253
253
|
let text = '';
|
|
254
254
|
const stream = query({
|
|
255
255
|
prompt,
|
|
256
|
-
options: {
|
|
256
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
257
257
|
model: MODELS.haiku,
|
|
258
258
|
maxTurns: 1,
|
|
259
259
|
systemPrompt: 'You are a planning assistant. Analyze the context and produce a prioritized daily plan as JSON. Return only valid JSON, no markdown fencing.',
|
|
260
|
-
},
|
|
260
|
+
}),
|
|
261
261
|
});
|
|
262
262
|
for await (const msg of stream) {
|
|
263
|
-
if (msg.type === 'result')
|
|
263
|
+
if (msg.type === 'result') {
|
|
264
|
+
if (msg.is_error) {
|
|
265
|
+
const errorText = Array.isArray(msg.errors)
|
|
266
|
+
? msg.errors.join('; ')
|
|
267
|
+
: String(msg.result ?? '');
|
|
268
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
269
|
+
applyOneMillionContextRecovery();
|
|
270
|
+
throw new Error(errorText || 'Claude SDK query failed');
|
|
271
|
+
}
|
|
264
272
|
text = msg.result ?? '';
|
|
273
|
+
}
|
|
265
274
|
}
|
|
266
275
|
if (!text) {
|
|
267
276
|
logger.warn('LLM returned empty plan — using fallback');
|
|
@@ -278,6 +287,8 @@ Rules:
|
|
|
278
287
|
return plan;
|
|
279
288
|
}
|
|
280
289
|
catch (err) {
|
|
290
|
+
if (looksLikeClaudeOneMillionContextError(err))
|
|
291
|
+
applyOneMillionContextRecovery();
|
|
281
292
|
logger.warn({ err }, 'LLM plan generation failed — using fallback');
|
|
282
293
|
return this.fallbackPlan(today);
|
|
283
294
|
}
|
package/dist/agent/mcp-bridge.js
CHANGED
|
@@ -10,7 +10,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import pino from 'pino';
|
|
13
|
-
import { BASE_DIR } from '../config.js';
|
|
13
|
+
import { BASE_DIR, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
14
14
|
const logger = pino({ name: 'clementine.mcp-bridge' });
|
|
15
15
|
const MCP_SERVERS_FILE = path.join(BASE_DIR, 'mcp-servers.json');
|
|
16
16
|
const INTEGRATIONS_FILE = path.join(BASE_DIR, 'claude-integrations.json');
|
|
@@ -435,13 +435,13 @@ export async function probeAvailableTools(force = false) {
|
|
|
435
435
|
const externalMcpServers = getMcpServersForAgent();
|
|
436
436
|
const stream = query({
|
|
437
437
|
prompt: 'ok',
|
|
438
|
-
options: {
|
|
438
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
439
439
|
systemPrompt: 'Reply ok.',
|
|
440
440
|
model: 'claude-haiku-4-5',
|
|
441
441
|
permissionMode: 'bypassPermissions',
|
|
442
442
|
allowDangerouslySkipPermissions: true,
|
|
443
443
|
mcpServers: externalMcpServers,
|
|
444
|
-
},
|
|
444
|
+
}),
|
|
445
445
|
});
|
|
446
446
|
let tools = [];
|
|
447
447
|
for await (const msg of stream) {
|
|
@@ -449,8 +449,14 @@ export async function probeAvailableTools(force = false) {
|
|
|
449
449
|
tools = msg.tools;
|
|
450
450
|
break;
|
|
451
451
|
}
|
|
452
|
-
if (msg?.type === 'result')
|
|
452
|
+
if (msg?.type === 'result') {
|
|
453
|
+
if (msg.is_error) {
|
|
454
|
+
const errorText = Array.isArray(msg.errors) ? msg.errors.join('; ') : String(msg.result ?? '');
|
|
455
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
456
|
+
applyOneMillionContextRecovery();
|
|
457
|
+
}
|
|
453
458
|
break;
|
|
459
|
+
}
|
|
454
460
|
}
|
|
455
461
|
const inv = { probedAt: new Date().toISOString(), tools };
|
|
456
462
|
saveToolInventory(inv);
|
|
@@ -471,6 +477,8 @@ export async function probeAvailableTools(force = false) {
|
|
|
471
477
|
return inv;
|
|
472
478
|
}
|
|
473
479
|
catch (err) {
|
|
480
|
+
if (looksLikeClaudeOneMillionContextError(err))
|
|
481
|
+
applyOneMillionContextRecovery();
|
|
474
482
|
logger.warn({ err }, 'Tool inventory probe failed — using cached or empty');
|
|
475
483
|
return cached ?? { probedAt: new Date(0).toISOString(), tools: [] };
|
|
476
484
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import pino from 'pino';
|
|
15
|
-
import { BASE_DIR, GOALS_DIR, MODELS } from '../config.js';
|
|
15
|
+
import { BASE_DIR, GOALS_DIR, MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
16
16
|
import { listAllGoals } from '../tools/shared.js';
|
|
17
17
|
const logger = pino({ name: 'clementine.strategic-planner' });
|
|
18
18
|
const DAILY_PLANS_DIR = path.join(BASE_DIR, 'plans', 'daily');
|
|
@@ -23,15 +23,24 @@ async function llmJsonCall(prompt, systemPrompt) {
|
|
|
23
23
|
let text = '';
|
|
24
24
|
const stream = query({
|
|
25
25
|
prompt,
|
|
26
|
-
options: {
|
|
26
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
27
27
|
model: MODELS.haiku,
|
|
28
28
|
maxTurns: 1,
|
|
29
29
|
systemPrompt,
|
|
30
|
-
},
|
|
30
|
+
}),
|
|
31
31
|
});
|
|
32
32
|
for await (const msg of stream) {
|
|
33
|
-
if (msg.type === 'result')
|
|
33
|
+
if (msg.type === 'result') {
|
|
34
|
+
if (msg.is_error) {
|
|
35
|
+
const errorText = Array.isArray(msg.errors)
|
|
36
|
+
? msg.errors.join('; ')
|
|
37
|
+
: String(msg.result ?? '');
|
|
38
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
39
|
+
applyOneMillionContextRecovery();
|
|
40
|
+
throw new Error(errorText || 'Claude SDK query failed');
|
|
41
|
+
}
|
|
34
42
|
text = msg.result ?? '';
|
|
43
|
+
}
|
|
35
44
|
}
|
|
36
45
|
return text;
|
|
37
46
|
}
|
|
@@ -14,7 +14,7 @@ import { readFileSync } from 'node:fs';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import pdfParse from 'pdf-parse';
|
|
16
16
|
import { contentHash } from './common.js';
|
|
17
|
-
import { MODELS } from '../../config.js';
|
|
17
|
+
import { MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../../config.js';
|
|
18
18
|
export async function* parsePdf(filePath) {
|
|
19
19
|
let buf;
|
|
20
20
|
try {
|
|
@@ -91,7 +91,7 @@ async function ocrPdfViaClaude(filePath) {
|
|
|
91
91
|
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
92
92
|
const stream = query({
|
|
93
93
|
prompt: `Read the PDF at ${JSON.stringify(filePath)} using the Read tool. Transcribe every page's text verbatim — preserve the reading order, headings, lists, and paragraphs exactly as they appear. Separate pages with the form-feed character (\\f). Do NOT summarize, paraphrase, add commentary, or wrap in code fences. Output only the transcribed text.`,
|
|
94
|
-
options: {
|
|
94
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
95
95
|
model: MODELS.haiku,
|
|
96
96
|
maxTurns: 4, // Read tool call + response (a few turns of thinking is fine)
|
|
97
97
|
systemPrompt: 'You are a faithful OCR transcriber. Copy text exactly as written. When the PDF has images or scans, read the text from them using vision. Never invent content.',
|
|
@@ -99,7 +99,7 @@ async function ocrPdfViaClaude(filePath) {
|
|
|
99
99
|
allowedTools: ['Read'],
|
|
100
100
|
permissionMode: 'bypassPermissions',
|
|
101
101
|
settingSources: [],
|
|
102
|
-
},
|
|
102
|
+
}),
|
|
103
103
|
});
|
|
104
104
|
let text = '';
|
|
105
105
|
for await (const message of stream) {
|
|
@@ -113,6 +113,13 @@ async function ocrPdfViaClaude(filePath) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
else if (message.type === 'result') {
|
|
116
|
+
const result = message;
|
|
117
|
+
if (result.is_error) {
|
|
118
|
+
const errorText = Array.isArray(result.errors) ? result.errors.join('; ') : String(result.result ?? '');
|
|
119
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
120
|
+
applyOneMillionContextRecovery();
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
116
123
|
break;
|
|
117
124
|
}
|
|
118
125
|
}
|
|
@@ -121,7 +128,9 @@ async function ocrPdfViaClaude(filePath) {
|
|
|
121
128
|
return [];
|
|
122
129
|
return splitPages(cleaned);
|
|
123
130
|
}
|
|
124
|
-
catch {
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (looksLikeClaudeOneMillionContextError(err))
|
|
133
|
+
applyOneMillionContextRecovery();
|
|
125
134
|
return [];
|
|
126
135
|
}
|
|
127
136
|
}
|
package/dist/brain/llm-client.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* pipeline can be verified without spawning the SDK subprocess.
|
|
14
14
|
*/
|
|
15
15
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
-
import { MODELS } from '../config.js';
|
|
16
|
+
import { MODELS, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
17
17
|
let override = null;
|
|
18
18
|
/** Inject a deterministic override (used by tests). Pass null to restore. */
|
|
19
19
|
export function setLLMOverride(fn) {
|
|
@@ -29,10 +29,11 @@ export async function callLLM(prompt, opts = {}) {
|
|
|
29
29
|
if (opts.format === 'json') {
|
|
30
30
|
systemParts.push('Respond with a single valid JSON object. No prose, no code fences, no explanation.');
|
|
31
31
|
}
|
|
32
|
+
const model = opts.model ?? MODELS.haiku;
|
|
32
33
|
const stream = query({
|
|
33
34
|
prompt,
|
|
34
|
-
options: {
|
|
35
|
-
model
|
|
35
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
36
|
+
model,
|
|
36
37
|
maxTurns: 1,
|
|
37
38
|
systemPrompt: systemParts.join('\n\n') || undefined,
|
|
38
39
|
// No built-in tools: brain calls are pure completions
|
|
@@ -42,7 +43,7 @@ export async function callLLM(prompt, opts = {}) {
|
|
|
42
43
|
// allowed-tool lists, and statusline config that can slow or
|
|
43
44
|
// fail our minimal call.
|
|
44
45
|
settingSources: [],
|
|
45
|
-
},
|
|
46
|
+
}),
|
|
46
47
|
});
|
|
47
48
|
let assistantText = '';
|
|
48
49
|
for await (const message of stream) {
|
|
@@ -56,6 +57,13 @@ export async function callLLM(prompt, opts = {}) {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
else if (message.type === 'result') {
|
|
60
|
+
const result = message;
|
|
61
|
+
if (result.is_error) {
|
|
62
|
+
const errorText = Array.isArray(result.errors) ? result.errors.join('; ') : String(result.result ?? '');
|
|
63
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
64
|
+
applyOneMillionContextRecovery();
|
|
65
|
+
throw new Error(errorText || 'Claude SDK query failed');
|
|
66
|
+
}
|
|
59
67
|
break; // Single-turn done
|
|
60
68
|
}
|
|
61
69
|
}
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -18,7 +18,7 @@ import cron from 'node-cron';
|
|
|
18
18
|
import { TunnelManager } from './tunnel.js';
|
|
19
19
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
20
20
|
import { discoverMcpServers, getClaudeIntegrations } from '../agent/mcp-bridge.js';
|
|
21
|
-
import { AGENTS_DIR, SESSIONS_FILE } from '../config.js';
|
|
21
|
+
import { AGENTS_DIR, SESSIONS_FILE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
22
22
|
import { parseTasks } from '../tools/shared.js';
|
|
23
23
|
import { todayISO } from '../gateway/cron-scheduler.js';
|
|
24
24
|
import { goalsRouter } from './routes/goals.js';
|
|
@@ -2066,7 +2066,14 @@ export async function cmdDashboard(opts) {
|
|
|
2066
2066
|
}
|
|
2067
2067
|
catch { /* ignore */ }
|
|
2068
2068
|
}
|
|
2069
|
-
oauthQuery = sdkQuery({
|
|
2069
|
+
oauthQuery = sdkQuery({
|
|
2070
|
+
prompt: '',
|
|
2071
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
2072
|
+
permissionMode: 'bypassPermissions',
|
|
2073
|
+
allowDangerouslySkipPermissions: true,
|
|
2074
|
+
maxTurns: 0,
|
|
2075
|
+
}),
|
|
2076
|
+
});
|
|
2070
2077
|
const result = await oauthQuery.claudeAuthenticate(true);
|
|
2071
2078
|
res.json({ ok: true, result });
|
|
2072
2079
|
}
|
|
@@ -4003,7 +4010,7 @@ export async function cmdDashboard(opts) {
|
|
|
4003
4010
|
Return ONLY a JSON array. Each element must be an object with shape \`{"id": string, "label": string, "sublabel"?: string}\`. Use the source system's stable id for \`id\` (so the feed can reference it later). Use a short human-readable title for \`label\`. Optional \`sublabel\` can include path, email, date, or any disambiguating detail.
|
|
4004
4011
|
Do NOT include prose, markdown, code fences, or explanation. Just the JSON array.
|
|
4005
4012
|
If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
4006
|
-
options: {
|
|
4013
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
4007
4014
|
model: MODELS.haiku,
|
|
4008
4015
|
maxTurns: 3,
|
|
4009
4016
|
systemPrompt: 'You are a data enumerator. You call the given tool once, extract the items from its response, and emit a strict JSON array. No commentary.',
|
|
@@ -4011,7 +4018,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4011
4018
|
mcpServers,
|
|
4012
4019
|
permissionMode: 'bypassPermissions',
|
|
4013
4020
|
settingSources: [],
|
|
4014
|
-
},
|
|
4021
|
+
}),
|
|
4015
4022
|
});
|
|
4016
4023
|
let text = '';
|
|
4017
4024
|
for await (const msg of stream) {
|
|
@@ -4023,6 +4030,13 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4023
4030
|
}
|
|
4024
4031
|
}
|
|
4025
4032
|
else if (msg.type === 'result') {
|
|
4033
|
+
if (msg.is_error) {
|
|
4034
|
+
const errorText = Array.isArray(msg.errors)
|
|
4035
|
+
? msg.errors.join('; ')
|
|
4036
|
+
: String(msg.result ?? '');
|
|
4037
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
4038
|
+
applyOneMillionContextRecovery();
|
|
4039
|
+
}
|
|
4026
4040
|
break;
|
|
4027
4041
|
}
|
|
4028
4042
|
}
|
|
@@ -4039,6 +4053,8 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
4039
4053
|
res.json({ items, cached: false, rawPreview: text.slice(0, 400) });
|
|
4040
4054
|
}
|
|
4041
4055
|
catch (err) {
|
|
4056
|
+
if (looksLikeClaudeOneMillionContextError(err))
|
|
4057
|
+
applyOneMillionContextRecovery();
|
|
4042
4058
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
4043
4059
|
}
|
|
4044
4060
|
});
|
|
@@ -6321,20 +6337,30 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6321
6337
|
let result = '';
|
|
6322
6338
|
const stream = query({
|
|
6323
6339
|
prompt,
|
|
6324
|
-
options: {
|
|
6340
|
+
options: normalizeClaudeSdkOptionsForOneMillionContext({
|
|
6325
6341
|
model: 'claude-haiku-4-5-20251001',
|
|
6326
6342
|
maxTurns: 1,
|
|
6327
6343
|
systemPrompt: 'You are a memory consolidation assistant. Extract only facts directly evidenced by the corpus. Be terse. Output exactly the requested format.',
|
|
6328
|
-
},
|
|
6344
|
+
}),
|
|
6329
6345
|
});
|
|
6330
6346
|
for await (const msg of stream) {
|
|
6331
6347
|
if (msg.type === 'result') {
|
|
6348
|
+
if (msg.is_error) {
|
|
6349
|
+
const errorText = Array.isArray(msg.errors)
|
|
6350
|
+
? (msg.errors ?? []).join('; ')
|
|
6351
|
+
: String(msg.result ?? '');
|
|
6352
|
+
if (looksLikeClaudeOneMillionContextError(errorText))
|
|
6353
|
+
applyOneMillionContextRecovery();
|
|
6354
|
+
return '';
|
|
6355
|
+
}
|
|
6332
6356
|
result = msg.result ?? '';
|
|
6333
6357
|
}
|
|
6334
6358
|
}
|
|
6335
6359
|
return result;
|
|
6336
6360
|
}
|
|
6337
|
-
catch {
|
|
6361
|
+
catch (err) {
|
|
6362
|
+
if (looksLikeClaudeOneMillionContextError(err))
|
|
6363
|
+
applyOneMillionContextRecovery();
|
|
6338
6364
|
return '';
|
|
6339
6365
|
}
|
|
6340
6366
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export type OneMillionContextMode = 'auto' | 'off' | 'on';
|
|
|
15
15
|
export declare const CLEMENTINE_1M_CONTEXT_MODE: OneMillionContextMode;
|
|
16
16
|
export declare function currentOneMillionContextMode(): OneMillionContextMode;
|
|
17
17
|
export declare function claudeCodeDisableOneMillionForModel(model: string | null | undefined, mode?: OneMillionContextMode): '1' | '0' | undefined;
|
|
18
|
+
export declare function looksLikeClaudeOneMillionContextError(value: unknown): boolean;
|
|
19
|
+
export declare function applyOneMillionContextRecovery(baseDir?: string): void;
|
|
20
|
+
export declare function claudeOneMillionEnvForModel(model: string | null | undefined, mode?: OneMillionContextMode): Record<string, string>;
|
|
21
|
+
type ClaudeSdkOptionsLike = {
|
|
22
|
+
model?: string;
|
|
23
|
+
env?: Record<string, string | undefined>;
|
|
24
|
+
betas?: unknown[];
|
|
25
|
+
mcpServers?: Record<string, unknown>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
export declare function normalizeClaudeSdkOptionsForOneMillionContext<T extends ClaudeSdkOptionsLike>(options: T): T;
|
|
18
29
|
export declare function normalizeClaudeModelForOneMillionContext(model: string, mode?: OneMillionContextMode): string;
|
|
19
30
|
export declare function usesOneMillionContext(model: string | null | undefined, mode?: OneMillionContextMode): boolean;
|
|
20
31
|
/**
|
|
@@ -196,4 +207,5 @@ export declare function getCredential(ref: string): string | null;
|
|
|
196
207
|
export declare function setCredential(ref: string, value: string): void;
|
|
197
208
|
/** List known credential refs (not their values) for dashboard display. */
|
|
198
209
|
export declare function listCredentialRefs(): string[];
|
|
210
|
+
export {};
|
|
199
211
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
|
@@ -88,6 +88,82 @@ export function claudeCodeDisableOneMillionForModel(model, mode = currentOneMill
|
|
|
88
88
|
return '0';
|
|
89
89
|
return modelFamily(model) === 'opus' ? undefined : '1';
|
|
90
90
|
}
|
|
91
|
+
function upsertRuntimeEnvValue(baseDir, key, value) {
|
|
92
|
+
const envPath = path.join(baseDir, '.env');
|
|
93
|
+
let text = '';
|
|
94
|
+
if (existsSync(envPath)) {
|
|
95
|
+
text = readFileSync(envPath, 'utf-8');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
const line = `${key}=${value}`;
|
|
101
|
+
const re = new RegExp(`^${key}=.*$`, 'm');
|
|
102
|
+
const next = re.test(text)
|
|
103
|
+
? text.replace(re, line)
|
|
104
|
+
: `${text}${text && !text.endsWith('\n') ? '\n' : ''}${line}\n`;
|
|
105
|
+
fs.writeFileSync(envPath, next, { mode: 0o600 });
|
|
106
|
+
}
|
|
107
|
+
export function looksLikeClaudeOneMillionContextError(value) {
|
|
108
|
+
const text = String(value ?? '');
|
|
109
|
+
return /extra usage.*1m context|1m context.*extra usage|context-1m|1m.*extra usage|requires?.*1m/i.test(text);
|
|
110
|
+
}
|
|
111
|
+
export function applyOneMillionContextRecovery(baseDir = BASE_DIR) {
|
|
112
|
+
process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
|
|
113
|
+
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
|
|
114
|
+
try {
|
|
115
|
+
upsertRuntimeEnvValue(baseDir, 'CLEMENTINE_1M_CONTEXT_MODE', 'off');
|
|
116
|
+
upsertRuntimeEnvValue(baseDir, 'CLAUDE_CODE_DISABLE_1M_CONTEXT', '1');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Runtime env is already safe. Persisting is best-effort because this path
|
|
120
|
+
// is often called while handling an SDK failure.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function claudeOneMillionEnvForModel(model, mode = currentOneMillionContextMode()) {
|
|
124
|
+
const disableValue = claudeCodeDisableOneMillionForModel(model, mode);
|
|
125
|
+
return {
|
|
126
|
+
CLEMENTINE_1M_CONTEXT_MODE: mode,
|
|
127
|
+
...(disableValue !== undefined ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: disableValue } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function normalizeClaudeSdkOptionsForOneMillionContext(options) {
|
|
131
|
+
const rawModel = typeof options.model === 'string' ? options.model : '';
|
|
132
|
+
const model = rawModel ? normalizeClaudeModelForOneMillionContext(rawModel) : rawModel;
|
|
133
|
+
const oneMillionEnv = claudeOneMillionEnvForModel(model || rawModel || null);
|
|
134
|
+
const disableValue = oneMillionEnv.CLAUDE_CODE_DISABLE_1M_CONTEXT;
|
|
135
|
+
const next = {
|
|
136
|
+
...options,
|
|
137
|
+
...(rawModel ? { model } : {}),
|
|
138
|
+
env: { ...(options.env ?? {}), ...oneMillionEnv },
|
|
139
|
+
...(disableValue === '1' ? { betas: [] } : {}),
|
|
140
|
+
};
|
|
141
|
+
if (options.mcpServers && typeof options.mcpServers === 'object') {
|
|
142
|
+
const servers = {};
|
|
143
|
+
for (const [name, server] of Object.entries(options.mcpServers)) {
|
|
144
|
+
if (server && typeof server === 'object' && !Array.isArray(server)) {
|
|
145
|
+
const serverObj = server;
|
|
146
|
+
const supportsEnv = serverObj.type === 'stdio' || 'env' in serverObj;
|
|
147
|
+
if (!supportsEnv) {
|
|
148
|
+
servers[name] = server;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const serverEnv = serverObj.env && typeof serverObj.env === 'object' && !Array.isArray(serverObj.env)
|
|
152
|
+
? serverObj.env
|
|
153
|
+
: {};
|
|
154
|
+
servers[name] = {
|
|
155
|
+
...serverObj,
|
|
156
|
+
env: { ...serverEnv, ...oneMillionEnv },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
servers[name] = server;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
next.mcpServers = servers;
|
|
164
|
+
}
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
91
167
|
export function normalizeClaudeModelForOneMillionContext(model, mode = currentOneMillionContextMode()) {
|
|
92
168
|
if (mode === 'on')
|
|
93
169
|
return model;
|
package/dist/index.js
CHANGED
|
@@ -626,14 +626,32 @@ async function asyncMain() {
|
|
|
626
626
|
const llmCall = async (prompt) => {
|
|
627
627
|
try {
|
|
628
628
|
let result = '';
|
|
629
|
-
const stream = query({
|
|
629
|
+
const stream = query({
|
|
630
|
+
prompt,
|
|
631
|
+
options: config.normalizeClaudeSdkOptionsForOneMillionContext({
|
|
632
|
+
model: 'claude-haiku-4-5-20251001',
|
|
633
|
+
maxTurns: 1,
|
|
634
|
+
systemPrompt: 'You are a memory consolidation assistant. Be concise.',
|
|
635
|
+
}),
|
|
636
|
+
});
|
|
630
637
|
for await (const msg of stream) {
|
|
631
|
-
if (msg.type === 'result')
|
|
638
|
+
if (msg.type === 'result') {
|
|
639
|
+
if (msg.is_error) {
|
|
640
|
+
const errorText = Array.isArray(msg.errors)
|
|
641
|
+
? msg.errors.join('; ')
|
|
642
|
+
: String(msg.result ?? '');
|
|
643
|
+
if (config.looksLikeClaudeOneMillionContextError(errorText))
|
|
644
|
+
config.applyOneMillionContextRecovery();
|
|
645
|
+
return '';
|
|
646
|
+
}
|
|
632
647
|
result = msg.result ?? '';
|
|
648
|
+
}
|
|
633
649
|
}
|
|
634
650
|
return result;
|
|
635
651
|
}
|
|
636
|
-
catch {
|
|
652
|
+
catch (err) {
|
|
653
|
+
if (config.looksLikeClaudeOneMillionContextError(err))
|
|
654
|
+
config.applyOneMillionContextRecovery();
|
|
637
655
|
return '';
|
|
638
656
|
}
|
|
639
657
|
};
|