clementine-agent 1.18.2 → 1.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,11 @@ 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, } 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, envSnapshot, } from '../config.js';
17
+ import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
18
+ import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, } from '../integrations/tool-preferences.js';
19
+ import { loadClaudeIntegrations } from './mcp-bridge.js';
20
+ import { detectFrustrationSignals, detectRepeatedTopics } from './insight-engine.js';
17
21
  import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
18
22
  import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, setProfileTier, setProfileAllowedTools, setAgentDir, setSendPolicy, setInteractionSource, logAuditJsonl, } from './hooks.js';
19
23
  import { scanner } from '../security/scanner.js';
@@ -1558,8 +1562,6 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1558
1562
  // Integration status — changes as owner adds credentials.
1559
1563
  if (!isAutonomous) {
1560
1564
  try {
1561
- const { summarizeIntegrationStatus } = require('../config/integrations-registry.js');
1562
- const { envSnapshot } = require('../config.js');
1563
1565
  const summary = summarizeIntegrationStatus(envSnapshot());
1564
1566
  if (summary)
1565
1567
  volatileParts.push(`## Integration Status\n\n${summary}\n\nCall \`integration_status\`, \`list_integrations\`, or \`setup_integration\` for details.`);
@@ -1578,8 +1580,6 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1578
1580
  // every turn regardless.
1579
1581
  if (!isAutonomous) {
1580
1582
  try {
1581
- const { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock } = require('../integrations/tool-preferences.js');
1582
- const { loadClaudeIntegrations } = require('./mcp-bridge.js');
1583
1583
  const composioSet = new Set(composioConnectedSlugs);
1584
1584
  const cdIntegrations = loadClaudeIntegrations();
1585
1585
  const cdActive = new Set(Object.values(cdIntegrations).filter(i => i.connected).map(i => i.name));
@@ -1605,7 +1605,6 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
1605
1605
  // one signal fires — keeps the prompt clean during normal sessions.
1606
1606
  if (!isAutonomous) {
1607
1607
  try {
1608
- const { detectFrustrationSignals, detectRepeatedTopics } = require('./insight-engine.js');
1609
1608
  const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
1610
1609
  const since7d = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
1611
1610
  let recent = this.getRecentActivity(since24h, 50);
@@ -25,6 +25,8 @@ const SPECS = [
25
25
  { key: 'HAIKU_MODEL', group: 'models', jsonPath: 'models.haiku', default: 'claude-haiku-4-5-20251001' },
26
26
  { key: 'SONNET_MODEL', group: 'models', jsonPath: 'models.sonnet', default: 'claude-sonnet-4-6' },
27
27
  { key: 'OPUS_MODEL', group: 'models', jsonPath: 'models.opus', default: 'claude-opus-4-7' },
28
+ // Team routing
29
+ { key: 'AUTO_DELEGATE_ENABLED', group: 'team', default: false },
28
30
  // Budgets
29
31
  { key: 'BUDGET_HEARTBEAT_USD', group: 'budgets', jsonPath: 'budgets.heartbeat', default: 0.50 },
30
32
  { key: 'BUDGET_CRON_T1_USD', group: 'budgets', jsonPath: 'budgets.cronT1', default: 2.00 },
package/dist/config.d.ts CHANGED
@@ -84,6 +84,7 @@ export declare const TASK_BUDGET_TOKENS: {
84
84
  };
85
85
  export declare const DEFAULT_MODEL_TIER: keyof Models;
86
86
  export declare const MODEL: string;
87
+ export declare const AUTO_DELEGATE_ENABLED: boolean;
87
88
  export declare const DISCORD_TOKEN: string;
88
89
  export declare const DISCORD_OWNER_ID: string;
89
90
  export declare const DISCORD_WATCHED_CHANNELS: string[];
package/dist/config.js CHANGED
@@ -236,6 +236,13 @@ export const TASK_BUDGET_TOKENS = {
236
236
  };
237
237
  export const DEFAULT_MODEL_TIER = (getEnvOrJson('DEFAULT_MODEL_TIER', json.models?.default, 'sonnet'));
238
238
  export const MODEL = MODELS[DEFAULT_MODEL_TIER] ?? MODELS.sonnet;
239
+ // ── Team routing ─────────────────────────────────────────────────────
240
+ // When false (default), the gateway never auto-delegates a Clementine-
241
+ // owned message to a sub-agent without explicit user opt-in. High-
242
+ // confidence routing is demoted to a soft-suggest so the user can
243
+ // confirm ("send to Ross") or override before any handoff happens.
244
+ // Flip via dashboard or `clementine config set AUTO_DELEGATE_ENABLED true`.
245
+ export const AUTO_DELEGATE_ENABLED = getEnv('AUTO_DELEGATE_ENABLED', 'false').toLowerCase() === 'true';
239
246
  // ── Discord ──────────────────────────────────────────────────────────
240
247
  export const DISCORD_TOKEN = getSecret('DISCORD_TOKEN');
241
248
  export const DISCORD_OWNER_ID = getEnv('DISCORD_OWNER_ID', '0');
@@ -10,7 +10,7 @@ import pino from 'pino';
10
10
  import { PersonalAssistant } from '../agent/assistant.js';
11
11
  import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
12
12
  import { SelfImproveLoop } from '../agent/self-improve.js';
13
- import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE } from '../config.js';
13
+ import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED } from '../config.js';
14
14
  import { scanner } from '../security/scanner.js';
15
15
  import { lanes } from './lanes.js';
16
16
  import { AgentManager } from '../agent/agent-manager.js';
@@ -255,8 +255,10 @@ export class Gateway {
255
255
  const targetProfile = agents.find(a => a.slug === decision.targetAgent);
256
256
  if (!targetProfile)
257
257
  return null;
258
- // Auto-delegate at high confidence
259
- if (decision.confidence >= 0.8) {
258
+ // Auto-delegate at high confidence — only when the user has explicitly
259
+ // opted in via AUTO_DELEGATE_ENABLED. Default behavior is to demote
260
+ // every routing to a soft-suggest so the user controls every handoff.
261
+ if (decision.confidence >= 0.8 && AUTO_DELEGATE_ENABLED) {
260
262
  // Fire the team task in the background; ack immediately.
261
263
  const ackMessage = `Routing this to **${targetProfile.name}** (${decision.reasoning.toLowerCase()}). I'll post their response back here when done.`;
262
264
  onText?.(ackMessage).catch(() => { });
@@ -266,7 +268,32 @@ export class Gateway {
266
268
  if (!sess.teamTaskControllers)
267
269
  sess.teamTaskControllers = new Set();
268
270
  sess.teamTaskControllers.add(teamAbortController);
269
- this.handleTeamTask('Clementine', 'clementine', text, targetProfile, undefined, teamAbortController)
271
+ // Progress visibility — without this the channel goes dark from
272
+ // ack to final result, which can be many minutes on research-style
273
+ // tasks. We batch the delegated agent's tool announcements and
274
+ // flush every PROGRESS_INTERVAL_MS so the user sees what's
275
+ // happening without a token firehose.
276
+ const recentTools = [];
277
+ let progressTimer;
278
+ const PROGRESS_INTERVAL_MS = 30_000;
279
+ const dispatcher = this._dispatcher;
280
+ const flushProgress = () => {
281
+ progressTimer = undefined;
282
+ if (recentTools.length === 0)
283
+ return;
284
+ const tools = recentTools.slice(-5).join(', ');
285
+ recentTools.length = 0;
286
+ void dispatcher?.send(`_${targetProfile.name} is working — recent actions: ${tools}_`, { sessionKey, agentSlug: targetProfile.slug });
287
+ };
288
+ const onTeamProgress = (chunk) => {
289
+ const m = chunk.match(/\[using ([^\]]+?)\.\.\.\]/);
290
+ if (m && m[1])
291
+ recentTools.push(m[1]);
292
+ if (!progressTimer && recentTools.length > 0) {
293
+ progressTimer = setTimeout(flushProgress, PROGRESS_INTERVAL_MS);
294
+ }
295
+ };
296
+ this.handleTeamTask('Clementine', 'clementine', text, targetProfile, onTeamProgress, teamAbortController)
270
297
  .then(response => {
271
298
  if (!response)
272
299
  return;
@@ -282,6 +309,10 @@ export class Gateway {
282
309
  void this._dispatcher?.send(`**${targetProfile.name}** hit an error handling that: ${String(err).slice(0, 200)}`, { sessionKey, agentSlug: targetProfile.slug });
283
310
  })
284
311
  .finally(() => {
312
+ if (progressTimer) {
313
+ clearTimeout(progressTimer);
314
+ progressTimer = undefined;
315
+ }
285
316
  const s = this.sessions.get(sessionKey);
286
317
  s?.teamTaskControllers?.delete(teamAbortController);
287
318
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.2",
3
+ "version": "1.18.4",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",