clementine-agent 1.18.16 → 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/README.md +3 -1
- 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 +139 -19
- 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/README.md
CHANGED
|
@@ -326,7 +326,9 @@ Your overrides live in `~/.clementine/.env` — **they survive every `npm update
|
|
|
326
326
|
|
|
327
327
|
The dashboard exposes these spend controls in Settings -> Channels & Env ->
|
|
328
328
|
Spend Guards & Context Health, including direct dollar-cap editing, Default
|
|
329
|
-
Caps, Safe Recovery, and No Caps presets.
|
|
329
|
+
Caps, Safe Recovery, and No Caps presets. When a dashboard change needs the
|
|
330
|
+
daemon to reload, Clementine shows a Restart Clementine prompt and handles the
|
|
331
|
+
restart from the browser.
|
|
330
332
|
|
|
331
333
|
For spend/context tuning, `clementine budgets` gives a safer shortcut:
|
|
332
334
|
|
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
|
};
|
|
@@ -15414,7 +15440,7 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15414
15440
|
<div class="tab-pane active" id="tab-settings-general">
|
|
15415
15441
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
|
15416
15442
|
<p style="color:var(--text-muted);margin:0">Manage API keys and configuration. Changes are saved to <code>~/.clementine/.env</code>.</p>
|
|
15417
|
-
<button class="btn-sm" style="white-space:nowrap;
|
|
15443
|
+
<button class="btn-sm btn-primary" style="white-space:nowrap;padding:6px 12px;border-radius:6px;cursor:pointer" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
|
|
15418
15444
|
</div>
|
|
15419
15445
|
<div id="budget-health-content" style="margin-bottom:16px"><div class="empty-state">Loading budget health...</div></div>
|
|
15420
15446
|
<div id="settings-content"><div class="empty-state">Loading settings...</div></div>
|
|
@@ -15579,8 +15605,8 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
15579
15605
|
<div class="card" style="margin-bottom:16px">
|
|
15580
15606
|
<div class="card-header">Diagnostics & maintenance</div>
|
|
15581
15607
|
<div class="card-body" style="padding:16px;display:flex;gap:8px;flex-wrap:wrap">
|
|
15608
|
+
<button class="btn-sm btn-primary" onclick="restartDaemonFromDashboard()">Restart Clementine</button>
|
|
15582
15609
|
<button class="btn-sm" onclick="restartDashboard()">Restart Dashboard</button>
|
|
15583
|
-
<button class="btn-sm" onclick="if(confirm('Restart the daemon? Active sessions drain first.')) apiPost('/api/restart')">Restart Daemon</button>
|
|
15584
15610
|
<button class="btn-sm" onclick="apiFetch('/api/doctor').then(function(r){return r.text()}).then(function(t){alert(t)})">Run Doctor</button>
|
|
15585
15611
|
<button class="btn-sm" onclick="apiFetch('/api/version').then(function(r){return r.json()}).then(function(d){alert('Version: '+(d.version||'?')+'\\nNode: '+(d.node||'?'))})">Build info</button>
|
|
15586
15612
|
</div>
|
|
@@ -17633,6 +17659,7 @@ async function apiPost(url) {
|
|
|
17633
17659
|
if (d.ok) toast(d.message, 'success');
|
|
17634
17660
|
else toast(d.error || 'Error', 'error');
|
|
17635
17661
|
setTimeout(refreshAll, 1000);
|
|
17662
|
+
return d;
|
|
17636
17663
|
} catch(e) { toast(String(e), 'error'); }
|
|
17637
17664
|
}
|
|
17638
17665
|
async function apiJson(method, url, body) {
|
|
@@ -17656,9 +17683,84 @@ async function apiDelete(url) {
|
|
|
17656
17683
|
if (d.ok) toast(d.message, 'success');
|
|
17657
17684
|
else toast(d.error || 'Error', 'error');
|
|
17658
17685
|
setTimeout(refreshAll, 500);
|
|
17686
|
+
return d;
|
|
17659
17687
|
} catch(e) { toast(String(e), 'error'); }
|
|
17660
17688
|
}
|
|
17661
17689
|
|
|
17690
|
+
function settingRequiresDaemonRestart(key) {
|
|
17691
|
+
if (!key) return true;
|
|
17692
|
+
if (key === 'COMPOSIO_API_KEY' || key === 'COMPOSIO_USER_ID') return false;
|
|
17693
|
+
if (key.indexOf('ASSISTANT_') === 0) return false;
|
|
17694
|
+
return true;
|
|
17695
|
+
}
|
|
17696
|
+
|
|
17697
|
+
function renderRestartRequiredBanner() {
|
|
17698
|
+
var reason = '';
|
|
17699
|
+
try { reason = localStorage.getItem('clem-restart-required') || ''; } catch(e) { reason = ''; }
|
|
17700
|
+
var existing = document.getElementById('restart-required-banner');
|
|
17701
|
+
if (!reason) {
|
|
17702
|
+
if (existing) existing.remove();
|
|
17703
|
+
return;
|
|
17704
|
+
}
|
|
17705
|
+
if (!existing) {
|
|
17706
|
+
existing = document.createElement('div');
|
|
17707
|
+
existing.id = 'restart-required-banner';
|
|
17708
|
+
existing.style.cssText = 'position:fixed;left:18px;right:18px;bottom:18px;z-index:9999;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);box-shadow:0 8px 28px rgba(0,0,0,0.28);padding:12px 14px;color:var(--text-primary)';
|
|
17709
|
+
document.body.appendChild(existing);
|
|
17710
|
+
}
|
|
17711
|
+
existing.innerHTML = '<div style="min-width:220px;flex:1"><div style="font-weight:700;font-size:13px">Restart required</div>'
|
|
17712
|
+
+ '<div style="font-size:12px;color:var(--text-secondary);margin-top:2px">' + esc(reason) + '</div></div>'
|
|
17713
|
+
+ '<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
|
|
17714
|
+
+ '<button class="btn-sm btn-primary" onclick="restartDaemonFromDashboard()">Restart Clementine</button>'
|
|
17715
|
+
+ '<button class="btn-sm" onclick="dismissRestartRequiredBanner()">Later</button>'
|
|
17716
|
+
+ '</div>';
|
|
17717
|
+
}
|
|
17718
|
+
|
|
17719
|
+
function markRestartRequired(reason) {
|
|
17720
|
+
var msg = reason || 'This change needs a Clementine restart before the daemon and channel workers use it.';
|
|
17721
|
+
try { localStorage.setItem('clem-restart-required', msg); } catch(e) { /* ignore */ }
|
|
17722
|
+
renderRestartRequiredBanner();
|
|
17723
|
+
}
|
|
17724
|
+
|
|
17725
|
+
function clearRestartRequired() {
|
|
17726
|
+
try { localStorage.removeItem('clem-restart-required'); } catch(e) { /* ignore */ }
|
|
17727
|
+
var existing = document.getElementById('restart-required-banner');
|
|
17728
|
+
if (existing) existing.remove();
|
|
17729
|
+
}
|
|
17730
|
+
|
|
17731
|
+
function dismissRestartRequiredBanner() {
|
|
17732
|
+
var existing = document.getElementById('restart-required-banner');
|
|
17733
|
+
if (existing) existing.remove();
|
|
17734
|
+
}
|
|
17735
|
+
|
|
17736
|
+
async function restartDaemonFromDashboard(skipConfirm) {
|
|
17737
|
+
if (!skipConfirm && !confirm('Restart Clementine now? Active work may pause briefly while the daemon reloads.')) return;
|
|
17738
|
+
toast('Restarting Clementine...', 'info');
|
|
17739
|
+
try {
|
|
17740
|
+
var r = await apiFetch('/api/restart', { method: 'POST' });
|
|
17741
|
+
var d = {};
|
|
17742
|
+
try { d = await r.json(); } catch(e) { d = {}; }
|
|
17743
|
+
if (!r.ok || d.error) {
|
|
17744
|
+
var err = String(d.error || 'Restart failed');
|
|
17745
|
+
if (/not running/i.test(err)) {
|
|
17746
|
+
var launch = await apiFetch('/api/launch', { method: 'POST' });
|
|
17747
|
+
var launchData = await launch.json();
|
|
17748
|
+
if (!launch.ok || launchData.error) throw new Error(launchData.error || 'Launch failed');
|
|
17749
|
+
clearRestartRequired();
|
|
17750
|
+
toast('Clementine started', 'success');
|
|
17751
|
+
setTimeout(refreshAll, 2000);
|
|
17752
|
+
return;
|
|
17753
|
+
}
|
|
17754
|
+
throw new Error(err);
|
|
17755
|
+
}
|
|
17756
|
+
clearRestartRequired();
|
|
17757
|
+
toast('Clementine restart requested', 'success');
|
|
17758
|
+
setTimeout(refreshAll, 2500);
|
|
17759
|
+
} catch(e) {
|
|
17760
|
+
toast('Restart failed: ' + String(e), 'error');
|
|
17761
|
+
}
|
|
17762
|
+
}
|
|
17763
|
+
|
|
17662
17764
|
// ── Status + Overview ─────────────────────
|
|
17663
17765
|
let lastStatusData = {};
|
|
17664
17766
|
async function refreshStatus(preloaded) {
|
|
@@ -18809,6 +18911,7 @@ async function saveHeartbeatControl() {
|
|
|
18809
18911
|
if (!r.ok || d.error) throw new Error(d.error || 'Save failed');
|
|
18810
18912
|
if (statusEl) { statusEl.textContent = 'Saved. Restart daemon for schedule changes.'; statusEl.style.color = 'var(--green)'; }
|
|
18811
18913
|
toast('Heartbeat controls saved', 'success');
|
|
18914
|
+
markRestartRequired('Heartbeat control changes need a Clementine restart before the schedule uses them.');
|
|
18812
18915
|
} catch(e) {
|
|
18813
18916
|
if (statusEl) { statusEl.textContent = 'Save failed'; statusEl.style.color = 'var(--red)'; }
|
|
18814
18917
|
toast(String(e), 'error');
|
|
@@ -20071,13 +20174,15 @@ async function postBudgetAction(url, body) {
|
|
|
20071
20174
|
}
|
|
20072
20175
|
|
|
20073
20176
|
async function applySafeBudgetPreset() {
|
|
20074
|
-
await postBudgetAction('/api/budgets/safe', {});
|
|
20177
|
+
var d = await postBudgetAction('/api/budgets/safe', {});
|
|
20178
|
+
if (d && d.ok) markRestartRequired('Safe Recovery changed spend/context settings. Restart Clementine to apply them to chat and background workers.');
|
|
20075
20179
|
refreshSettings();
|
|
20076
20180
|
}
|
|
20077
20181
|
|
|
20078
20182
|
async function applyBudgetPreset(preset) {
|
|
20079
20183
|
if (preset === 'uncapped' && !confirm('Remove all spend caps? Clementine can still hit account limits or credits if a job runs long.')) return;
|
|
20080
|
-
await postBudgetAction('/api/budgets/preset', { preset: preset });
|
|
20184
|
+
var d = await postBudgetAction('/api/budgets/preset', { preset: preset });
|
|
20185
|
+
if (d && d.ok) markRestartRequired('Spend guard changes need a Clementine restart before running workers use the new caps.');
|
|
20081
20186
|
refreshSettings();
|
|
20082
20187
|
}
|
|
20083
20188
|
|
|
@@ -20090,12 +20195,14 @@ async function saveBudgetCap(key) {
|
|
|
20090
20195
|
toast('Budget must be a non-negative dollar amount. Use 0 for no cap.', 'error');
|
|
20091
20196
|
return;
|
|
20092
20197
|
}
|
|
20093
|
-
await postBudgetAction('/api/budgets/set', { key: key, value: value });
|
|
20198
|
+
var d = await postBudgetAction('/api/budgets/set', { key: key, value: value });
|
|
20199
|
+
if (d && d.ok) markRestartRequired('Budget cap changes need a Clementine restart before running workers use the new value.');
|
|
20094
20200
|
refreshSettings();
|
|
20095
20201
|
}
|
|
20096
20202
|
|
|
20097
20203
|
async function setBudgetContextMode(mode) {
|
|
20098
|
-
await postBudgetAction('/api/budgets/1m', { mode: mode });
|
|
20204
|
+
var d = await postBudgetAction('/api/budgets/1m', { mode: mode });
|
|
20205
|
+
if (d && d.ok) markRestartRequired('1M context changes need a Clementine restart before new Claude calls use the setting.');
|
|
20099
20206
|
refreshSettings();
|
|
20100
20207
|
}
|
|
20101
20208
|
|
|
@@ -20105,7 +20212,10 @@ async function forceBudgetOneMillion() {
|
|
|
20105
20212
|
}
|
|
20106
20213
|
|
|
20107
20214
|
async function applyBudgetDoctorFix() {
|
|
20108
|
-
await postBudgetAction('/api/budgets/doctor-fix', {});
|
|
20215
|
+
var d = await postBudgetAction('/api/budgets/doctor-fix', {});
|
|
20216
|
+
if (d && d.ok && d.result && d.result.changed && d.result.changed.length) {
|
|
20217
|
+
markRestartRequired('Doctor Fix changed Clementine configuration. Restart Clementine to apply the fixes.');
|
|
20218
|
+
}
|
|
20109
20219
|
refreshSettings();
|
|
20110
20220
|
}
|
|
20111
20221
|
|
|
@@ -20209,8 +20319,7 @@ async function refreshSettings() {
|
|
|
20209
20319
|
+ '</div></div>';
|
|
20210
20320
|
|
|
20211
20321
|
html += '<div style="padding:12px;color:var(--text-muted);font-size:12px">'
|
|
20212
|
-
+ '<strong>Note:</strong> Changes
|
|
20213
|
-
+ 'Use <code>clementine restart</code> after updating channel tokens.'
|
|
20322
|
+
+ '<strong>Note:</strong> Changes that require a daemon restart show a Restart Clementine prompt here in the dashboard.'
|
|
20214
20323
|
+ '</div>';
|
|
20215
20324
|
container.innerHTML = html;
|
|
20216
20325
|
|
|
@@ -20273,8 +20382,11 @@ async function toggleSetting(el) {
|
|
|
20273
20382
|
async function saveSettingValue(key, value) {
|
|
20274
20383
|
var statusEl = document.getElementById('setting-' + key + '-status');
|
|
20275
20384
|
try {
|
|
20276
|
-
await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20385
|
+
var result = await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20277
20386
|
if (statusEl) { statusEl.textContent = 'Saved'; statusEl.style.color = 'var(--green)'; setTimeout(function(){ statusEl.textContent = ''; }, 2000); }
|
|
20387
|
+
if (result && result.ok && settingRequiresDaemonRestart(key)) {
|
|
20388
|
+
markRestartRequired(key + ' changed. Restart Clementine so the daemon and channel workers use the new value.');
|
|
20389
|
+
}
|
|
20278
20390
|
} catch(e) {
|
|
20279
20391
|
if (statusEl) { statusEl.textContent = 'Error'; statusEl.style.color = 'var(--red)'; }
|
|
20280
20392
|
}
|
|
@@ -20299,8 +20411,11 @@ async function saveAssistantPreferences() {
|
|
|
20299
20411
|
async function removeSetting(key) {
|
|
20300
20412
|
if (!confirm('Remove ' + key + ' from .env?')) return;
|
|
20301
20413
|
try {
|
|
20302
|
-
await apiDelete('/api/settings/' + encodeURIComponent(key));
|
|
20414
|
+
var result = await apiDelete('/api/settings/' + encodeURIComponent(key));
|
|
20303
20415
|
toast(key + ' removed', 'success');
|
|
20416
|
+
if (result && result.ok && settingRequiresDaemonRestart(key)) {
|
|
20417
|
+
markRestartRequired(key + ' was removed. Restart Clementine so running workers stop using the old value.');
|
|
20418
|
+
}
|
|
20304
20419
|
refreshSettings();
|
|
20305
20420
|
} catch(e) { toast('Failed: ' + e, 'error'); }
|
|
20306
20421
|
}
|
|
@@ -20330,8 +20445,11 @@ async function addCustomEnv() {
|
|
|
20330
20445
|
if (!key || !value) { toast('Both key and value are required', 'error'); return; }
|
|
20331
20446
|
if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { toast('Invalid key format — use UPPER_SNAKE_CASE', 'error'); return; }
|
|
20332
20447
|
try {
|
|
20333
|
-
await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20448
|
+
var result = await apiJson('PUT', '/api/settings/' + encodeURIComponent(key), { value: value });
|
|
20334
20449
|
toast(key + ' added', 'success');
|
|
20450
|
+
if (result && result.ok && settingRequiresDaemonRestart(key)) {
|
|
20451
|
+
markRestartRequired(key + ' was added. Restart Clementine so the daemon and channel workers can use it.');
|
|
20452
|
+
}
|
|
20335
20453
|
keyInput.value = '';
|
|
20336
20454
|
valInput.value = '';
|
|
20337
20455
|
refreshSettings();
|
|
@@ -27701,6 +27819,7 @@ async function refreshSalesforce() {
|
|
|
27701
27819
|
|
|
27702
27820
|
// ── Initial load — single batch request instead of 12+ parallel fetches ──
|
|
27703
27821
|
(async function initDashboard() {
|
|
27822
|
+
renderRestartRequiredBanner();
|
|
27704
27823
|
try {
|
|
27705
27824
|
var r = await apiFetch('/api/init');
|
|
27706
27825
|
var d = await r.json();
|
|
@@ -27772,6 +27891,7 @@ try {
|
|
|
27772
27891
|
if (currentPage === 'home') refreshSessions();
|
|
27773
27892
|
}
|
|
27774
27893
|
if (evt.type === 'daemon_restarted') {
|
|
27894
|
+
clearRestartRequired();
|
|
27775
27895
|
toast('Daemon restarted \u2014 refreshing data...', 'info');
|
|
27776
27896
|
setTimeout(function() { refreshAll(); }, 1500);
|
|
27777
27897
|
}
|
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
|
};
|