clementine-agent 1.18.47 → 1.18.48

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.
@@ -29,8 +29,6 @@ import { type ToolsetName } from './toolsets.js';
29
29
  */
30
30
  export declare function estimateTokens(text: string): number;
31
31
  export declare function looksLikeContextThrashText(value: unknown): boolean;
32
- export declare function contextThrashRecoveryNotice(): string;
33
- export declare function buildContextThrashRecoveryPrompt(userRequest: string, priorFailureText?: string): string;
34
32
  /** Format a millisecond duration as a human-friendly "X ago" string. */
35
33
  export declare function formatTimeAgo(ms: number): string;
36
34
  export declare function scrubInternalContextBlocks(text: string): string;
@@ -200,33 +200,6 @@ class UnleashedTaskFailedError extends Error {
200
200
  this.name = 'UnleashedTaskFailedError';
201
201
  }
202
202
  }
203
- export function contextThrashRecoveryNotice() {
204
- return [
205
- 'I hit a context-size recovery issue while working on that.',
206
- 'I saved the request and reset the session so I can continue with smaller reads instead of repeating the same large-output path.',
207
- ].join(' ');
208
- }
209
- export function buildContextThrashRecoveryPrompt(userRequest, priorFailureText = '') {
210
- const parts = [
211
- '[CONTEXT-THRASH RECOVERY]',
212
- '',
213
- 'The previous interactive attempt failed because tool output filled the context window and SDK autocompact thrashed. Continue the user request, but use a small diagnostic pass.',
214
- '',
215
- 'User request:',
216
- userRequest,
217
- '',
218
- 'Recovery rules:',
219
- '- Do not repeat broad reads, full log dumps, full JSON dumps, or unbounded API/list commands.',
220
- '- Prefer status files, summaries, indexes, `rg`, `tail -80`, `head -80`, and `sed -n` slices.',
221
- '- For cron or unleashed jobs, inspect only `status.json`, the tail of `progress.jsonl`, and the latest run preview first. Do not read full run logs unless a short slice identifies the exact file and range.',
222
- '- Preserve the user intent. Identify what failed, what you changed or verified, and the next action.',
223
- '- Finish with `TASK_COMPLETE:` followed by a concise user-facing summary.',
224
- ];
225
- if (priorFailureText.trim()) {
226
- parts.push('', 'Prior failure excerpt:', priorFailureText.trim().slice(0, 1200));
227
- }
228
- return parts.join('\n');
229
- }
230
203
  /**
231
204
  * Strip lone Unicode surrogates (U+D800–U+DFFF) from a string so it can be
232
205
  * safely serialized to JSON. Lone surrogates are valid in JS strings but
@@ -3893,7 +3866,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3893
3866
  responseText = '';
3894
3867
  continue;
3895
3868
  }
3896
- responseText = responseText || contextThrashRecoveryNotice();
3869
+ responseText = responseText || 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
3897
3870
  }
3898
3871
  else if (errStr.includes('prompt is too long') || errStr.includes('prompt too long') || errStr.includes('context_length')) {
3899
3872
  responseText = responseText || ('The conversation got too large to process (tool responses filled the context window). ' +
@@ -3941,7 +3914,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3941
3914
  responseText = '';
3942
3915
  if (contextRecovery) {
3943
3916
  if (contextRecoveryRetries >= 1) {
3944
- responseText = contextThrashRecoveryNotice();
3917
+ responseText = 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
3945
3918
  staleSession = false;
3946
3919
  contextRecovery = false;
3947
3920
  }
@@ -3958,7 +3931,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3958
3931
  }
3959
3932
  }
3960
3933
  if (staleSession && contextRecovery && !responseText.trim()) {
3961
- responseText = contextThrashRecoveryNotice();
3934
+ responseText = 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
3962
3935
  }
3963
3936
  if (hitRateLimit && attempt < PersonalAssistant.RATE_LIMIT_MAX_RETRIES) {
3964
3937
  const base = rateLimitRetryAfterMs
@@ -3992,7 +3965,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
3992
3965
  responseText = '';
3993
3966
  continue;
3994
3967
  }
3995
- responseText = contextThrashRecoveryNotice();
3968
+ responseText = 'I hit a context-window issue mid-task. Try again — run `!clear` if it persists.';
3996
3969
  }
3997
3970
  if (looksLikeNoResponseRequested(responseText)) {
3998
3971
  logger.warn({ sessionKey, attempt }, 'SDK/model returned no-response sentinel during interactive chat');
@@ -84,7 +84,6 @@ export declare class Gateway {
84
84
  */
85
85
  private _deliverDeepResult;
86
86
  private startInteractiveBackgroundTask;
87
- private startContextThrashRecovery;
88
87
  /**
89
88
  * For Clementine-owned sessions, classify whether the message should be
90
89
  * delegated to a specialist agent. Returns null when routing isn't
@@ -7,7 +7,7 @@
7
7
  import path from 'node:path';
8
8
  import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
9
9
  import pino from 'pino';
10
- import { buildContextThrashRecoveryPrompt, contextThrashRecoveryNotice, isAutonomousNothingOutput, looksLikeContextThrashText, looksLikeProviderApiErrorResponse, oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
10
+ import { isAutonomousNothingOutput, looksLikeProviderApiErrorResponse, oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
11
11
  import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
12
12
  import { SelfImproveLoop } from '../agent/self-improve.js';
13
13
  import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
@@ -59,7 +59,7 @@ export function classifyChatError(err) {
59
59
  return 'rate_limit';
60
60
  if (looksLikeClaudeOneMillionContextError(msg))
61
61
  return 'one_million_context';
62
- if (looksLikeContextThrashText(msg) || /context.?length|token.?limit|maximum.?context|prompt.?too.?long/i.test(msg))
62
+ if (/context.?length|token.?limit|maximum.?context|prompt.?too.?long|rapid_refill_breaker|autocompact|context.?refilled/i.test(msg))
63
63
  return 'context_overflow';
64
64
  if (/\b401\b|\b403\b|auth|forbidden|invalid.?api.?key|permission|does not have access|please run \/login/i.test(msg))
65
65
  return 'auth';
@@ -839,46 +839,6 @@ export class Gateway {
839
839
  return opts.ack
840
840
  ?? `On it — running this in the background. I'll follow up when it's done. Task ${task.id}. Reply "status" to check in or "cancel" to stop.`;
841
841
  }
842
- startContextThrashRecovery(sessionKey, text, priorFailureText, details = {}) {
843
- const currentSess = this.getSession(sessionKey);
844
- const jobName = `recovery-${Date.now()}`;
845
- currentSess.deepTask = {
846
- jobName,
847
- taskDesc: `Recover after context overflow: ${text.slice(0, 160)}`,
848
- startedAt: new Date().toISOString(),
849
- };
850
- const agentSlug = this._agentSlugFromSessionKey(sessionKey);
851
- this.recordInteractiveFailure(sessionKey, text, priorFailureText, 'context_thrash', {
852
- jobName,
853
- ...details,
854
- });
855
- this.assistant.runUnleashedTask(jobName, buildContextThrashRecoveryPrompt(text, priorFailureText), 2, undefined, undefined, undefined, 1, agentSlug).then(async (result) => {
856
- if (this.sessions.get(sessionKey)?.deepTask?.jobName !== jobName) {
857
- logger.info({ sessionKey, jobName }, 'Context-thrash recovery resolved after cancellation/replacement; suppressing follow-up');
858
- return;
859
- }
860
- logger.info({ sessionKey, jobName, resultLen: result?.length ?? 0 }, 'Context-thrash recovery completed');
861
- if (result && !isAutonomousNothingOutput(result)) {
862
- this.assistant.injectPendingContext(sessionKey, text, result);
863
- await this._deliverDeepResult(sessionKey, `[CONTEXT_THRASH_RECOVERY_RESULT] You just completed the smaller recovery pass. Summarize the result conversationally and briefly. Lead with whether the original request is fixed, still blocked, or needs approval.\n\nOriginal request: ${text.slice(0, 500)}\n\nResult:\n${result.slice(0, 3000)}`, result);
864
- }
865
- }).catch(async (err) => {
866
- if (this.sessions.get(sessionKey)?.deepTask?.jobName !== jobName) {
867
- logger.info({ sessionKey, jobName }, 'Context-thrash recovery failed after cancellation/replacement; suppressing failure follow-up');
868
- return;
869
- }
870
- logger.error({ err, sessionKey, jobName }, 'Context-thrash recovery failed');
871
- this.recordInteractiveFailure(sessionKey, text, err, 'context_thrash_recovery_failed', { jobName });
872
- const failMsg = `Recovery pass failed: ${String(err).slice(0, 200)}`;
873
- this.assistant.injectPendingContext(sessionKey, text, failMsg);
874
- await this._deliverDeepResult(sessionKey, `[CONTEXT_THRASH_RECOVERY_RESULT] The smaller recovery pass failed: ${failMsg}. Tell the user briefly and suggest checking status/log slices, not full logs.`, failMsg);
875
- }).finally(() => {
876
- const s = this.sessions.get(sessionKey);
877
- if (s?.deepTask?.jobName === jobName)
878
- delete s.deepTask;
879
- });
880
- return `${contextThrashRecoveryNotice()} I restarted it as a smaller background recovery pass and will follow up here.`;
881
- }
882
842
  /**
883
843
  * For Clementine-owned sessions, classify whether the message should be
884
844
  * delegated to a specialist agent. Returns null when routing isn't
@@ -2088,20 +2048,12 @@ export class Gateway {
2088
2048
  delete sessState.pendingInterrupt;
2089
2049
  }
2090
2050
  try {
2091
- // ── Phase 2: opt-in canonical SDK chat path ──────────────────
2092
- // When CLEMENTINE_USE_RUNAGENT_CHAT=1 is set, route through
2093
- // the new runAgent() wrapper instead of the legacy
2094
- // assistant.chat path. This is the SDK-canonical pattern
2095
- // (one query() call, agents map for subagents, no
2096
- // wrapper layers). Today's Phase 2 connects only the
2097
- // Clementine MCP server — Composio/external integrations
2098
- // come in Phase 3. Useful for testing the new path on
2099
- // tool-light sessions like cron-fix or memory queries.
2100
- //
2101
- // The legacy path (default) keeps full Composio/external
2102
- // routing + all post-response handlers, so this flag is
2103
- // safe to leave off until we're ready.
2104
- if (process.env.CLEMENTINE_USE_RUNAGENT_CHAT === '1'
2051
+ // ── Phase 5: canonical SDK chat path is now DEFAULT ──────────
2052
+ // The new runAgent() wrapper is the canonical path. Set
2053
+ // CLEMENTINE_USE_RUNAGENT_CHAT=0 to fall back to legacy.
2054
+ // The legacy path remains as the in-process error fallback
2055
+ // when runAgent throws.
2056
+ if (process.env.CLEMENTINE_USE_RUNAGENT_CHAT !== '0'
2105
2057
  && this.isTrustedPersonalSession(sessionKey)
2106
2058
  && !sessState.pendingInterrupt) {
2107
2059
  const { runAgent } = await import('../agent/run-agent.js');
@@ -2351,13 +2303,6 @@ export class Gateway {
2351
2303
  this.clearSession(effectiveSessionKey);
2352
2304
  return "Claude returned a provider API error instead of a normal answer. I've reset this session so the error does not get replayed into future context. Please try that question again.";
2353
2305
  }
2354
- if (response && looksLikeContextThrashText(response)) {
2355
- logger.warn({ sessionKey, responsePreview: response.slice(0, 200) }, 'Context-thrash text returned from assistant — starting recovery pass');
2356
- return this.startContextThrashRecovery(sessionKey, text, response, {
2357
- toolActivityCount,
2358
- source: 'assistant_response',
2359
- });
2360
- }
2361
2306
  // ── Auto-plan detection ──────────────────────────────────────
2362
2307
  // If the agent signals a complex task, auto-route to the orchestrator
2363
2308
  const planMatch = response?.match(/^\[PLAN_NEEDED:\s*(.+?)\]\s*/);
@@ -2480,13 +2425,6 @@ export class Gateway {
2480
2425
  if (chatAc.signal.aborted) {
2481
2426
  return "Stopped. What would you like to do instead?";
2482
2427
  }
2483
- if (looksLikeContextThrashText(err)) {
2484
- logger.warn({ sessionKey, err: String(err).slice(0, 300) }, 'Context-thrash exception — starting recovery pass');
2485
- return this.startContextThrashRecovery(sessionKey, text, String(err), {
2486
- toolActivityCount,
2487
- source: 'exception',
2488
- });
2489
- }
2490
2428
  // ── Max turns hit — auto-escalate to deep mode instead of failing silently ──
2491
2429
  // This is the #1 cause of "agent stops responding": it ran out of turns
2492
2430
  // exploring files, the SDK throws, and the user gets nothing.
@@ -2578,11 +2516,11 @@ export class Gateway {
2578
2516
  events.emit('heartbeat:start', { agent, timestamp: Date.now() });
2579
2517
  const hbStart = Date.now();
2580
2518
  try {
2581
- // ── Phase 4: opt-in canonical SDK heartbeat path ──────────────
2582
- // CLEMENTINE_USE_RUNAGENT_HEARTBEAT=1 routes through
2583
- // runAgentHeartbeat (no tools, Haiku, single turn). Default OFF;
2584
- // falls back to legacy on error.
2585
- const useRunAgentHeartbeat = process.env.CLEMENTINE_USE_RUNAGENT_HEARTBEAT === '1';
2519
+ // ── Phase 5: canonical SDK heartbeat path is now DEFAULT ──────
2520
+ // runAgentHeartbeat is the canonical path (no tools, Haiku,
2521
+ // single turn). Set CLEMENTINE_USE_RUNAGENT_HEARTBEAT=0 to
2522
+ // fall back to legacy.
2523
+ const useRunAgentHeartbeat = process.env.CLEMENTINE_USE_RUNAGENT_HEARTBEAT !== '0';
2586
2524
  if (useRunAgentHeartbeat) {
2587
2525
  try {
2588
2526
  const { runAgentHeartbeat } = await import('../agent/run-agent-heartbeat.js');
@@ -2638,11 +2576,10 @@ export class Gateway {
2638
2576
  const cronStart = Date.now();
2639
2577
  try {
2640
2578
  let response;
2641
- // ── Phase 3: opt-in canonical SDK cron path ──────────────────
2642
- // CLEMENTINE_USE_RUNAGENT_CRON=1 routes the job through
2643
- // runAgentCron() the canonical SDK pattern. Default OFF.
2644
- // Falls back to legacy on error so the job always completes.
2645
- const useRunAgentCron = process.env.CLEMENTINE_USE_RUNAGENT_CRON === '1';
2579
+ // ── Phase 5: canonical SDK cron path is now DEFAULT ──────────
2580
+ // runAgentCron() is the canonical path. Set
2581
+ // CLEMENTINE_USE_RUNAGENT_CRON=0 to fall back to legacy.
2582
+ const useRunAgentCron = process.env.CLEMENTINE_USE_RUNAGENT_CRON !== '0';
2646
2583
  if (useRunAgentCron && !opts?.disableAllTools) {
2647
2584
  try {
2648
2585
  const { runAgentCron } = await import('../agent/run-agent-cron.js');
@@ -2720,11 +2657,11 @@ export class Gateway {
2720
2657
  const releaseLane = await lanes.acquire('cron');
2721
2658
  try {
2722
2659
  logger.info({ fromSlug, toSlug: profile.slug }, 'Running team message as autonomous task');
2723
- // ── Phase 4: opt-in canonical SDK team-task path ───────────────
2724
- // CLEMENTINE_USE_RUNAGENT_TEAM=1 routes through runAgentTeamTask
2725
- // (one runAgent call, SDK owns the loop no phase wrapper).
2726
- // Default OFF; falls back to legacy on error.
2727
- const useRunAgentTeam = process.env.CLEMENTINE_USE_RUNAGENT_TEAM === '1';
2660
+ // ── Phase 5: canonical SDK team-task path is now DEFAULT ───────
2661
+ // runAgentTeamTask is the canonical path (one runAgent call —
2662
+ // SDK owns the inner loop). Set CLEMENTINE_USE_RUNAGENT_TEAM=0
2663
+ // to fall back to legacy.
2664
+ const useRunAgentTeam = process.env.CLEMENTINE_USE_RUNAGENT_TEAM !== '0';
2728
2665
  if (useRunAgentTeam) {
2729
2666
  try {
2730
2667
  const { runAgentTeamTask } = await import('../agent/run-agent-team-task.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.47",
3
+ "version": "1.18.48",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",