clementine-agent 1.18.22 → 1.18.24

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.
@@ -291,6 +291,10 @@ export declare class PersonalAssistant {
291
291
  };
292
292
  delegateProfile?: AgentProfile;
293
293
  abortSignal?: AbortSignal;
294
+ usageSource?: string;
295
+ usageSessionKey?: string;
296
+ usageLabel?: string;
297
+ usageAgentSlug?: string;
294
298
  }): Promise<string>;
295
299
  runCronJob(jobName: string, jobPrompt: string, tier?: number, maxTurns?: number, model?: string, workDir?: string, timeoutMs?: number, successCriteria?: string[], agentSlug?: string, opts?: {
296
300
  disableAllTools?: boolean;
@@ -179,6 +179,27 @@ export function looksLikeContextThrashText(value) {
179
179
  const text = String(value ?? '');
180
180
  return /autocompact\s+is\s+thrashing|context\s+refilled\s+to\s+the\s+limit|refilled\s+to\s+the\s+limit\s+within/i.test(text);
181
181
  }
182
+ function inferTerminalReasonFromFailure(value) {
183
+ const text = String(value ?? '').toLowerCase();
184
+ if (looksLikeContextThrashText(text) || /rapid_refill_breaker|maximum context|context.?length/.test(text)) {
185
+ return 'rapid_refill_breaker';
186
+ }
187
+ if (/prompt is too long|prompt too long|input is too long|request too large/.test(text)) {
188
+ return 'prompt_too_long';
189
+ }
190
+ if (/maximum number of turns|max_turns/.test(text)) {
191
+ return 'max_turns';
192
+ }
193
+ return undefined;
194
+ }
195
+ class UnleashedTaskFailedError extends Error {
196
+ terminalReason;
197
+ constructor(message, terminalReason) {
198
+ super(message);
199
+ this.terminalReason = terminalReason;
200
+ this.name = 'UnleashedTaskFailedError';
201
+ }
202
+ }
182
203
  export function contextThrashRecoveryNotice() {
183
204
  return [
184
205
  'I hit a context-size recovery issue while working on that.',
@@ -4987,7 +5008,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
4987
5008
  }
4988
5009
  // ── Plan Step Execution ───────────────────────────────────────────
4989
5010
  async runPlanStep(stepId, prompt, opts = {}) {
4990
- const { tier = 2, maxTurns = 15, model, disableTools = false, outputFormat, delegateProfile, abortSignal } = opts;
5011
+ const { tier = 2, maxTurns = 15, model, disableTools = false, outputFormat, delegateProfile, abortSignal, usageSource = 'plan_step', usageSessionKey, usageLabel, usageAgentSlug, } = opts;
4991
5012
  // Don't mutate the global — pass source through the closure instead
4992
5013
  // Per-step stall guard so concurrent steps don't cross-contaminate
4993
5014
  const stepGuard = new StallGuard();
@@ -5034,7 +5055,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5034
5055
  }
5035
5056
  }
5036
5057
  else if (message.type === 'result') {
5037
- this.logQueryResult(message, 'plan_step', `plan:${stepId}`, stepId);
5058
+ this.logQueryResult(message, usageSource, usageSessionKey ?? `plan:${stepId}`, usageLabel ?? stepId, usageAgentSlug);
5038
5059
  }
5039
5060
  }
5040
5061
  return extractDeliverable(trace) ||
@@ -5529,6 +5550,12 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5529
5550
  let lastOutput = '';
5530
5551
  let consecutiveErrors = 0;
5531
5552
  const MAX_CONSECUTIVE_ERRORS = 3;
5553
+ const unleashedContextSafety = [
5554
+ 'CONTEXT SAFETY:',
5555
+ '- Keep each phase bounded. Do not read full run logs, full CRON.md, raw exports, or large integration responses.',
5556
+ '- Pull records in small batches, summarize IDs/counts/statuses, and write bulky intermediate data to files instead of pasting it into the conversation.',
5557
+ '- If the task looks too broad for the remaining context, stop with a compact status summary and pending list rather than retrying broader reads.',
5558
+ ].join('\n');
5532
5559
  while (phase < UNLEASHED_MAX_PHASES) {
5533
5560
  // Check cancellation
5534
5561
  if (fs.existsSync(cancelFile)) {
@@ -5644,6 +5671,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5644
5671
  `After each phase completes, your session will be resumed with fresh context.\n\n` +
5645
5672
  `TASK:\n${jobPrompt}\n\n` +
5646
5673
  unleashedSkillContext +
5674
+ `${unleashedContextSafety}\n\n` +
5647
5675
  `IMPORTANT:\n` +
5648
5676
  `- Work methodically through the task in phases\n` +
5649
5677
  `- At the end of this phase, output a STATUS SUMMARY of what you accomplished and what remains\n` +
@@ -5682,6 +5710,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5682
5710
  `Continuing unleashed task. This is phase ${phase}.\n` +
5683
5711
  `Time remaining: ${remainingHours} hours. You have ${turnsPerPhase} turns this phase.\n` +
5684
5712
  checkpointContext +
5713
+ `\n${unleashedContextSafety}\n` +
5685
5714
  `\nContinue working on the task. Pick up where you left off.\n` +
5686
5715
  `If the task is COMPLETE, output "TASK_COMPLETE:" followed by a final summary.\n\n` +
5687
5716
  `IMPORTANT: Output a STATUS SUMMARY at the end of this phase.`;
@@ -5695,6 +5724,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5695
5724
  `Previous phases encountered an error and the session was reset.\n\n` +
5696
5725
  `TASK:\n${jobPrompt}\n` +
5697
5726
  checkpointContext +
5727
+ `\n${unleashedContextSafety}\n` +
5698
5728
  `\nCheck any files or progress from prior phases, then continue the work.\n` +
5699
5729
  `If the task is COMPLETE, output "TASK_COMPLETE:" followed by a final summary.\n\n` +
5700
5730
  `IMPORTANT: Output a STATUS SUMMARY at the end of this phase.`;
@@ -5827,8 +5857,27 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5827
5857
  catch (err) {
5828
5858
  clearTimeout(phaseTimer);
5829
5859
  clearInterval(beaconTimer);
5860
+ const terminalReason = inferTerminalReasonFromFailure(err);
5861
+ if (terminalReason && !this._lastTerminalReason) {
5862
+ this._lastTerminalReason = terminalReason;
5863
+ }
5830
5864
  logger.error({ err, jobName, phase }, `Unleashed task phase ${phase} error`);
5831
- appendProgress({ event: 'phase_error', phase, error: String(err) });
5865
+ appendProgress({ event: 'phase_error', phase, error: String(err), terminalReason });
5866
+ if (terminalReason === 'rapid_refill_breaker' || terminalReason === 'prompt_too_long') {
5867
+ appendProgress({ event: 'aborted', phase, reason: terminalReason });
5868
+ writeStatus({
5869
+ jobName,
5870
+ status: 'error',
5871
+ phase,
5872
+ startedAt,
5873
+ finishedAt: new Date().toISOString(),
5874
+ terminalReason,
5875
+ });
5876
+ const message = (`Task "${jobName}" aborted in phase ${phase}: ${terminalReason}. ` +
5877
+ `The phase exceeded the context window, so Clementine stopped instead of retrying the same broad task shape.`);
5878
+ logger.error({ jobName, phase, terminalReason }, 'Unleashed task aborted on context-size failure');
5879
+ throw new UnleashedTaskFailedError(message, terminalReason);
5880
+ }
5832
5881
  consecutiveErrors++;
5833
5882
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
5834
5883
  appendProgress({ event: 'aborted', phase, reason: `${MAX_CONSECUTIVE_ERRORS} consecutive phase errors` });
@@ -5837,13 +5886,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5837
5886
  const errorResult = lastOutput || (`Task "${jobName}" aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive phase errors. ` +
5838
5887
  `Check \`clementine cron runs ${jobName}\` for the failing phase, or retry with ` +
5839
5888
  `\`clementine cron run ${jobName}\`.`);
5840
- if (this.onUnleashedComplete) {
5841
- try {
5842
- this.onUnleashedComplete(jobName, errorResult);
5843
- }
5844
- catch { /* non-fatal */ }
5845
- }
5846
- return errorResult;
5889
+ throw new UnleashedTaskFailedError(errorResult, this._lastTerminalReason);
5847
5890
  }
5848
5891
  // On error, try to continue with a fresh session
5849
5892
  sessionId = '';
@@ -5930,13 +5973,7 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5930
5973
  writeStatus({ jobName, status: 'max_phases', phase, startedAt, finishedAt: new Date().toISOString() });
5931
5974
  logger.warn(`Unleashed task ${jobName} hit max phases (${UNLEASHED_MAX_PHASES})`);
5932
5975
  const maxPhasesResult = lastOutput || `Task "${jobName}" reached maximum phase limit (${UNLEASHED_MAX_PHASES}).`;
5933
- if (this.onUnleashedComplete) {
5934
- try {
5935
- this.onUnleashedComplete(jobName, maxPhasesResult);
5936
- }
5937
- catch { /* non-fatal */ }
5938
- }
5939
- return maxPhasesResult;
5976
+ throw new UnleashedTaskFailedError(maxPhasesResult, this._lastTerminalReason);
5940
5977
  }
5941
5978
  // ── Team Task Execution (Unleashed for Team Messages) ────────────
5942
5979
  /**
@@ -31,6 +31,7 @@ export declare function createBackgroundTask(input: {
31
31
  fromAgent: string;
32
32
  prompt: string;
33
33
  maxMinutes: number;
34
+ sessionKey?: string;
34
35
  }, opts?: BackgroundTaskOptions): BackgroundTask;
35
36
  /** Load a task by id, or null if not found / malformed. */
36
37
  export declare function loadBackgroundTask(id: string, opts?: BackgroundTaskOptions): BackgroundTask | null;
@@ -51,6 +52,8 @@ export declare function markFailed(id: string, error: string, reason?: 'failed'
51
52
  * Returns the count of tasks aborted.
52
53
  */
53
54
  export declare function abortStaleRunningTasks(opts?: BackgroundTaskOptions): number;
54
- /** Test-only: delete a task file. Production code never deletes history matters. */
55
- export declare function _deleteBackgroundTask(id: string, opts?: BackgroundTaskOptions): void;
55
+ /** Delete a task file. Callers should avoid deleting active tasks. */
56
+ export declare function deleteBackgroundTask(id: string, opts?: BackgroundTaskOptions): void;
57
+ /** Backward-compatible test helper alias. */
58
+ export declare const _deleteBackgroundTask: typeof deleteBackgroundTask;
56
59
  //# sourceMappingURL=background-tasks.d.ts.map
@@ -56,6 +56,8 @@ export function createBackgroundTask(input, opts) {
56
56
  status: 'pending',
57
57
  createdAt: now.toISOString(),
58
58
  };
59
+ if (input.sessionKey)
60
+ task.sessionKey = input.sessionKey;
59
61
  safeWrite(pathFor(task.id, opts), task);
60
62
  return task;
61
63
  }
@@ -104,6 +106,8 @@ export function markRunning(id, opts) {
104
106
  const task = loadBackgroundTask(id, opts);
105
107
  if (!task)
106
108
  return null;
109
+ if (task.status !== 'pending')
110
+ return null;
107
111
  task.status = 'running';
108
112
  task.startedAt = new Date().toISOString();
109
113
  safeWrite(pathFor(id, opts), task);
@@ -114,6 +118,8 @@ export function markDone(id, result, deliverableNote, opts) {
114
118
  const task = loadBackgroundTask(id, opts);
115
119
  if (!task)
116
120
  return null;
121
+ if (task.status !== 'running')
122
+ return task;
117
123
  task.status = 'done';
118
124
  task.completedAt = new Date().toISOString();
119
125
  task.result = result;
@@ -127,6 +133,8 @@ export function markFailed(id, error, reason = 'failed', opts) {
127
133
  const task = loadBackgroundTask(id, opts);
128
134
  if (!task)
129
135
  return null;
136
+ if (task.status === 'done' || task.status === 'failed' || task.status === 'aborted')
137
+ return task;
130
138
  task.status = reason;
131
139
  task.completedAt = new Date().toISOString();
132
140
  task.error = error.slice(0, 1000);
@@ -147,8 +155,8 @@ export function abortStaleRunningTasks(opts) {
147
155
  }
148
156
  return aborted;
149
157
  }
150
- /** Test-only: delete a task file. Production code never deletes history matters. */
151
- export function _deleteBackgroundTask(id, opts) {
158
+ /** Delete a task file. Callers should avoid deleting active tasks. */
159
+ export function deleteBackgroundTask(id, opts) {
152
160
  try {
153
161
  const file = pathFor(id, opts);
154
162
  if (existsSync(file))
@@ -156,4 +164,6 @@ export function _deleteBackgroundTask(id, opts) {
156
164
  }
157
165
  catch { /* ignore */ }
158
166
  }
167
+ /** Backward-compatible test helper alias. */
168
+ export const _deleteBackgroundTask = deleteBackgroundTask;
159
169
  //# sourceMappingURL=background-tasks.js.map
@@ -14,15 +14,20 @@ function wordCount(text) {
14
14
  }
15
15
  export function isStopRequest(text) {
16
16
  const n = normalize(text);
17
+ if (/\bbg-[a-z0-9]+-[a-f0-9]{6}\b/i.test(n) && /^(stop|cancel|abort)\b/.test(n))
18
+ return true;
17
19
  if (wordCount(n) > 5)
18
20
  return false;
19
- return /^(stop|cancel|abort|halt|pause|nevermind|never mind|wait stop|stop please|cancel that|stop that)$/.test(n);
21
+ return /^(stop|cancel|abort|halt|pause|nevermind|never mind|wait stop|stop please|cancel that|stop that|cancel it|stop it|cancel task|stop task|cancel the task|stop the task|cancel background|stop background)$/.test(n);
20
22
  }
21
23
  export function isStatusRequest(text) {
22
24
  const n = normalize(text);
23
- if (wordCount(n) > 8)
25
+ if (wordCount(n) > 12)
24
26
  return false;
25
- return /^(status|task status|deep status|progress|what'?s happening|what'?s going on|what are you doing|what are you working on|what are you running|are you working|anything running|what'?s runnin?g?(?: now| right now)?|what is runnin?g?(?: now| right now)?|background status|check status|where are we)$/.test(n);
27
+ if (/\bbg-[a-z0-9]+-[a-f0-9]{6}\b/i.test(n) && /\b(status|progress|check|update|running|done|finished)\b/.test(n)) {
28
+ return true;
29
+ }
30
+ return /^(status|task status|deep status|progress|progress update|what'?s happening|what'?s going on|what are you doing|what are you working on|what are you running|are you working|anything running|what'?s runnin?g?(?: now| right now)?|what is runnin?g?(?: now| right now)?|background status|check status|where are we|any update|any updates|can i get an update|do you have an update|update me|is it done|is it done yet|is it finished|is it finished yet|done yet|did it finish|still running|is it still running|are we done|how'?s (?:it|that|this|the task|the job|the run|the background task) (?:coming along|progressing)|how is (?:it|that|this|the task|the job|the run|the background task) (?:coming along|progressing)|how'?s (?:the task|the job|the run|the background task) going|how is (?:the task|the job|the run|the background task) going)$/.test(n);
26
31
  }
27
32
  export function isLastActionRequest(text) {
28
33
  const n = normalize(text);
@@ -105,7 +105,7 @@ export function loadPromptOverridesForJob(jobName, agentSlug, opts) {
105
105
  if (o.scope === 'agent')
106
106
  return agentSlug != null && o.scopeKey === agentSlug;
107
107
  if (o.scope === 'job')
108
- return o.scopeKey === jobName;
108
+ return o.scopeKey === jobName || o.scopeKey === bareJobName(jobName);
109
109
  return false;
110
110
  });
111
111
  if (applicable.length === 0)
@@ -113,6 +113,9 @@ export function loadPromptOverridesForJob(jobName, agentSlug, opts) {
113
113
  applicable.sort((a, b) => a.priority - b.priority);
114
114
  return applicable.map(o => o.body).join('\n\n');
115
115
  }
116
+ function bareJobName(jobName) {
117
+ return jobName.includes(':') ? jobName.split(':').slice(1).join(':') : jobName;
118
+ }
116
119
  /** Install fs.watch on the overrides directory tree. Safe to call multiple times. */
117
120
  export function watchPromptOverrides(opts) {
118
121
  if (watcherInstalled)
@@ -12,6 +12,7 @@ const GOAL_REF_RE = /\b(goal|goals|objective|objectives|blocker|next action|next
12
12
  const LOCAL_TOOL_RE = /\b(repo|repository|code|file|files|folder|directory|path|log|logs|config|build|test|typecheck|lint|npm|git|commit|push|pull|branch|diff|patch|edit|write|implement|fix|refactor|run|diagnose|investigate|troubleshoot|cron|scheduler|lease)\b/i;
13
13
  const COMPLEX_RE = /\b(multiple|several|many|bulk|batch|parallel|deep mode|background|research|analyze|audit|review|across|end to end|entire)\b/i;
14
14
  const ADMIN_RE = /\b(self[- ]?update|restart|daemon|npm publish|publish to npm|doctor|integration|credential|env var|environment variable|set up|setup|configure)\b/i;
15
+ const BACKGROUND_STATUS_FOLLOWUP_RE = /\bbg-[a-z0-9]+-[a-f0-9]{6}\b|\b(status|progress|progress update|any updates?|done yet|did it finish|still running|coming along|background status)\b/i;
15
16
  const STANDALONE_GREETINGS = new Set([
16
17
  'hi',
17
18
  'hey',
@@ -90,6 +91,18 @@ export function decideTurnPolicy(input) {
90
91
  reason: 'explicit-full-surface',
91
92
  };
92
93
  }
94
+ if (input.hasRecentContext && BACKGROUND_STATUS_FOLLOWUP_RE.test(text)) {
95
+ return {
96
+ retrievalTier: 'search',
97
+ disableAllTools: false,
98
+ enableTeams: false,
99
+ maxTurns: Math.min(intent.suggestedMaxTurns, 6),
100
+ effort: 'low',
101
+ allowProactiveGoals: false,
102
+ fetchLinks: false,
103
+ reason: 'background-status-followup',
104
+ };
105
+ }
93
106
  if (isStandaloneGreeting(text)) {
94
107
  return {
95
108
  retrievalTier: 'none',
@@ -230,6 +230,10 @@ export class WorkflowRunner {
230
230
  tier: resolvedStep.tier,
231
231
  maxTurns: resolvedStep.maxTurns,
232
232
  model: resolvedStep.model,
233
+ usageSource: 'workflow_step',
234
+ usageSessionKey: `workflow:${workflow.name}:${step.id}`,
235
+ usageLabel: `${workflow.name}:${step.id}`,
236
+ usageAgentSlug: workflow.agentSlug,
233
237
  });
234
238
  return { stepId: step.id, result, durationMs: Date.now() - stepStart };
235
239
  }), MAX_CONCURRENT_STEPS);
@@ -269,6 +273,10 @@ export class WorkflowRunner {
269
273
  try {
270
274
  finalOutput = await this.assistant.runPlanStep('__synthesis__', synthPrompt, {
271
275
  tier: 2, maxTurns: 5, disableTools: true,
276
+ usageSource: 'workflow_step',
277
+ usageSessionKey: `workflow:${workflow.name}:__synthesis__`,
278
+ usageLabel: `${workflow.name}:__synthesis__`,
279
+ usageAgentSlug: workflow.agentSlug,
272
280
  });
273
281
  }
274
282
  catch (err) {