clementine-agent 1.18.17 → 1.18.19

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.
@@ -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
- const text = String(value ?? '');
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
- process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
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 this process to 200K recovery mode and reset the session. To persist the fix across restarts, run `clementine budgets safe`, then `clementine restart`.");
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
- process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
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 this process to 200K recovery mode and reset the session. To persist the fix across restarts, run `clementine budgets safe`, then `clementine restart`.");
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
- process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
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
- process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
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
  }
@@ -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
  }
@@ -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: opts.model ?? MODELS.haiku,
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
  }
@@ -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({ prompt: '', options: { permissionMode: 'bypassPermissions', allowDangerouslySkipPermissions: true, maxTurns: 0 } });
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
  });
@@ -5817,7 +5833,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
5817
5833
  }
5818
5834
  else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
5819
5835
  writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
5820
- message = 'Removed spend caps by setting all budget values to 0. Restart Clementine to apply to running workers.';
5836
+ message = 'Removed spend caps by setting all budget values to 0. This does not change 1M context mode; use Force 200K or Safe Recovery for 1M errors. Restart Clementine to apply to running workers.';
5821
5837
  }
5822
5838
  else {
5823
5839
  res.status(400).json({ error: 'preset must be defaults or uncapped' });
@@ -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
  };
@@ -20063,7 +20089,7 @@ async function refreshBudgetHealth() {
20063
20089
  + '<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">'
20064
20090
  + '<button class="btn-sm btn-primary" onclick="applySafeBudgetPreset()">Safe Recovery</button>'
20065
20091
  + '<button class="btn-sm" onclick="applyBudgetPreset(\\x27defaults\\x27)">Default Caps</button>'
20066
- + '<button class="btn-sm" onclick="applyBudgetPreset(\\x27uncapped\\x27)">No Caps</button>'
20092
+ + '<button class="btn-sm" onclick="applyBudgetPreset(\\x27uncapped\\x27)">No Spend Caps</button>'
20067
20093
  + '<button class="btn-sm" onclick="setBudgetContextMode(\\x27auto\\x27)">Smart Auto</button>'
20068
20094
  + '<button class="btn-sm" onclick="setBudgetContextMode(\\x27off\\x27)">Force 200K</button>'
20069
20095
  + '<button class="btn-sm" onclick="forceBudgetOneMillion()">Force 1M</button>'
@@ -20154,7 +20180,7 @@ async function applySafeBudgetPreset() {
20154
20180
  }
20155
20181
 
20156
20182
  async function applyBudgetPreset(preset) {
20157
- if (preset === 'uncapped' && !confirm('Remove all spend caps? Clementine can still hit account limits or credits if a job runs long.')) return;
20183
+ if (preset === 'uncapped' && !confirm('Remove spend caps only? This does not disable 1M context. For 1M errors, use Safe Recovery or Force 200K.')) return;
20158
20184
  var d = await postBudgetAction('/api/budgets/preset', { preset: preset });
20159
20185
  if (d && d.ok) markRestartRequired('Spend guard changes need a Clementine restart before running workers use the new caps.');
20160
20186
  refreshSettings();
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({ prompt, options: { model: 'claude-haiku-4-5-20251001', maxTurns: 1, systemPrompt: 'You are a memory consolidation assistant. Be concise.' } });
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.17",
3
+ "version": "1.18.19",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",