clementine-agent 1.18.176 → 1.18.178

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.
@@ -2343,6 +2343,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2343
2343
  ...process.env,
2344
2344
  CLEMENTINE_HOME: BASE_DIR,
2345
2345
  CLEMENTINE_TEAM_AGENT: profile?.slug ?? 'clementine',
2346
+ ...(sessionKey ? { CLEMENTINE_SESSION_KEY: sessionKey } : {}),
2346
2347
  CLEMENTINE_INTERACTION_SOURCE: sourceOverride ?? inferInteractionSource(sessionKey),
2347
2348
  CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
2348
2349
  CLEMENTINE_1M_CONTEXT_MODE: oneMillionModeValue,
@@ -1,9 +1,26 @@
1
+ /**
2
+ * Explicit background-intent detector.
3
+ *
4
+ * Returns a recommendation ONLY when the user explicitly asks for background
5
+ * / autonomous / overnight execution. We deliberately do not classify "this
6
+ * looks complex" anymore — chat now stays in the live SDK loop, with
7
+ * automatic compaction and inline subagent delegation (Agent → planner /
8
+ * researcher / etc.) for context isolation, just like Claude Code itself.
9
+ * Big work that genuinely blows past the SDK's auto-compact is caught by the
10
+ * gateway's overflow → retry → promote-to-background fallback, which is the
11
+ * *real* escape hatch instead of a regex pre-classifier.
12
+ *
13
+ * The narrow detection here is what lets a user say "go research this
14
+ * overnight" and have it actually queue as a durable background task.
15
+ */
1
16
  export interface ComplexTaskRecommendation {
2
- score: number;
3
17
  reasons: string[];
4
18
  suggestedMaxMinutes: number;
5
19
  plan: string[];
6
- queueImmediately: boolean;
20
+ /** Always true when this function returns a recommendation — the only
21
+ * trigger is the user explicitly asking for background execution. Kept
22
+ * on the type for back-compat with the post-overflow rescue path. */
23
+ queueImmediately: true;
7
24
  }
8
25
  export declare function detectComplexTaskForBackground(text: string): ComplexTaskRecommendation | null;
9
26
  //# sourceMappingURL=complex-task-detector.d.ts.map
@@ -1,32 +1,18 @@
1
+ // Skill authoring is an interactive build-with-the-user flow; never auto-queue.
1
2
  const SKILL_AUTHORING_RE = /\b(create|make|build|draft|write|teach|save|update)\b.{0,40}\b(skill|SKILL\.md)\b|\bskill[- ]creator\b/i;
2
- const EXPLICIT_BACKGROUND_RE = /\b(background|deep mode|keep working|don't stop|dont stop|autonomous|long[- ]running|run overnight|take your time)\b/i;
3
- const COMPLEX_WORK_RE = /\b(audit|research|analy[sz]e|review|scrape|crawl|extract|enrich|compile|compare|verify|cross[- ]check|triage|reconcile|draft|generate|update|sync|report back|write back)\b/i;
4
- const BATCH_RE = /\b(all|every|each|bulk|batch|list of|contacts?|leads?|accounts?|tasks?|tickets?|records?|rows?|pages?|repos?|projects?)\b/i;
5
- const SIDE_EFFECT_RE = /\b(update|write|create|draft|send|post|comment|reply|upload|append|sync|mark|close|move)\b/i;
6
- const MULTI_STEP_RE = /\b(and then|then|after that|finally|from .* to |against .* and |across|compile .* into|check .* then)\b/i;
3
+ // The ONLY trigger. Matches "in the background", "overnight", "keep working",
4
+ // "don't stop", "autonomous", "long-running", "take your time", "deep mode".
5
+ const EXPLICIT_BACKGROUND_RE = /\b(background|deep mode|keep working|don't stop|dont stop|autonomous|long[- ]running|run overnight|overnight|take your time)\b/i;
6
+ // Light scope hints used only for the duration estimate + plan text. None of
7
+ // these alter whether the function fires they shape the recommendation
8
+ // once the explicit-intent gate has already opened.
9
+ const BATCH_RE = /\b(all|every|each|bulk|batch|list of|contacts?|leads?|accounts?|tasks?|tickets?|records?|rows?|pages?|repos?|projects?|firms?|metros?|prospects?)\b/i;
10
+ const SIDE_EFFECT_RE = /\b(update|write|create|draft|send|post|comment|reply|upload|append|sync|mark|close|move|deploy|host|publish)\b/i;
7
11
  const SYSTEM_KEYWORDS = [
8
- 'asana',
9
- 'salesforce',
10
- 'google sheet',
11
- 'google sheets',
12
- 'sheet',
13
- 'sheets',
14
- 'dataforseo',
15
- 'hubspot',
16
- 'notion',
17
- 'github',
18
- 'gmail',
19
- 'outlook',
20
- 'slack',
21
- 'discord',
22
- 'website',
23
- 'websites',
24
- 'crm',
25
- 'spreadsheet',
26
- 'csv',
27
- 'airtable',
28
- 'linear',
29
- 'jira',
12
+ 'asana', 'salesforce', 'google sheet', 'google sheets', 'sheet', 'sheets',
13
+ 'dataforseo', 'hubspot', 'notion', 'github', 'gmail', 'outlook', 'slack',
14
+ 'discord', 'website', 'websites', 'crm', 'spreadsheet', 'csv', 'netlify',
15
+ 'vercel', 'airtable', 'linear', 'jira',
30
16
  ];
31
17
  function countSystemMentions(text) {
32
18
  const lower = text.toLowerCase();
@@ -37,10 +23,10 @@ function countSystemMentions(text) {
37
23
  }
38
24
  return count;
39
25
  }
40
- function estimatedMinutes(score, systemCount) {
41
- if (score >= 8 || systemCount >= 4)
26
+ function estimatedMinutes(systemCount, textLength) {
27
+ if (systemCount >= 4 || textLength > 800)
42
28
  return 90;
43
- if (score >= 6 || systemCount >= 3)
29
+ if (systemCount >= 2 || textLength > 400)
44
30
  return 60;
45
31
  return 30;
46
32
  }
@@ -72,47 +58,17 @@ export function detectComplexTaskForBackground(text) {
72
58
  return null;
73
59
  if (SKILL_AUTHORING_RE.test(trimmed))
74
60
  return null;
75
- const systemCount = countSystemMentions(trimmed);
76
- const reasons = [];
77
- let score = 0;
78
- if (EXPLICIT_BACKGROUND_RE.test(trimmed)) {
79
- score += 4;
80
- reasons.push('explicit background/deep-work wording');
81
- }
82
- if (COMPLEX_WORK_RE.test(trimmed)) {
83
- score += 2;
84
- reasons.push('multi-step work verb');
85
- }
86
- if (BATCH_RE.test(trimmed)) {
87
- score += 2;
88
- reasons.push('batch or many-record scope');
89
- }
90
- if (SIDE_EFFECT_RE.test(trimmed)) {
91
- score += 1;
92
- reasons.push('write/draft/update side effects');
93
- }
94
- if (MULTI_STEP_RE.test(trimmed)) {
95
- score += 1;
96
- reasons.push('multi-step sequencing');
97
- }
98
- if (systemCount >= 2) {
99
- score += Math.min(4, systemCount);
100
- reasons.push(`${systemCount} named systems or data surfaces`);
101
- }
102
- if (trimmed.length > 450) {
103
- score += 1;
104
- reasons.push('long detailed request');
105
- }
106
- const queueImmediately = EXPLICIT_BACKGROUND_RE.test(trimmed) && score >= 5;
107
- const shouldOffer = queueImmediately || score >= 5 || (systemCount >= 2 && (BATCH_RE.test(trimmed) || SIDE_EFFECT_RE.test(trimmed)));
108
- if (!shouldOffer)
61
+ if (!EXPLICIT_BACKGROUND_RE.test(trimmed))
109
62
  return null;
63
+ const systemCount = countSystemMentions(trimmed);
64
+ const reasons = ['explicit background/deep-work wording'];
65
+ if (systemCount >= 2)
66
+ reasons.push(`${systemCount} named systems`);
110
67
  return {
111
- score,
112
68
  reasons,
113
- suggestedMaxMinutes: estimatedMinutes(score, systemCount),
69
+ suggestedMaxMinutes: estimatedMinutes(systemCount, trimmed.length),
114
70
  plan: buildPlan(trimmed, systemCount),
115
- queueImmediately,
71
+ queueImmediately: true,
116
72
  };
117
73
  }
118
74
  //# sourceMappingURL=complex-task-detector.js.map
@@ -26,6 +26,31 @@ const CRON_PROGRESS_PENDING_MAX_ITEMS = 20;
26
26
  const CRON_PROGRESS_NOTES_MAX_CHARS = 2000;
27
27
  const logger = pino({ name: 'clementine.run-agent-cron' });
28
28
  const CRON_CONTEXT_ITEM_MAX = 80;
29
+ const CLEMENTINE_TOOLS_SERVER = `${(process.env.ASSISTANT_NAME ?? 'Clementine').toLowerCase()}-tools`;
30
+ const BACKGROUND_TASK_WORKER_NAME = 'background-task-worker';
31
+ const DEFAULT_BACKGROUND_WORKER_TOOLS = [
32
+ 'Agent',
33
+ 'Read',
34
+ 'Write',
35
+ 'Edit',
36
+ 'Glob',
37
+ 'Grep',
38
+ 'Bash',
39
+ 'WebSearch',
40
+ 'WebFetch',
41
+ 'TodoWrite',
42
+ ];
43
+ const BACKGROUND_TASK_WORKER_PROMPT = [
44
+ 'You are Clementine\'s background task worker for long-running user requests.',
45
+ '',
46
+ 'Run the assigned task to completion using the available tools. Keep raw API responses, scraped pages, and large file contents out of the final answer; extract the fields you need and continue.',
47
+ '',
48
+ 'Use TodoWrite for multi-step state. Process batch work in bounded chunks, checkpoint meaningful progress in durable artifacts when useful, and avoid repeating the same expensive read or tool call.',
49
+ '',
50
+ 'If credentials, missing scope, human approval, or an irreversible action blocks completion, stop with one concise blocker/question and the exact next action needed. Do not keep retrying blindly.',
51
+ '',
52
+ 'Return only the final user-facing result: links or changed locations, counts, skipped/error records, and the next recommended action.',
53
+ ].join('\n');
29
54
  /** Total number of skill blocks injected into a cron prompt — pinned + auto. */
30
55
  const MAX_INJECTED_SKILLS = 4;
31
56
  /**
@@ -172,6 +197,27 @@ function capContextBlock(s, max) {
172
197
  return '';
173
198
  return s.length <= max ? s : s.slice(0, max - 3) + '...';
174
199
  }
200
+ function backgroundWorkerTools(effectiveAllowedTools, mcpServersApplied) {
201
+ if (effectiveAllowedTools)
202
+ return [...new Set(['Agent', ...effectiveAllowedTools])];
203
+ const mcpWildcards = [CLEMENTINE_TOOLS_SERVER, ...mcpServersApplied]
204
+ .filter(Boolean)
205
+ .map((server) => `mcp__${server}__*`);
206
+ return [...new Set([...DEFAULT_BACKGROUND_WORKER_TOOLS, ...mcpWildcards])];
207
+ }
208
+ function buildBackgroundTaskWorker(tools, model, maxTurns) {
209
+ return {
210
+ description: [
211
+ 'Use for background tasks queued from chat, especially multi-step work, batch data collection, project builds, deployments, and external-system writebacks.',
212
+ 'This agent owns the heavy tool loop so the parent task context stays small.',
213
+ ].join(' '),
214
+ prompt: BACKGROUND_TASK_WORKER_PROMPT,
215
+ tools,
216
+ ...(model ? { model } : { model: 'sonnet' }),
217
+ effort: 'medium',
218
+ maxTurns: typeof maxTurns === 'number' && maxTurns > 0 ? maxTurns : 40,
219
+ };
220
+ }
175
221
  /**
176
222
  * Build the previous-progress block from the cron progress JSON file.
177
223
  * Lets the agent continue where the prior run left off without re-doing
@@ -749,10 +795,18 @@ export async function runAgentCron(opts) {
749
795
  skillsMissing: plan.skillsMissing.length,
750
796
  trickAllowedTools: effectiveAllowedTools?.length,
751
797
  trickAllowedMcp: opts.allowedMcpServers?.length,
798
+ forcedBackgroundWorker: opts.jobName.startsWith('bg:'),
752
799
  widenedFromSkills: plan.widenedFromSkills,
753
800
  ...(promptOversized ? { warning: 'prompt > 50KB; risk of "Prompt is too long" failure' } : {}),
754
801
  }, 'runAgentCron: dispatching to runAgent');
755
802
  const startedAt = Date.now();
803
+ const forceBackgroundWorker = opts.jobName.startsWith('bg:');
804
+ const workerTools = forceBackgroundWorker
805
+ ? backgroundWorkerTools(effectiveAllowedTools, mcpServersApplied)
806
+ : [];
807
+ const workerDef = forceBackgroundWorker
808
+ ? buildBackgroundTaskWorker(workerTools, opts.model, opts.maxTurns)
809
+ : null;
756
810
  const result = await runAgent(builtPrompt, {
757
811
  sessionKey: `cron:${opts.jobName}`,
758
812
  source: 'cron',
@@ -762,9 +816,16 @@ export async function runAgentCron(opts) {
762
816
  model: opts.model,
763
817
  effort,
764
818
  ...(maxBudget !== undefined ? { maxBudgetUsd: maxBudget } : {}),
765
- maxTurns: opts.maxTurns,
819
+ maxTurns: forceBackgroundWorker ? 5 : opts.maxTurns,
766
820
  abortSignal: opts.abortSignal,
767
- ...(effectiveAllowedTools ? { allowedTools: effectiveAllowedTools } : {}),
821
+ ...(forceBackgroundWorker
822
+ ? {
823
+ allowedTools: ['Agent'],
824
+ permissionTools: workerTools,
825
+ forceSubagent: BACKGROUND_TASK_WORKER_NAME,
826
+ agents: { [BACKGROUND_TASK_WORKER_NAME]: workerDef },
827
+ }
828
+ : (effectiveAllowedTools ? { allowedTools: effectiveAllowedTools } : {})),
768
829
  extraMcpServers: mcpServerMap,
769
830
  // 1.18.121 — pipe the merged addDirs+pinned-skill folders to the SDK
770
831
  // so a skill's bundled scripts/templates are reachable via Bash/Read
@@ -16,6 +16,7 @@
16
16
  * long-task preflight, NO mode=unleashed wrapper.
17
17
  */
18
18
  import { type AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
19
+ import type { TerminalReason } from '../types.js';
19
20
  /** Read the latest MCP status snapshot. Safe to call from any module. */
20
21
  export declare function getLatestMcpStatusSnapshot(): {
21
22
  servers: Array<{
@@ -26,6 +27,10 @@ export declare function getLatestMcpStatusSnapshot(): {
26
27
  };
27
28
  /** Write a fresh snapshot. Called from system/init handlers. */
28
29
  export declare function recordMcpStatusFromSystemInit(rawMcpServers: unknown): void;
30
+ /** True when the SDK emits an internal context-pressure diagnostic as an
31
+ * assistant text block. These are operational warnings, not useful user
32
+ * output, and they can appear while the run is still recovering/continuing. */
33
+ export declare function isSdkContextDiagnosticText(text: string): boolean;
29
34
  /** Drop one server from the cache so the next query repopulates it. */
30
35
  export declare function invalidateMcpStatusEntry(name: string): {
31
36
  cleared: boolean;
@@ -133,6 +138,8 @@ export interface RunAgentResult {
133
138
  sessionId: string;
134
139
  /** Final stop reason from the SDK (success, error_max_turns, error_max_budget_usd, etc). */
135
140
  subtype: string;
141
+ /** Precise SDK loop terminal reason, when available. */
142
+ terminalReason?: TerminalReason;
136
143
  /** Token usage breakdown (input, output, cache). */
137
144
  usage?: {
138
145
  input_tokens?: number;
@@ -75,6 +75,16 @@ function truncateForLog(value, maxBytes) {
75
75
  return { _unstringifiable: true };
76
76
  }
77
77
  }
78
+ /** True when the SDK emits an internal context-pressure diagnostic as an
79
+ * assistant text block. These are operational warnings, not useful user
80
+ * output, and they can appear while the run is still recovering/continuing. */
81
+ export function isSdkContextDiagnosticText(text) {
82
+ const t = text.trim();
83
+ if (!t)
84
+ return false;
85
+ return /^Autocompact is thrashing:\s*the context refilled to the limit/i.test(t)
86
+ || /^rapid_refill_breaker\b/i.test(t);
87
+ }
78
88
  /** Drop one server from the cache so the next query repopulates it. */
79
89
  export function invalidateMcpStatusEntry(name) {
80
90
  const before = _lastMcpStatusSnapshot.servers.length;
@@ -287,6 +297,7 @@ export async function runAgent(prompt, opts) {
287
297
  ...subprocessEnv,
288
298
  CLEMENTINE_HOME: BASE_DIR,
289
299
  ...(opts.profile?.slug ? { CLEMENTINE_TEAM_AGENT: opts.profile.slug } : {}),
300
+ CLEMENTINE_SESSION_KEY: opts.sessionKey,
290
301
  CLEMENTINE_INTERACTION_SOURCE: interactionSourceForSession(opts.sessionKey, source),
291
302
  CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
292
303
  },
@@ -482,6 +493,7 @@ export async function runAgent(prompt, opts) {
482
493
  let totalCostUsd = 0;
483
494
  let numTurns = 0;
484
495
  let subtype = 'unknown';
496
+ let terminalReason;
485
497
  let usage;
486
498
  const stream = query({ prompt: effectivePrompt, options: sdkOptions });
487
499
  try {
@@ -537,6 +549,15 @@ export async function runAgent(prompt, opts) {
537
549
  const blocks = (am.message?.content ?? []);
538
550
  for (const block of blocks) {
539
551
  if (block.type === 'text' && typeof block.text === 'string') {
552
+ if (isSdkContextDiagnosticText(block.text)) {
553
+ logger.warn({
554
+ sessionKey: opts.sessionKey,
555
+ source,
556
+ subtype,
557
+ preview: block.text.slice(0, 240),
558
+ }, 'runAgent: suppressed SDK context diagnostic text');
559
+ continue;
560
+ }
540
561
  finalText += block.text;
541
562
  // PRD Phase 4a / 1.18.85: llm_text Event. Truncate at 8KB to keep
542
563
  // the JSONL light — full text is reachable via the SDK transcript.
@@ -610,6 +631,7 @@ export async function runAgent(prompt, opts) {
610
631
  const result = message;
611
632
  sessionId = sessionId || (result.session_id ?? '');
612
633
  subtype = result.subtype ?? 'unknown';
634
+ terminalReason = result.terminal_reason;
613
635
  numTurns = result.num_turns ?? numTurns;
614
636
  totalCostUsd = result.total_cost_usd ?? 0;
615
637
  const u = result.usage;
@@ -628,7 +650,7 @@ export async function runAgent(prompt, opts) {
628
650
  ts: new Date().toISOString(),
629
651
  sessionId,
630
652
  costUsd: totalCostUsd,
631
- stopReason: subtype,
653
+ stopReason: terminalReason && terminalReason !== 'completed' ? `${subtype}:${terminalReason}` : subtype,
632
654
  });
633
655
  // PRD Phase 4d / 1.18.101: unregister from the hook-session registry.
634
656
  // Late-arriving hook events for this sessionId silently drop after this.
@@ -757,6 +779,7 @@ export async function runAgent(prompt, opts) {
757
779
  numTurns,
758
780
  sessionId,
759
781
  subtype,
782
+ ...(terminalReason ? { terminalReason } : {}),
760
783
  ...(usage ? { usage } : {}),
761
784
  runId,
762
785
  permissionMode: toolPolicy.permissionMode,
@@ -13,6 +13,16 @@ import type { NotificationDispatcher } from './notifications.js';
13
13
  import { type ProactiveNotificationInput } from './notification-context.js';
14
14
  import { type ToolsetName } from '../agent/toolsets.js';
15
15
  export { isLiveUnleashedStatus } from './unleashed-status.js';
16
+ export declare function buildContextOverflowRetryPrompt(opts: {
17
+ chatPrompt: string;
18
+ turnContextPrefix?: string;
19
+ project?: ProjectMeta | null;
20
+ }): string;
21
+ export declare function runAgentResultIndicatesContextOverflow(result: {
22
+ subtype?: string;
23
+ terminalReason?: string;
24
+ text?: string;
25
+ }): boolean;
16
26
  export type ChatErrorKind = 'rate_limit' | 'one_million_context' | 'context_overflow' | 'auth' | 'billing' | 'transient' | 'unknown';
17
27
  export declare function classifyChatError(err: unknown): ChatErrorKind;
18
28
  /** Detect auth-like errors in response text that the SDK returned as "successful" results. */
@@ -59,13 +69,14 @@ export declare class Gateway {
59
69
  private extractBackgroundTaskId;
60
70
  private makeBackgroundOfferId;
61
71
  private backgroundAgentForSession;
72
+ private buildBackgroundTaskPrompt;
62
73
  private pruneExpiredBackgroundOffers;
63
74
  private latestBackgroundOfferForSession;
64
75
  private getBackgroundOfferForSession;
65
76
  private createBackgroundOffer;
66
77
  private queueBackgroundOffer;
67
78
  private formatBackgroundQueuedResponse;
68
- private formatBackgroundOfferResponse;
79
+ private queueBackgroundTaskAfterContextOverflow;
69
80
  acceptBackgroundOffer(sessionKey: string, id: string): {
70
81
  ok: boolean;
71
82
  response: string;
@@ -49,6 +49,8 @@ const CHAT_TIMEOUT_MS = 10 * 60 * 1000;
49
49
  * Safety net so no session runs forever, even if active.
50
50
  * Primary guardrail is cost budget (maxBudgetUsd), not this timer. */
51
51
  const CHAT_MAX_WALL_MS = 30 * 60 * 1000;
52
+ const CHAT_CONTEXT_RETRY_CONTEXT_MAX_CHARS = 6_000;
53
+ const CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS = 16_000;
52
54
  const BACKGROUND_TASK_ID_RE = /\bbg-[a-z0-9]+-[a-f0-9]{6}\b/i;
53
55
  function collectRunToolNames(runId) {
54
56
  if (!runId)
@@ -74,6 +76,37 @@ function compactToolNames(names) {
74
76
  }
75
77
  return out;
76
78
  }
79
+ function trimContextRecoveryText(text, maxChars) {
80
+ if (!text || text.length <= maxChars)
81
+ return text;
82
+ return `${text.slice(0, maxChars - 80).trimEnd()}\n\n[context recovery: trimmed oversized carry-over context]`;
83
+ }
84
+ export function buildContextOverflowRetryPrompt(opts) {
85
+ const parts = [
86
+ '[Context recovery: the previous SDK session was too large, so this is a fresh session. Continue with the current user request. Do not ask the user to resend it.]',
87
+ ];
88
+ if (opts.project?.path) {
89
+ const description = opts.project.description ? ` (${opts.project.description})` : '';
90
+ parts.push(`[Active project: ${opts.project.path}${description}]`);
91
+ }
92
+ const compactContext = trimContextRecoveryText((opts.turnContextPrefix ?? '').trim(), CHAT_CONTEXT_RETRY_CONTEXT_MAX_CHARS);
93
+ if (compactContext) {
94
+ parts.push(compactContext);
95
+ }
96
+ parts.push(opts.chatPrompt);
97
+ return parts.filter(Boolean).join('\n\n');
98
+ }
99
+ export function runAgentResultIndicatesContextOverflow(result) {
100
+ const terminalReason = (result.terminalReason ?? '').trim();
101
+ if (terminalReason && classifyChatError(terminalReason) === 'context_overflow')
102
+ return true;
103
+ const subtype = (result.subtype ?? '').trim();
104
+ if (subtype && subtype !== 'success' && classifyChatError(subtype) === 'context_overflow')
105
+ return true;
106
+ const text = (result.text ?? '').trim();
107
+ return /^Autocompact is thrashing:\s*the context refilled to the limit/i.test(text)
108
+ || /^rapid_refill_breaker\b/i.test(text);
109
+ }
77
110
  export function classifyChatError(err) {
78
111
  const msg = String(err);
79
112
  if (isCreditBalanceError(msg))
@@ -82,7 +115,7 @@ export function classifyChatError(err) {
82
115
  return 'rate_limit';
83
116
  if (looksLikeClaudeOneMillionContextError(msg))
84
117
  return 'one_million_context';
85
- if (/context.?length|token.?limit|maximum.?context|prompt.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
118
+ if (/context.?length|token.?limit|maximum.?context|prompt(?:\s+is)?.?too.?long|input.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
86
119
  return 'context_overflow';
87
120
  if (/\b401\b|\b403\b|auth|forbidden|invalid.?api.?key|permission|does not have access|please run \/login/i.test(msg))
88
121
  return 'auth';
@@ -277,6 +310,18 @@ export class Gateway {
277
310
  backgroundAgentForSession(sessionKey) {
278
311
  return this._agentSlugFromSessionKey(sessionKey) ?? this.getSessionProfile(sessionKey) ?? 'clementine';
279
312
  }
313
+ buildBackgroundTaskPrompt(sessionKey, prompt) {
314
+ const sess = this.sessions.get(sessionKey);
315
+ const parts = [
316
+ '[Background task from chat: run this in a fresh task execution. Do not rely on the live chat transcript being resumed; use the self-contained request below.]',
317
+ ];
318
+ if (sess?.project?.path) {
319
+ const description = sess.project.description ? ` (${sess.project.description})` : '';
320
+ parts.push(`[Active project: ${sess.project.path}${description}]`);
321
+ }
322
+ parts.push(prompt.trim());
323
+ return parts.filter(Boolean).join('\n\n');
324
+ }
280
325
  pruneExpiredBackgroundOffers() {
281
326
  const now = Date.now();
282
327
  for (const [id, offer] of this.pendingBackgroundOffers) {
@@ -305,6 +350,7 @@ export class Gateway {
305
350
  sessionKey,
306
351
  fromAgent: this.backgroundAgentForSession(sessionKey),
307
352
  prompt,
353
+ taskPrompt: this.buildBackgroundTaskPrompt(sessionKey, prompt),
308
354
  recommendation,
309
355
  createdAt: Date.now(),
310
356
  expiresAt: Date.now() + 30 * 60_000,
@@ -315,7 +361,7 @@ export class Gateway {
315
361
  queueBackgroundOffer(offer) {
316
362
  const task = createBackgroundTask({
317
363
  fromAgent: offer.fromAgent,
318
- prompt: offer.prompt,
364
+ prompt: offer.taskPrompt,
319
365
  maxMinutes: offer.recommendation.suggestedMaxMinutes,
320
366
  sessionKey: offer.sessionKey,
321
367
  });
@@ -333,25 +379,37 @@ export class Gateway {
333
379
  return [
334
380
  `Queued background task **${task.id}**.`,
335
381
  '',
336
- `It will run as **${task.fromAgent}** with a ${task.maxMinutes} minute cap.`,
382
+ `It will run as **${task.fromAgent}** in a fresh task session with a ${task.maxMinutes} minute cap.`,
337
383
  `Use \`status ${task.id}\` or check the dashboard Background Tasks panel for progress.`,
338
384
  ].join('\n');
339
385
  }
340
- formatBackgroundOfferResponse(offer) {
341
- const lines = [
342
- 'This looks like long-running, multi-tool work. I recommend running it in the background so chat does not go stale.',
343
- '',
344
- '**Plan**',
345
- ...offer.recommendation.plan.map((step, idx) => `${idx + 1}. ${step}`),
346
- '',
347
- `**Why background:** ${offer.recommendation.reasons.join('; ')}.`,
348
- `**Estimated cap:** ${offer.recommendation.suggestedMaxMinutes} minutes.`,
349
- `**Background offer:** ${offer.id}`,
350
- '',
351
- `Reply \`run background ${offer.id}\` to queue it, \`run inline ${offer.id}\` to run it in this chat, or \`save skill ${offer.id}\` to make it reusable first.`,
352
- ];
353
- return lines.join('\n');
386
+ queueBackgroundTaskAfterContextOverflow(sessionKey, prompt) {
387
+ const recommendation = detectComplexTaskForBackground(prompt);
388
+ const task = createBackgroundTask({
389
+ fromAgent: this.backgroundAgentForSession(sessionKey),
390
+ prompt,
391
+ maxMinutes: recommendation?.suggestedMaxMinutes ?? 60,
392
+ sessionKey,
393
+ });
394
+ logger.warn({
395
+ taskId: task.id,
396
+ sessionKey,
397
+ fromAgent: task.fromAgent,
398
+ maxMinutes: task.maxMinutes,
399
+ }, 'Queued background task after repeated chat context overflow');
400
+ return {
401
+ task,
402
+ response: [
403
+ `The live chat context hit the limit, so I moved this into background task **${task.id}** and kept your request attached.`,
404
+ '',
405
+ `It will run as **${task.fromAgent}** in a fresh task session with a ${task.maxMinutes} minute cap.`,
406
+ `Use \`status ${task.id}\` or check the dashboard Background Tasks panel for progress.`,
407
+ ].join('\n'),
408
+ };
354
409
  }
410
+ // Offer-message formatter was removed in the Saturday-feel restoration —
411
+ // the chat path no longer asks "want me to run this in the background?".
412
+ // Auto-queue on explicit user intent is silent; everything else just runs.
355
413
  acceptBackgroundOffer(sessionKey, id) {
356
414
  const offer = this.getBackgroundOfferForSession(sessionKey, id);
357
415
  if (!offer) {
@@ -1896,45 +1954,38 @@ export class Gateway {
1896
1954
  || text.startsWith('[Approval:')
1897
1955
  || text.startsWith('[Reaction:')
1898
1956
  || text.startsWith('[System:');
1957
+ // ── Explicit background-intent shortcut ────────────────────────
1958
+ // Chat normally runs in-place — the SDK auto-compacts and the model
1959
+ // can spawn `planner` / `researcher` subagents for context-heavy
1960
+ // sub-steps, just like Claude Code. We only auto-queue a durable
1961
+ // background task when the user *explicitly* says "in the
1962
+ // background", "overnight", "keep working", "don't stop", etc. The
1963
+ // post-overflow rescue path below still catches the rare case
1964
+ // where chat actually drowns despite all that.
1899
1965
  if (!skipBackgroundOffer && !isBuilderSession && !isInternalMsg && this.isTrustedPersonalSession(sessionKey)) {
1900
1966
  const recommendation = detectComplexTaskForBackground(text);
1901
1967
  if (recommendation) {
1902
1968
  const offer = this.createBackgroundOffer(sessionKey, text, recommendation);
1903
- if (recommendation.queueImmediately) {
1904
- const task = this.queueBackgroundOffer(offer);
1905
- const queued = this.formatBackgroundQueuedResponse(task);
1906
- if (ledgerRunMetadata) {
1907
- ledgerRunMetadata.executionMode = 'background_queued';
1908
- ledgerRunMetadata.backgroundTaskId = task.id;
1909
- }
1910
- if (onText) {
1911
- try {
1912
- await onText(queued);
1913
- }
1914
- catch { /* channel streaming is best-effort */ }
1915
- }
1916
- this.mirrorChatExchange(sessionKey, originalText, queued, { model: 'chat-control' });
1917
- return queued;
1918
- }
1919
- const offerText = this.formatBackgroundOfferResponse(offer);
1969
+ const task = this.queueBackgroundOffer(offer);
1970
+ const queued = this.formatBackgroundQueuedResponse(task);
1920
1971
  if (ledgerRunMetadata) {
1921
- ledgerRunMetadata.executionMode = 'background_offer';
1972
+ ledgerRunMetadata.executionMode = 'background_queued';
1973
+ ledgerRunMetadata.backgroundTaskId = task.id;
1922
1974
  }
1923
1975
  logger.info({
1924
1976
  sessionKey,
1925
- offerId: offer.id,
1926
- score: recommendation.score,
1977
+ taskId: task.id,
1927
1978
  reasons: recommendation.reasons,
1928
1979
  maxMinutes: recommendation.suggestedMaxMinutes,
1929
- }, 'Offering background execution for complex chat request');
1980
+ }, 'Auto-queued background task on explicit user intent');
1930
1981
  if (onText) {
1931
1982
  try {
1932
- await onText(offerText);
1983
+ await onText(queued);
1933
1984
  }
1934
1985
  catch { /* channel streaming is best-effort */ }
1935
1986
  }
1936
- this.mirrorChatExchange(sessionKey, originalText, offerText, { model: 'chat-control' });
1937
- return offerText;
1987
+ this.mirrorChatExchange(sessionKey, originalText, queued, { model: 'chat-control' });
1988
+ return queued;
1938
1989
  }
1939
1990
  }
1940
1991
  if (!isInternalMsg && !sess?.profile && !text.startsWith('!') && !isStructuredWorkflowMsg && onProgress) {
@@ -2058,6 +2109,7 @@ export class Gateway {
2058
2109
  // Interrupt flag was set but no useful partial text — just clear it.
2059
2110
  delete sessState.pendingInterrupt;
2060
2111
  }
2112
+ let contextOverflowRecoveryPrompt = '';
2061
2113
  try {
2062
2114
  // ── Canonical SDK chat path (Phase 5) ────────────────────────
2063
2115
  // runAgent() owns chat. No legacy fallback — errors propagate
@@ -2117,6 +2169,7 @@ export class Gateway {
2117
2169
  const chatSystemAppend = resolvedSkills && resolvedSkills.promptBlock
2118
2170
  ? (baseSystemAppend ? `${baseSystemAppend}\n\n${resolvedSkills.promptBlock}` : resolvedSkills.promptBlock)
2119
2171
  : baseSystemAppend;
2172
+ const retrySystemAppend = trimContextRecoveryText(chatSystemAppend, CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS);
2120
2173
  // Per-turn context (recall + persistent learnings + silent
2121
2174
  // blocks + security/toolset directives) — real chat only.
2122
2175
  // Builder doesn't need recall of unrelated transcripts.
@@ -2156,7 +2209,7 @@ export class Gateway {
2156
2209
  skillMatchNames: resolvedSkills?.matches.map(m => m.name) ?? [],
2157
2210
  skillHintedMcpServers: resolvedSkills?.hintedMcpServers ?? [],
2158
2211
  }, 'Routing chat through runAgent');
2159
- const runAgentResult = await runAgent(finalPrompt, {
2212
+ const buildRunAgentChatOptions = (opts) => ({
2160
2213
  sessionKey: effectiveSessionKey,
2161
2214
  source: 'chat',
2162
2215
  profile: resolvedProfile,
@@ -2166,8 +2219,8 @@ export class Gateway {
2166
2219
  ...(maxTurns ? { maxTurns } : {}),
2167
2220
  ...(chatBudget !== undefined ? { maxBudgetUsd: chatBudget } : {}),
2168
2221
  ...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
2169
- ...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
2170
- ...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
2222
+ ...(opts.systemPromptAppend ? { systemPromptAppend: opts.systemPromptAppend } : {}),
2223
+ ...(opts.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {}),
2171
2224
  ...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
2172
2225
  onText: wrappedOnText,
2173
2226
  onToolActivity: ({ tool, input }) => {
@@ -2178,6 +2231,74 @@ export class Gateway {
2178
2231
  },
2179
2232
  abortSignal: chatAc.signal,
2180
2233
  });
2234
+ let didContextOverflowRetry = false;
2235
+ const contextOverflowAfterRetryError = () => new Error('rapid_refill_breaker after context overflow retry');
2236
+ const retryAfterContextOverflow = async () => {
2237
+ if (didContextOverflowRetry)
2238
+ throw contextOverflowAfterRetryError();
2239
+ didContextOverflowRetry = true;
2240
+ const retryPrompt = buildContextOverflowRetryPrompt({
2241
+ chatPrompt,
2242
+ turnContextPrefix,
2243
+ project: sess?.project ?? null,
2244
+ });
2245
+ contextOverflowRecoveryPrompt = retryPrompt;
2246
+ logger.info({
2247
+ sessionKey: effectiveSessionKey,
2248
+ hadResume: !!priorSdkSessionId,
2249
+ promptChars: finalPrompt.length,
2250
+ retryPromptChars: retryPrompt.length,
2251
+ systemAppendChars: chatSystemAppend.length,
2252
+ retrySystemAppendChars: retrySystemAppend.length,
2253
+ }, 'Context overflow — retrying current message in fresh SDK session');
2254
+ if (onProgress) {
2255
+ await onProgress('refreshing conversation context...').catch(() => { });
2256
+ }
2257
+ this.assistant.clearSession(effectiveSessionKey);
2258
+ return runAgent(retryPrompt, buildRunAgentChatOptions({
2259
+ ...(retrySystemAppend ? { systemPromptAppend: retrySystemAppend } : {}),
2260
+ }));
2261
+ };
2262
+ let runAgentResult;
2263
+ try {
2264
+ runAgentResult = await runAgent(finalPrompt, buildRunAgentChatOptions({
2265
+ ...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
2266
+ ...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
2267
+ }));
2268
+ }
2269
+ catch (err) {
2270
+ if (chatAc.signal.aborted || classifyChatError(err) !== 'context_overflow') {
2271
+ throw err;
2272
+ }
2273
+ runAgentResult = await retryAfterContextOverflow();
2274
+ }
2275
+ if (!chatAc.signal.aborted && runAgentResultIndicatesContextOverflow(runAgentResult)) {
2276
+ if (didContextOverflowRetry) {
2277
+ logger.info({
2278
+ sessionKey: effectiveSessionKey,
2279
+ subtype: runAgentResult.subtype,
2280
+ terminalReason: runAgentResult.terminalReason,
2281
+ textPreview: runAgentResult.text?.slice(0, 240),
2282
+ }, 'Context overflow result after retry — queueing background task');
2283
+ throw contextOverflowAfterRetryError();
2284
+ }
2285
+ logger.info({
2286
+ sessionKey: effectiveSessionKey,
2287
+ subtype: runAgentResult.subtype,
2288
+ terminalReason: runAgentResult.terminalReason,
2289
+ textPreview: runAgentResult.text?.slice(0, 240),
2290
+ }, 'Context overflow result — retrying current message in fresh SDK session');
2291
+ runAgentResult = await retryAfterContextOverflow();
2292
+ if (runAgentResultIndicatesContextOverflow(runAgentResult)) {
2293
+ logger.info({
2294
+ sessionKey: effectiveSessionKey,
2295
+ subtype: runAgentResult.subtype,
2296
+ terminalReason: runAgentResult.terminalReason,
2297
+ textPreview: runAgentResult.text?.slice(0, 240),
2298
+ }, 'Context overflow result after retry — queueing background task');
2299
+ throw contextOverflowAfterRetryError();
2300
+ }
2301
+ }
2181
2302
  if (ledgerRunMetadata) {
2182
2303
  ledgerRunMetadata.runId = runAgentResult.runId;
2183
2304
  ledgerRunMetadata.executionMode = ledgerRunMetadata.executionMode ?? 'inline';
@@ -2254,9 +2375,18 @@ export class Gateway {
2254
2375
  this.clearSession(effectiveSessionKey);
2255
2376
  return oneMillionContextRecoveryMessage();
2256
2377
  case 'context_overflow':
2257
- logger.info({ sessionKey }, 'Context overflow — rotating session');
2378
+ logger.info({ sessionKey }, 'Context overflow after retry queueing background task');
2258
2379
  this.assistant.clearSession(effectiveSessionKey);
2259
- return "That conversation got too long — I've started a fresh session. Please resend your message.";
2380
+ {
2381
+ const promptForBackground = contextOverflowRecoveryPrompt || chatPrompt;
2382
+ const { response, task } = this.queueBackgroundTaskAfterContextOverflow(sessionKey, promptForBackground);
2383
+ if (ledgerRunMetadata) {
2384
+ ledgerRunMetadata.executionMode = 'background_queued';
2385
+ ledgerRunMetadata.backgroundTaskId = task.id;
2386
+ }
2387
+ this.mirrorChatExchange(sessionKey, originalText, response, { model: 'chat-control' });
2388
+ return response;
2389
+ }
2260
2390
  case 'auth':
2261
2391
  this.recordAuthFailure();
2262
2392
  return "I'm temporarily offline due to an authentication issue. The owner needs to re-authenticate — I'll recover automatically once it's resolved.";
@@ -32,6 +32,12 @@ export declare function isComposioEnabled(): boolean;
32
32
  * the dashboard PUT /api/settings/COMPOSIO_API_KEY handler.
33
33
  */
34
34
  export declare function resetComposioClient(): void;
35
+ /**
36
+ * Drop the per-process connection-list cache so the next call to
37
+ * `listConnectedToolkits()` hits Composio fresh. Used after authorize /
38
+ * disconnect / rename so the dashboard and agent see the change immediately.
39
+ */
40
+ export declare function clearConnectedToolkitsCache(): void;
35
41
  export declare function getPreferredUserId(): Promise<string>;
36
42
  export declare function clementineUserId(): string;
37
43
  export declare function displayNameFor(slug: string): string;
@@ -83,6 +83,30 @@ export function resetComposioClient() {
83
83
  identityCache.clear();
84
84
  catalogCache = null;
85
85
  detectedPreferredUserId = null;
86
+ connectionsCache = null;
87
+ void busComposioMcpCache();
88
+ }
89
+ /**
90
+ * Drop the per-process connection-list cache so the next call to
91
+ * `listConnectedToolkits()` hits Composio fresh. Used after authorize /
92
+ * disconnect / rename so the dashboard and agent see the change immediately.
93
+ */
94
+ export function clearConnectedToolkitsCache() {
95
+ connectionsCache = null;
96
+ }
97
+ /**
98
+ * Fire-and-forget MCP-server cache bust. Imported lazily to avoid the
99
+ * client → mcp-bridge → client cycle that an `import { ... }` at the top
100
+ * would create.
101
+ */
102
+ async function busComposioMcpCache() {
103
+ try {
104
+ const mod = await import('./mcp-bridge.js');
105
+ mod.clearComposioMcpCache?.();
106
+ }
107
+ catch {
108
+ /* mcp-bridge optional at boot; safe to ignore */
109
+ }
86
110
  }
87
111
  // Public: same logic as the internal detector, exposed for the MCP bridge so
88
112
  // agent sessions land on the right user_id.
@@ -310,10 +334,22 @@ async function getIdentityFor(composio, id, slug, seed) {
310
334
  identityCache.set(id, { at: Date.now(), identity });
311
335
  return identity;
312
336
  }
337
+ // Short-lived per-process cache + stale-while-revalidate. Composio API hiccups
338
+ // between turns used to make tools "vanish" from the chat; with this, a single
339
+ // failed list call falls back to the prior good snapshot. TTL is short enough
340
+ // (60s) that legit reconnects / disconnects show up quickly, and the dashboard
341
+ // auth/disconnect handlers explicitly bust the cache via
342
+ // `clearConnectedToolkitsCache()` for instant reflection.
343
+ let connectionsCache = null;
344
+ const CONNECTIONS_TTL_MS = 60_000;
313
345
  export async function listConnectedToolkits() {
314
346
  const composio = getComposio();
315
347
  if (!composio)
316
348
  return [];
349
+ const now = Date.now();
350
+ if (connectionsCache && now - connectionsCache.at < CONNECTIONS_TTL_MS) {
351
+ return connectionsCache.data;
352
+ }
317
353
  try {
318
354
  // No userIds filter: a Composio API key is account-scoped, and a personal
319
355
  // agent should see every connection on the account regardless of which
@@ -337,10 +373,15 @@ export async function listConnectedToolkits() {
337
373
  createdAt: it.createdAt,
338
374
  };
339
375
  }));
376
+ connectionsCache = { at: now, data: enriched };
340
377
  return enriched;
341
378
  }
342
379
  catch (err) {
343
- logger.error({ err }, 'listConnectedToolkits failed');
380
+ if (connectionsCache) {
381
+ logger.warn({ err, staleAgeMs: now - connectionsCache.at, items: connectionsCache.data.length }, 'listConnectedToolkits failed — returning stale cache');
382
+ return connectionsCache.data;
383
+ }
384
+ logger.error({ err }, 'listConnectedToolkits failed (no cache to fall back to)');
344
385
  return [];
345
386
  }
346
387
  }
@@ -481,6 +522,8 @@ _opts) {
481
522
  // others created in parallel via Composio's web UI) get picked up
482
523
  // immediately, even within the 60s TTL window.
483
524
  detectedPreferredUserId = null;
525
+ connectionsCache = null;
526
+ void busComposioMcpCache();
484
527
  return { redirectUrl: conn.redirectUrl ?? null, connectionId: conn.id };
485
528
  }
486
529
  catch (err) {
@@ -509,6 +552,8 @@ export async function disconnectToolkit(connectionId) {
509
552
  throw new Error('COMPOSIO_API_KEY not set');
510
553
  await composio.connectedAccounts.delete(connectionId);
511
554
  identityCache.delete(connectionId);
555
+ connectionsCache = null;
556
+ void busComposioMcpCache();
512
557
  }
513
558
  export async function renameConnection(connectionId, alias) {
514
559
  const composio = getComposio();
@@ -520,5 +565,6 @@ export async function renameConnection(connectionId, alias) {
520
565
  // hatch and the alternative (bypassing the wrapper entirely) loses retry
521
566
  // and auth handling.
522
567
  await composio.client.connectedAccounts.patch(connectionId, { alias });
568
+ connectionsCache = null;
523
569
  }
524
570
  //# sourceMappingURL=client.js.map
@@ -16,6 +16,7 @@
16
16
  * always works — Composio is purely additive.
17
17
  */
18
18
  import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk';
19
+ export declare function clearComposioMcpCache(slug?: string): void;
19
20
  /**
20
21
  * Build SDK MCP server configs for the given toolkit slugs (or all active
21
22
  * connected toolkits when omitted). Each toolkit becomes one MCP server.
@@ -19,6 +19,15 @@ import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
19
19
  import pino from 'pino';
20
20
  import { getComposio, getPreferredUserId, listConnectedToolkits, } from './client.js';
21
21
  const logger = pino({ name: 'clementine.composio.mcp' });
22
+ const serverCache = new Map();
23
+ const SERVER_CACHE_TTL_MS = 5 * 60_000;
24
+ export function clearComposioMcpCache(slug) {
25
+ if (slug) {
26
+ serverCache.delete(slug);
27
+ return;
28
+ }
29
+ serverCache.clear();
30
+ }
22
31
  /**
23
32
  * Build SDK MCP server configs for the given toolkit slugs (or all active
24
33
  * connected toolkits when omitted). Each toolkit becomes one MCP server.
@@ -70,6 +79,11 @@ export async function listComposioToolkitTools(slugs) {
70
79
  return out;
71
80
  }
72
81
  async function buildOne(composio, slug, _connected) {
82
+ const now = Date.now();
83
+ const cached = serverCache.get(slug);
84
+ if (cached && now - cached.at < SERVER_CACHE_TTL_MS) {
85
+ return cached.server;
86
+ }
73
87
  // composio.tools.get() returns the FLAT toolkit tools (OUTLOOK_LIST_MESSAGES,
74
88
  // GMAIL_SEND_EMAIL, …) — exactly the namespacing the agent expects as
75
89
  // mcp__outlook__OUTLOOK_LIST_MESSAGES. The alternative, composio.create()
@@ -83,11 +97,13 @@ async function buildOne(composio, slug, _connected) {
83
97
  // alphabetically come after OUTLOOK_LIST_CALENDAR_GROUP_*. GitHub has
84
98
  // 800+. Set 1000 — comfortable headroom for any single toolkit.
85
99
  const tools = await fetchToolkitTools(composio, slug);
86
- return createSdkMcpServer({
100
+ const server = createSdkMcpServer({
87
101
  name: slug,
88
102
  version: '0.1.0',
89
103
  tools: tools,
90
104
  });
105
+ serverCache.set(slug, { at: now, server });
106
+ return server;
91
107
  }
92
108
  async function fetchToolkitTools(composio, slug) {
93
109
  const userId = await getPreferredUserId();
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { z } from 'zod';
15
15
  import { createBackgroundTask, listBackgroundTasks, loadBackgroundTask, } from '../agent/background-tasks.js';
16
- import { ACTIVE_AGENT_SLUG, logger, textResult } from './shared.js';
16
+ import { ACTIVE_AGENT_SLUG, ACTIVE_SESSION_KEY, logger, textResult } from './shared.js';
17
17
  const DEFAULT_MAX_MINUTES = 30;
18
18
  export function registerBackgroundTaskTools(server) {
19
19
  server.tool('start_background_task', 'Kick off a long-running autonomous task in the background. Use when the work would burn the chat context (deep research, multi-page extraction, batch processing) or take longer than a chat turn. Returns a task id immediately. The daemon picks the task up within seconds, runs it with your profile + tools, and posts the deliverable to your Discord channel when done.', {
@@ -30,8 +30,9 @@ export function registerBackgroundTaskTools(server) {
30
30
  fromAgent,
31
31
  prompt: trimmed,
32
32
  maxMinutes: cap,
33
+ ...(ACTIVE_SESSION_KEY ? { sessionKey: ACTIVE_SESSION_KEY } : {}),
33
34
  });
34
- logger.info({ id: task.id, fromAgent, maxMinutes: task.maxMinutes }, 'Background task queued');
35
+ logger.info({ id: task.id, fromAgent, sessionKey: task.sessionKey, maxMinutes: task.maxMinutes }, 'Background task queued');
35
36
  return textResult(`Queued **${task.id}** (max ${task.maxMinutes} min). The daemon will pick it up within a few seconds and run it in the background. You'll get a notification in your channel when the deliverable lands. Use \`get_background_task\` to check status.`);
36
37
  });
37
38
  server.tool('get_background_task', 'Check the status of a background task. Returns its lifecycle state (pending|running|done|failed|aborted|interrupted), how long it has been running, and the result/error if terminal.', {
@@ -8,6 +8,6 @@
8
8
  * Usage:
9
9
  * npx tsx src/tools/mcp-server.ts
10
10
  */
11
- export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG } from './shared.js';
11
+ export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG, ACTIVE_SESSION_KEY } from './shared.js';
12
12
  export type { MemoryStoreType } from './shared.js';
13
13
  //# sourceMappingURL=mcp-server.d.ts.map
@@ -15,7 +15,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
15
15
  import { z } from 'zod';
16
16
  import { BASE_DIR, VAULT_DIR, env, getStore, getStoreSync, logger, textResult, externalResult, } from './shared.js';
17
17
  // Re-export for any code that imports from mcp-server.ts directly
18
- export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG } from './shared.js';
18
+ export { getStore, textResult, externalResult, incrementalSync, ACTIVE_AGENT_SLUG, ACTIVE_SESSION_KEY } from './shared.js';
19
19
  // ── Tool modules ────────────────────────────────────────────────────────
20
20
  import { registerMemoryTools } from './memory-tools.js';
21
21
  import { registerVaultTools } from './vault-tools.js';
@@ -671,6 +671,7 @@ export type MemoryStoreType = {
671
671
  export declare function getStore(): Promise<MemoryStoreType>;
672
672
  export declare function getStoreSync(): MemoryStoreType | null;
673
673
  export declare const ACTIVE_AGENT_SLUG: string | null;
674
+ export declare const ACTIVE_SESSION_KEY: string | null;
674
675
  export declare const GOALS_DIR: string;
675
676
  export declare function agentTasksFile(slug: string | null): string;
676
677
  export declare function agentWorkingMemoryFile(slug: string | null): string;
@@ -59,6 +59,8 @@ export function getStoreSync() {
59
59
  // ── Active Agent Slug ──────────────────────────────────────────────────
60
60
  const _rawAgentSlug = process.env.CLEMENTINE_TEAM_AGENT || null;
61
61
  export const ACTIVE_AGENT_SLUG = _rawAgentSlug === 'clementine' ? null : _rawAgentSlug;
62
+ const _rawSessionKey = process.env.CLEMENTINE_SESSION_KEY || null;
63
+ export const ACTIVE_SESSION_KEY = _rawSessionKey?.trim() || null;
62
64
  // ── Agent-aware path helpers ───────────────────────────────────────────
63
65
  // GOALS_DIR is defined in config.ts but not in shared.ts — define it here
64
66
  export const GOALS_DIR = path.join(BASE_DIR, 'goals');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.176",
3
+ "version": "1.18.178",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",