clementine-agent 1.18.175 → 1.18.177

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,
@@ -75,6 +75,10 @@ export interface RunAgentOptions {
75
75
  /** Optional explicit allowedTools list. When unset, falls back to a sensible default
76
76
  * including Agent (so subagents can be spawned) + core SDK tools + Clementine MCP. */
77
77
  allowedTools?: string[];
78
+ /** Extra tools to pre-approve without making their built-in tools visible to
79
+ * the main agent. Useful when the main agent may only call Agent, but the
80
+ * forced subagent still needs pre-approved MCP/Clementine tools. */
81
+ permissionTools?: string[];
78
82
  /** SDK permission mode. Defaults to dontAsk so allowedTools is enforceable.
79
83
  * Only explicit operator/full-surface paths should request bypassPermissions. */
80
84
  permissionMode?: ExecutionPermissionMode;
@@ -254,6 +254,27 @@ export async function runAgent(prompt, opts) {
254
254
  clementineServerName: TOOLS_SERVER,
255
255
  permissionMode: opts.permissionMode,
256
256
  });
257
+ const permissionToolPolicy = opts.permissionTools
258
+ ? buildExecutionToolPolicy({
259
+ requestedTools: opts.permissionTools,
260
+ defaultBuiltins: CORE_TOOLS_FOR_AGENT_PARENT,
261
+ mcpServerNames: policyMcpServerNames,
262
+ clementineServerName: TOOLS_SERVER,
263
+ permissionMode: opts.permissionMode,
264
+ })
265
+ : null;
266
+ const sdkAllowedTools = permissionToolPolicy
267
+ ? Array.from(new Set([...toolPolicy.allowedTools, ...permissionToolPolicy.allowedTools])).sort()
268
+ : toolPolicy.allowedTools;
269
+ const clementineToolAllowlist = (() => {
270
+ if (!permissionToolPolicy)
271
+ return toolPolicy.clementineToolAllowlist;
272
+ const parts = [toolPolicy.clementineToolAllowlist, permissionToolPolicy.clementineToolAllowlist]
273
+ .flatMap(v => v.split(',').map(s => s.trim()).filter(Boolean));
274
+ if (parts.includes('*'))
275
+ return '*';
276
+ return Array.from(new Set(parts)).sort().join(',');
277
+ })();
257
278
  // SDK accepts a Record<string, McpServerConfig> here. We cast on
258
279
  // assignment because we mix the always-on Clementine stdio server
259
280
  // with caller-supplied servers of various types.
@@ -266,8 +287,9 @@ export async function runAgent(prompt, opts) {
266
287
  ...subprocessEnv,
267
288
  CLEMENTINE_HOME: BASE_DIR,
268
289
  ...(opts.profile?.slug ? { CLEMENTINE_TEAM_AGENT: opts.profile.slug } : {}),
290
+ CLEMENTINE_SESSION_KEY: opts.sessionKey,
269
291
  CLEMENTINE_INTERACTION_SOURCE: interactionSourceForSession(opts.sessionKey, source),
270
- CLEMENTINE_TOOL_ALLOWLIST: toolPolicy.clementineToolAllowlist,
292
+ CLEMENTINE_TOOL_ALLOWLIST: clementineToolAllowlist,
271
293
  },
272
294
  },
273
295
  ...baseMcpServers,
@@ -407,7 +429,7 @@ export async function runAgent(prompt, opts) {
407
429
  // callers can mix stdio + http + sse server shapes.
408
430
  mcpServers: mcpServers,
409
431
  tools: toolPolicy.builtinTools,
410
- allowedTools: toolPolicy.allowedTools,
432
+ allowedTools: sdkAllowedTools,
411
433
  permissionMode: toolPolicy.permissionMode,
412
434
  ...(sessionStore ? { sessionStore } : {}),
413
435
  ...(toolPolicy.allowDangerouslySkipPermissions
@@ -447,7 +469,7 @@ export async function runAgent(prompt, opts) {
447
469
  maxBudgetUsd: maxBudgetUsd ?? 'uncapped',
448
470
  agentCount: Object.keys(agents).length,
449
471
  allowedToolCount: allowedTools.length,
450
- sdkAllowedToolCount: toolPolicy.allowedTools.length,
472
+ sdkAllowedToolCount: sdkAllowedTools.length,
451
473
  builtinToolCount: toolPolicy.builtinTools.length,
452
474
  permissionMode: toolPolicy.permissionMode,
453
475
  mcpServerCount: Object.keys(mcpServers).length,
@@ -739,7 +761,7 @@ export async function runAgent(prompt, opts) {
739
761
  ...(usage ? { usage } : {}),
740
762
  runId,
741
763
  permissionMode: toolPolicy.permissionMode,
742
- allowedToolsApplied: toolPolicy.allowedTools,
764
+ allowedToolsApplied: sdkAllowedTools,
743
765
  builtinToolsApplied: toolPolicy.builtinTools,
744
766
  mcpServersApplied: Object.keys(mcpServers),
745
767
  };
@@ -417,6 +417,9 @@ export async function runSkill(name, options = {}) {
417
417
  // the parent's context shape predictable and prevents it from
418
418
  // doing data-heavy work itself even if the LLM disagrees.
419
419
  allowedTools: ['Agent'],
420
+ // SDK permissions are session-level, so the worker's tools still
421
+ // need to be pre-approved even though the parent only sees Agent.
422
+ permissionTools: ['Agent', ...effectiveTools],
420
423
  // Force-routing: SDK wraps the prompt with "Use the skill-worker
421
424
  // agent to handle this request" so dispatch is the natural
422
425
  // first action.
@@ -13,6 +13,11 @@ 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;
16
21
  export type ChatErrorKind = 'rate_limit' | 'one_million_context' | 'context_overflow' | 'auth' | 'billing' | 'transient' | 'unknown';
17
22
  export declare function classifyChatError(err: unknown): ChatErrorKind;
18
23
  /** Detect auth-like errors in response text that the SDK returned as "successful" results. */
@@ -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,26 @@ 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
+ }
77
99
  export function classifyChatError(err) {
78
100
  const msg = String(err);
79
101
  if (isCreditBalanceError(msg))
@@ -82,7 +104,7 @@ export function classifyChatError(err) {
82
104
  return 'rate_limit';
83
105
  if (looksLikeClaudeOneMillionContextError(msg))
84
106
  return 'one_million_context';
85
- if (/context.?length|token.?limit|maximum.?context|prompt.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
107
+ 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
108
  return 'context_overflow';
87
109
  if (/\b401\b|\b403\b|auth|forbidden|invalid.?api.?key|permission|does not have access|please run \/login/i.test(msg))
88
110
  return 'auth';
@@ -2117,6 +2139,7 @@ export class Gateway {
2117
2139
  const chatSystemAppend = resolvedSkills && resolvedSkills.promptBlock
2118
2140
  ? (baseSystemAppend ? `${baseSystemAppend}\n\n${resolvedSkills.promptBlock}` : resolvedSkills.promptBlock)
2119
2141
  : baseSystemAppend;
2142
+ const retrySystemAppend = trimContextRecoveryText(chatSystemAppend, CHAT_CONTEXT_RETRY_SYSTEM_MAX_CHARS);
2120
2143
  // Per-turn context (recall + persistent learnings + silent
2121
2144
  // blocks + security/toolset directives) — real chat only.
2122
2145
  // Builder doesn't need recall of unrelated transcripts.
@@ -2156,7 +2179,7 @@ export class Gateway {
2156
2179
  skillMatchNames: resolvedSkills?.matches.map(m => m.name) ?? [],
2157
2180
  skillHintedMcpServers: resolvedSkills?.hintedMcpServers ?? [],
2158
2181
  }, 'Routing chat through runAgent');
2159
- const runAgentResult = await runAgent(finalPrompt, {
2182
+ const buildRunAgentChatOptions = (opts) => ({
2160
2183
  sessionKey: effectiveSessionKey,
2161
2184
  source: 'chat',
2162
2185
  profile: resolvedProfile,
@@ -2166,8 +2189,8 @@ export class Gateway {
2166
2189
  ...(maxTurns ? { maxTurns } : {}),
2167
2190
  ...(chatBudget !== undefined ? { maxBudgetUsd: chatBudget } : {}),
2168
2191
  ...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
2169
- ...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
2170
- ...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
2192
+ ...(opts.systemPromptAppend ? { systemPromptAppend: opts.systemPromptAppend } : {}),
2193
+ ...(opts.resumeSessionId ? { resumeSessionId: opts.resumeSessionId } : {}),
2171
2194
  ...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
2172
2195
  onText: wrappedOnText,
2173
2196
  onToolActivity: ({ tool, input }) => {
@@ -2178,6 +2201,38 @@ export class Gateway {
2178
2201
  },
2179
2202
  abortSignal: chatAc.signal,
2180
2203
  });
2204
+ let runAgentResult;
2205
+ try {
2206
+ runAgentResult = await runAgent(finalPrompt, buildRunAgentChatOptions({
2207
+ ...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
2208
+ ...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
2209
+ }));
2210
+ }
2211
+ catch (err) {
2212
+ if (chatAc.signal.aborted || classifyChatError(err) !== 'context_overflow') {
2213
+ throw err;
2214
+ }
2215
+ const retryPrompt = buildContextOverflowRetryPrompt({
2216
+ chatPrompt,
2217
+ turnContextPrefix,
2218
+ project: sess?.project ?? null,
2219
+ });
2220
+ logger.info({
2221
+ sessionKey: effectiveSessionKey,
2222
+ hadResume: !!priorSdkSessionId,
2223
+ promptChars: finalPrompt.length,
2224
+ retryPromptChars: retryPrompt.length,
2225
+ systemAppendChars: chatSystemAppend.length,
2226
+ retrySystemAppendChars: retrySystemAppend.length,
2227
+ }, 'Context overflow — retrying current message in fresh SDK session');
2228
+ if (onProgress) {
2229
+ await onProgress('refreshing conversation context...').catch(() => { });
2230
+ }
2231
+ this.assistant.clearSession(effectiveSessionKey);
2232
+ runAgentResult = await runAgent(retryPrompt, buildRunAgentChatOptions({
2233
+ ...(retrySystemAppend ? { systemPromptAppend: retrySystemAppend } : {}),
2234
+ }));
2235
+ }
2181
2236
  if (ledgerRunMetadata) {
2182
2237
  ledgerRunMetadata.runId = runAgentResult.runId;
2183
2238
  ledgerRunMetadata.executionMode = ledgerRunMetadata.executionMode ?? 'inline';
@@ -2368,7 +2423,10 @@ export class Gateway {
2368
2423
  if (scheduledSkillName) {
2369
2424
  const { runSkill } = await import('../agent/run-skill.js');
2370
2425
  const configuredCap = tier >= 2 ? BUDGET.cronT2 : BUDGET.cronT1;
2371
- const scheduledSkillBudget = configuredCap > 0 ? configuredCap : undefined;
2426
+ // Pass 0 through intentionally. It means "uncapped" and must
2427
+ // override skill-local clementine.limits.maxBudgetUsd values when
2428
+ // the operator has disabled global cron budgets.
2429
+ const scheduledSkillBudget = configuredCap > 0 ? configuredCap : 0;
2372
2430
  logger.info({ jobName, skill: scheduledSkillName, agentSlug, tier, wallMs, path: 'run_skill' }, 'Routing scheduled skill through runSkill');
2373
2431
  const skillResult = await runSkill(scheduledSkillName, {
2374
2432
  sessionKey: `cron:${jobName}`,
@@ -2380,7 +2438,7 @@ export class Gateway {
2380
2438
  projectWorkDir: workDir,
2381
2439
  model,
2382
2440
  ...(maxTurns ? { maxTurns } : {}),
2383
- ...(scheduledSkillBudget !== undefined ? { maxBudgetUsd: scheduledSkillBudget } : {}),
2441
+ maxBudgetUsd: scheduledSkillBudget,
2384
2442
  abortSignal: cronAc.signal,
2385
2443
  context: `[Scheduled skill: ${jobName}]`,
2386
2444
  });
@@ -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.175",
3
+ "version": "1.18.177",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",