clementine-agent 1.18.13 → 1.18.15

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 CHANGED
@@ -328,8 +328,9 @@ For spend/context tuning, `clementine budgets` gives a safer shortcut:
328
328
 
329
329
  ```bash
330
330
  clementine budgets # show chat/cron/heartbeat caps and 1M context state
331
- clementine budgets safe # lower background budgets and disable Claude 1M context
332
- clementine budgets 1m on # enable 1M context for eligible accounts / Extra Usage
331
+ clementine budgets safe # lower background budgets and force standard 200K context
332
+ clementine budgets 1m auto # allow included Opus 1M, keep Sonnet on 200K
333
+ clementine budgets 1m on # force 1M context for Extra Usage/API users
333
334
  clementine budgets 1m off # disable 1M context for maximum compatibility
334
335
  clementine budgets set chat 10 # raise one budget cap
335
336
  ```
@@ -342,7 +343,8 @@ clementine budgets set chat 10 # raise one budget cap
342
343
  | `BUDGET_CRON_T1_USD` | `0.75` | Max spend per tier-1 cron job |
343
344
  | `BUDGET_CRON_T2_USD` | `1.50` | Max spend per tier-2 cron job |
344
345
  | `BUDGET_HEARTBEAT_USD` | `0.25` | Max spend per heartbeat tick |
345
- | `CLAUDE_CODE_DISABLE_1M_CONTEXT` | `true` | `true`/`1` keeps Claude Code on 200K context unless the user explicitly enables 1M |
346
+ | `CLEMENTINE_1M_CONTEXT_MODE` | `auto` | `auto` allows included Opus 1M on Max/Team/Enterprise while keeping Sonnet on 200K; `off` forces 200K; `on` forces 1M |
347
+ | `CLAUDE_CODE_DISABLE_1M_CONTEXT` | legacy | Backward-compatible Claude Code switch; `budgets safe` writes `1`, `budgets 1m auto` removes it |
346
348
  | `DEFAULT_MODEL_TIER` | `sonnet` | Default model: `haiku` / `sonnet` / `opus` |
347
349
  | `HEARTBEAT_INTERVAL_MINUTES` | `30` | How often the agent auto-checks in |
348
350
  | `HEARTBEAT_ACTIVE_START` | `8` | First hour of the active window (0–23) |
@@ -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, 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, 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';
@@ -320,6 +320,8 @@ const MODEL_CONTEXT_WINDOWS = {
320
320
  'opus': 200_000,
321
321
  };
322
322
  function getContextWindow(model) {
323
+ if (usesOneMillionContext(model))
324
+ return 1_000_000;
323
325
  for (const [family, size] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
324
326
  if (model.includes(family))
325
327
  return size;
@@ -338,10 +340,6 @@ function resultInputTokens(result) {
338
340
  }
339
341
  return total;
340
342
  }
341
- function oneMillionContextDisabled() {
342
- const value = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
343
- return value === undefined || !/^(0|false|no)$/i.test(value);
344
- }
345
343
  export function looksLikeOneMillionContextError(value) {
346
344
  const text = String(value ?? '');
347
345
  return /extra usage.*1m context|1m context.*extra usage|context-1m/i.test(text);
@@ -537,12 +535,6 @@ function buildSafeEnv() {
537
535
  sanitized.ANTHROPIC_API_KEY = apiKeyVal;
538
536
  }
539
537
  // When all are absent: HOME lets the subprocess find Keychain OAuth automatically.
540
- // Preserve trusted Claude Code runtime flags set by config.ts. In
541
- // particular, CLAUDE_CODE_DISABLE_1M_CONTEXT defaults on so background
542
- // helper queries do not silently re-enable the 1M context beta.
543
- if (process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT !== undefined) {
544
- sanitized.CLAUDE_CODE_DISABLE_1M_CONTEXT = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
545
- }
546
538
  // Step 3: Add trusted markers AFTER sanitization
547
539
  sanitized.CLEMENTINE_HOME = BASE_DIR;
548
540
  return sanitized;
@@ -2148,7 +2140,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2148
2140
  && toolsDisabledForCall
2149
2141
  && turnPolicy?.retrievalTier === 'none'
2150
2142
  && turnPolicy.effort === 'low';
2151
- const resolvedModel = resolveModel(requestedModel) ?? (lightweightModelEligible ? MODELS.haiku : MODEL);
2143
+ const rawResolvedModel = resolveModel(requestedModel) ?? (lightweightModelEligible ? MODELS.haiku : MODEL);
2144
+ const resolvedModel = normalizeClaudeModelForOneMillionContext(rawResolvedModel);
2145
+ const oneMillionModeValue = currentOneMillionContextMode();
2146
+ const oneMillionDisableValue = claudeCodeDisableOneMillionForModel(resolvedModel);
2152
2147
  const modelRouteReason = model
2153
2148
  ? 'explicit'
2154
2149
  : profile?.model
@@ -2421,7 +2416,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2421
2416
  systemPrompt: fullSystemPrompt,
2422
2417
  model: resolvedModel,
2423
2418
  ...(fallback ? { fallbackModel: fallback } : {}),
2424
- ...(oneMillionContextDisabled() ? { betas: [] } : {}),
2419
+ ...(oneMillionDisableValue === '1' ? { betas: [] } : {}),
2425
2420
  permissionMode: effectivePermissionMode,
2426
2421
  allowDangerouslySkipPermissions: true,
2427
2422
  ...(sessionStore ? { sessionStore } : {}),
@@ -2459,6 +2454,10 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2459
2454
  CLEMENTINE_TEAM_AGENT: profile?.slug ?? 'clementine',
2460
2455
  CLEMENTINE_INTERACTION_SOURCE: sourceOverride ?? inferInteractionSource(sessionKey),
2461
2456
  CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
2457
+ CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
2458
+ ...(oneMillionDisableValue !== undefined
2459
+ ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: oneMillionDisableValue }
2460
+ : {}),
2462
2461
  },
2463
2462
  },
2464
2463
  ...externalMcpServers,
@@ -2472,14 +2471,21 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2472
2471
  // env only when the prompt/job mentions a connector-backed service.
2473
2472
  // Per-MCP-server env isolation still happens inside each mcpServers
2474
2473
  // entry; this only affects the Claude Code subprocess itself.
2475
- ...(shouldInheritClaudeEnv ? {} : {
2476
- env: {
2474
+ env: shouldInheritClaudeEnv
2475
+ ? {
2476
+ ...process.env,
2477
+ CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
2478
+ ...(oneMillionDisableValue !== undefined
2479
+ ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: oneMillionDisableValue }
2480
+ : {}),
2481
+ }
2482
+ : {
2477
2483
  ...SAFE_ENV,
2478
- ...(process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT !== undefined
2479
- ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT }
2484
+ CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
2485
+ ...(oneMillionDisableValue !== undefined
2486
+ ? { CLAUDE_CODE_DISABLE_1M_CONTEXT: oneMillionDisableValue }
2480
2487
  : {}),
2481
2488
  },
2482
- }),
2483
2489
  // Avoid ambient Claude Code user/project/local settings and plugins by
2484
2490
  // default. Those can silently attach hundreds of tools. Explicit MCP
2485
2491
  // servers above still work; "all integrations/full tool surface" keeps
@@ -3421,13 +3427,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3421
3427
  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.');
3422
3428
  }
3423
3429
  else if (looksLikeOneMillionContextError(errorText)) {
3430
+ process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
3424
3431
  process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
3425
3432
  if (sessionKey) {
3426
3433
  this.sessions.delete(sessionKey);
3427
3434
  this.exchangeCounts.set(sessionKey, 0);
3428
3435
  this._compactedSessions.delete(sessionKey);
3429
3436
  }
3430
- responseText = responseText || ("Claude rejected the 1M context beta for this account. I've disabled 1M context for this process and reset the session. To persist the fix across restarts, run `clementine config doctor --fix`, then `clementine restart`.");
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`.");
3431
3438
  }
3432
3439
  else if (lower.includes('rate') && lower.includes('limit')) {
3433
3440
  hitRateLimit = true;
@@ -3552,13 +3559,14 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3552
3559
  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.');
3553
3560
  }
3554
3561
  else if (looksLikeOneMillionContextError(e)) {
3562
+ process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
3555
3563
  process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
3556
3564
  if (sessionKey) {
3557
3565
  this.sessions.delete(sessionKey);
3558
3566
  this.exchangeCounts.set(sessionKey, 0);
3559
3567
  this._compactedSessions.delete(sessionKey);
3560
3568
  }
3561
- responseText = responseText || ("Claude rejected the 1M context beta for this account. I've disabled 1M context for this process and reset the session. To persist the fix across restarts, run `clementine config doctor --fix`, then `clementine restart`.");
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`.");
3562
3570
  }
3563
3571
  else if (errStr.includes('rate') && (errStr.includes('limit') || errStr.includes('rate_limit'))) {
3564
3572
  hitRateLimit = true;
@@ -4814,6 +4822,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
4814
4822
  throw new Error(errText);
4815
4823
  }
4816
4824
  if (looksLikeOneMillionContextError(errText)) {
4825
+ process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
4817
4826
  process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
4818
4827
  throw new Error(errText);
4819
4828
  }
@@ -5162,6 +5171,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5162
5171
  throw new Error(exitText);
5163
5172
  }
5164
5173
  if (looksLikeOneMillionContextError(exitText)) {
5174
+ process.env.CLEMENTINE_1M_CONTEXT_MODE = 'off';
5165
5175
  process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
5166
5176
  throw new Error(exitText);
5167
5177
  }
@@ -33,8 +33,13 @@ export async function* parseMarkdown(filePath) {
33
33
  mtime = statSync(filePath).mtime.toISOString();
34
34
  }
35
35
  catch { /* ignore */ }
36
+ const frontmatterExternalId = typeof parsed.data?.externalId === 'string' && parsed.data.externalId.trim()
37
+ ? parsed.data.externalId.trim()
38
+ : typeof parsed.data?.external_id === 'string' && parsed.data.external_id.trim()
39
+ ? parsed.data.external_id.trim()
40
+ : null;
36
41
  yield {
37
- externalId: `md-${hint}-${contentHash(body)}`,
42
+ externalId: frontmatterExternalId ?? `md-${hint}-${contentHash(body)}`,
38
43
  content: body,
39
44
  rawPayload: raw,
40
45
  metadata: {
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Each recipe is a blueprint for a one-click "auto-seed feed" that turns an
5
5
  * authenticated tool source (Claude Desktop connector, Composio toolkit, or
6
- * local MCP server) into a scheduled data feed that writes into the brain's
7
- * ingest folder.
6
+ * local MCP server) into a scheduled data feed that writes distilled notes
7
+ * into the brain's ingest folder.
8
8
  *
9
9
  * A feed materializes as:
10
10
  * 1. A CRON.md job entry with `managed: connector-feed` frontmatter
@@ -12,8 +12,8 @@
12
12
  *
13
13
  * The cron prompt tells the Claude Code agent to use the integration's MCP
14
14
  * tools to pull records, compare them with current memory when appropriate,
15
- * then call `brain_ingest_folder` to commit them — which writes markdown files
16
- * and runs the distillation pipeline in one step.
15
+ * then call `brain_ingest_folder` to commit them — which writes distilled
16
+ * markdown notes and indexes them in one step.
17
17
  *
18
18
  * Field syntax in prompt templates:
19
19
  * {{fieldKey}} — user-supplied value
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Each recipe is a blueprint for a one-click "auto-seed feed" that turns an
5
5
  * authenticated tool source (Claude Desktop connector, Composio toolkit, or
6
- * local MCP server) into a scheduled data feed that writes into the brain's
7
- * ingest folder.
6
+ * local MCP server) into a scheduled data feed that writes distilled notes
7
+ * into the brain's ingest folder.
8
8
  *
9
9
  * A feed materializes as:
10
10
  * 1. A CRON.md job entry with `managed: connector-feed` frontmatter
@@ -12,8 +12,8 @@
12
12
  *
13
13
  * The cron prompt tells the Claude Code agent to use the integration's MCP
14
14
  * tools to pull records, compare them with current memory when appropriate,
15
- * then call `brain_ingest_folder` to commit them — which writes markdown files
16
- * and runs the distillation pipeline in one step.
15
+ * then call `brain_ingest_folder` to commit them — which writes distilled
16
+ * markdown notes and indexes them in one step.
17
17
  *
18
18
  * Field syntax in prompt templates:
19
19
  * {{fieldKey}} — user-supplied value
@@ -33,16 +33,16 @@ const COMMIT_INSTRUCTIONS = `When you have the records collected, call the \`bra
33
33
  - \`slug\`: "{{slug}}"
34
34
  - \`records\`: an array of \`{title, externalId, content, metadata}\` objects (one per item). \`externalId\` should be the source provider's stable id so re-runs dedup. \`metadata\` can include any fields you want preserved (url, modifiedAt, author).
35
35
 
36
- That tool writes each record to \`{{targetFolder}}/\` and runs the brain's distillation pipeline. You do NOT need to use Write — brain_ingest_folder handles file creation. Finish by reporting a one-line summary like "Ingested N new records, M unchanged".
36
+ That tool runs the brain's distillation pipeline and writes the final notes to \`{{targetFolder}}/\`. You do NOT need to use Write — brain_ingest_folder handles note creation and indexing. Finish by reporting a one-line summary like "Ingested N new records, M unchanged".
37
37
 
38
38
  If the tool returns an error, include the error text in your summary.`;
39
- const MEMORY_DELTA_INSTRUCTIONS = `Before committing, call \`memory_recall\` for the feed slug/topic and use the returned chunks as the current memory state for this source. Keep records that are new, materially changed, or contain a new finding. Drop exact duplicates and rows that add no useful information. The ingestion pipeline will write markdown and embeddings; do not call \`memory_write\` for these feed records.`;
39
+ const MEMORY_DELTA_INSTRUCTIONS = `Before committing, call \`memory_recall\` for the feed slug/topic and use the returned chunks as the current memory state for this source. Keep records that are new, materially changed, or contain a new finding. Drop exact duplicates and rows that add no useful information. The ingestion pipeline will write markdown, chunk it, and index it for recall; do not call \`memory_write\` for these feed records.`;
40
40
  // ── Recipes ────────────────────────────────────────────────────────────
41
41
  export const RECIPES = [
42
42
  {
43
43
  id: 'tool-backed-memory-seed',
44
- label: 'Any tool: call and seed memory',
45
- description: 'Call a selected tool from this connector, compare results with current memory, and ingest new or changed findings.',
44
+ label: 'Seed memory from this tool',
45
+ description: 'Pick one tool, fetch records from it, compare them with current memory, and save only new or changed findings.',
46
46
  icon: '🔌',
47
47
  integration: '*',
48
48
  requiredTools: [],
@@ -52,36 +52,36 @@ export const RECIPES = [
52
52
  label: 'Memory topic',
53
53
  placeholder: 'customers, calls, leads, deals, meetings...',
54
54
  required: true,
55
- help: 'Used for recall, deduping, and the generated feed slug.',
55
+ help: 'Used to search current memory and name this feed.',
56
56
  },
57
57
  {
58
58
  key: 'toolName',
59
59
  label: 'Tool to call',
60
60
  required: true,
61
- help: 'Pick the exact tool this feed should call when it runs.',
61
+ help: 'Pick the exact tool this feed should call each time it runs.',
62
62
  },
63
63
  {
64
64
  key: 'callGoal',
65
- label: 'What to pull',
65
+ label: 'What should Clementine fetch?',
66
66
  placeholder: 'Fetch updated HubSpot contacts modified since the last run...',
67
67
  required: true,
68
68
  help: 'Describe the records to fetch, filters to apply, and any pagination bounds.',
69
69
  },
70
70
  {
71
71
  key: 'variablesJson',
72
- label: 'Variables JSON',
72
+ label: 'Tool variables (JSON)',
73
73
  placeholder: '{"listId":"123","limit":100,"updatedAfter":"last_run"}',
74
- help: 'Optional arguments, IDs, ranges, filters, or query variables the tool should use.',
74
+ help: 'Optional. Use {} if the tool needs no arguments.',
75
75
  },
76
76
  {
77
77
  key: 'recordStrategy',
78
- label: 'Record strategy',
78
+ label: 'How to save each result',
79
79
  placeholder: 'One record per contact. Use email as stable id. Summarize lifecycle stage, owner, last activity, and new changes.',
80
- help: 'Tell the agent how to convert the tool output into memory records.',
80
+ help: 'Tell Clementine what counts as one memory record and which field is the stable id.',
81
81
  },
82
82
  {
83
83
  key: 'slug',
84
- label: 'Slug override',
84
+ label: 'Memory bucket name (optional)',
85
85
  placeholder: 'hubspot-contacts',
86
86
  help: 'Optional. Leave blank to derive one from the connector and topic.',
87
87
  },
@@ -111,16 +111,16 @@ Tool source:
111
111
 
112
112
  Goal: ${v.callGoal || `Call ${v.toolName} and ingest useful returned data into memory.`}
113
113
 
114
- Variables JSON:
114
+ Tool variables JSON:
115
115
  \`\`\`json
116
116
  ${(v.variablesJson || '{}').trim() || '{}'}
117
117
  \`\`\`
118
118
 
119
- Record strategy:
119
+ How to save each result:
120
120
  ${v.recordStrategy || 'Convert the tool response into one memory record per returned entity or event. Use the provider stable id when available; otherwise use a deterministic hash of the source, topic, and meaningful record key.'}
121
121
 
122
122
  Steps:
123
- 1. Call exactly this selected tool: \`${v.toolName}\`. Use the Variables JSON and the Goal above as the tool-call inputs. If the tool schema needs differently named arguments, map the provided variables to that schema. Do not switch to a different external tool unless this tool returns a clear instruction that another tool is required to read the selected records.
123
+ 1. Call exactly this selected tool: \`${v.toolName}\`. Use the Tool variables JSON and the Goal above as the tool-call inputs. If the tool schema needs differently named arguments, map the provided variables to that schema. Do not switch to a different external tool unless this tool returns a clear instruction that another tool is required to read the selected records.
124
124
  2. If the tool supports pagination or modified-since filters, prefer new/updated records and stop after ${limit} records. If no modified-since filter is available, fetch the most relevant ${limit} records.
125
125
  3. Normalize the tool result into candidate records. Preserve stable ids, URLs, timestamps, owners/authors, status fields, and provider metadata. Skip empty or purely administrative records.
126
126
  4. ${MEMORY_DELTA_INSTRUCTIONS}