clementine-agent 1.18.207 → 1.18.209

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.
@@ -441,7 +441,7 @@ export async function runAgent(prompt, opts) {
441
441
  filePath: info.filePath,
442
442
  contentBytes: info.contentBytes,
443
443
  ...(info.archivePath ? { archivePath: info.archivePath } : {}),
444
- message: 'Large Write completed out-of-band; native Write tool denied to protect parent context.',
444
+ message: 'Large Write content restored after placeholder Write to protect parent context.',
445
445
  },
446
446
  });
447
447
  },
@@ -212,6 +212,11 @@ function extractProviderLogId(raw) {
212
212
  ?? (obj.data && typeof obj.data === 'object' ? extractProviderLogId(obj.data) : undefined);
213
213
  }
214
214
  function statusPhrase(call) {
215
+ if (call.result && !call.result.successful) {
216
+ const error = call.result.error ? `: ${call.result.error}` : '';
217
+ const status = call.result.statusCode ? ` (${call.result.statusCode})` : '';
218
+ return `failed${status}${error}`.slice(0, 260);
219
+ }
215
220
  const status = call.result?.statusCode;
216
221
  if (status && toolKindLabel(call.toolName) === 'email sends')
217
222
  return `accepted (${status})`;
@@ -289,10 +294,20 @@ export function formatOverflowRecoveryMessage(summary) {
289
294
  || summary.failedDelegations.length > 0
290
295
  || summary.pendingDelegations.length > 0) {
291
296
  lines.push('Needs attention:');
292
- if (summary.failedSideEffects.length > 0)
293
- lines.push(...formatGroupedLines('failed', summary.failedSideEffects));
294
- if (summary.pendingSideEffects.length > 0)
295
- lines.push(...formatGroupedLines('started, no confirmation', summary.pendingSideEffects));
297
+ if (summary.failedSideEffects.length > 0) {
298
+ for (const call of summary.failedSideEffects.slice(0, 5))
299
+ lines.push(formatDetailedCall(call));
300
+ if (summary.failedSideEffects.length > 5) {
301
+ lines.push(`- ...and ${summary.failedSideEffects.length - 5} more failed side effects`);
302
+ }
303
+ }
304
+ if (summary.pendingSideEffects.length > 0) {
305
+ for (const call of summary.pendingSideEffects.slice(0, 5))
306
+ lines.push(formatDetailedCall(call));
307
+ if (summary.pendingSideEffects.length > 5) {
308
+ lines.push(`- ...and ${summary.pendingSideEffects.length - 5} more side effects started with no confirmation`);
309
+ }
310
+ }
296
311
  for (const call of summary.failedDelegations.slice(0, 5))
297
312
  lines.push(formatDelegationCall(call, 'failed'));
298
313
  for (const call of summary.pendingDelegations.slice(0, 5))
@@ -312,10 +327,14 @@ function formatDetailedCall(call) {
312
327
  const recipients = extractRecipients(call.input);
313
328
  const subject = extractSubject(call.input);
314
329
  const filePath = extractFilePath(call.input, call.result?.raw);
330
+ const command = call.toolName === 'Bash'
331
+ ? firstString(call.input.command)?.replace(/\s+/g, ' ').slice(0, 180)
332
+ : undefined;
315
333
  const logId = call.result ? extractProviderLogId(call.result.raw) : undefined;
316
334
  const parts = [
317
335
  toolKindLabel(call.toolName),
318
336
  filePath ? `file ${filePath}` : undefined,
337
+ command ? `command "${command}"` : undefined,
319
338
  recipients.length ? `to ${recipients.join(', ')}` : undefined,
320
339
  subject ? `subject "${subject}"` : undefined,
321
340
  call.result ? statusPhrase(call) : 'started, no confirmation',
@@ -85,6 +85,8 @@ const BASH_SIDE_EFFECT_PATTERNS = [
85
85
  /\btee\b/i,
86
86
  /\bgit\s+(commit|push|merge|rebase|tag)\b/i,
87
87
  /\bnpm\s+(install|publish|update)\b/i,
88
+ /\bnetlify\s+deploy\b/i,
89
+ /\bvercel\s+(deploy|--prod)\b/i,
88
90
  /\b(?:sf|sfdx)\s+data\s+(update|delete|create|upsert)\b/i,
89
91
  /\b(?:sf|sfdx)\s+org\s+(create|delete)\b/i,
90
92
  /\bcurl\b.*(?:-X|--request)\s*(POST|PUT|DELETE|PATCH)\b/i,
@@ -200,10 +202,26 @@ function findError(value) {
200
202
  }
201
203
  return undefined;
202
204
  }
205
+ function findStringError(value) {
206
+ if (typeof value !== 'string')
207
+ return undefined;
208
+ const text = value.trim();
209
+ if (!text)
210
+ return undefined;
211
+ const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? text;
212
+ const prefixed = firstLine.match(/^(?:›\s*)?(?:error|failed|fatal):\s+(.+)$/i);
213
+ if (prefixed)
214
+ return prefixed[1]?.slice(0, 500);
215
+ const known = text.match(/\b(Project not found\. Please rerun "netlify link"|command not found|No such file or directory|deploy failed|build failed)\b/i);
216
+ return known?.[1]?.slice(0, 500);
217
+ }
203
218
  export function isToolResultSuccessful(rawResult, sdkIsError = false) {
204
219
  if (sdkIsError)
205
220
  return { successful: false, reason: 'sdk-is-error' };
206
221
  const result = normalizeResultPayload(rawResult);
222
+ const stringError = findStringError(result);
223
+ if (stringError)
224
+ return { successful: false, reason: 'tool-result-error-string', error: stringError };
207
225
  if (result && typeof result === 'object') {
208
226
  const obj = result;
209
227
  if (obj.is_error === true || obj.isError === true) {
@@ -22,8 +22,9 @@
22
22
  * The fix is the canonical Anthropic primitive: a `PostToolUse` hook that
23
23
  * returns `hookSpecificOutput.updatedToolOutput` to replace the result
24
24
  * before it reaches the model. A companion `PreToolUse` hook handles large
25
- * `Write` inputs by writing the artifact to disk before the native tool can
26
- * echo a giant file body into the parent conversation.
25
+ * `Write` inputs by swapping the native call to a small placeholder; the
26
+ * `PostToolUse` hook then restores the real artifact on disk after the
27
+ * placeholder write succeeds.
27
28
  *
28
29
  * Design properties
29
30
  * ─────────────────
@@ -22,8 +22,9 @@
22
22
  * The fix is the canonical Anthropic primitive: a `PostToolUse` hook that
23
23
  * returns `hookSpecificOutput.updatedToolOutput` to replace the result
24
24
  * before it reaches the model. A companion `PreToolUse` hook handles large
25
- * `Write` inputs by writing the artifact to disk before the native tool can
26
- * echo a giant file body into the parent conversation.
25
+ * `Write` inputs by swapping the native call to a small placeholder; the
26
+ * `PostToolUse` hook then restores the real artifact on disk after the
27
+ * placeholder write succeeds.
27
28
  *
28
29
  * Design properties
29
30
  * ─────────────────
@@ -100,6 +101,7 @@ const VERBOSE_FIELDS = [
100
101
  'message', 'transcript', 'raw', 'rawBody', 'rawMessage', 'contentText', 'plainText',
101
102
  ];
102
103
  const LARGE_WRITE_INPUT_BYTES = 8_000;
104
+ const LARGE_WRITE_PLACEHOLDER = '[Clementine large-write guard placeholder: full content is restored after native Write succeeds.]';
103
105
  function writeArchiveFile(baseDir, runId, toolUseId, toolName, suffix, payload) {
104
106
  try {
105
107
  const dir = join(baseDir, 'tool-archive', runId);
@@ -418,6 +420,7 @@ export function buildGuardHooks(opts) {
418
420
  return { hooks: {}, stats };
419
421
  }
420
422
  const config = opts.config ?? defaultGuardConfig();
423
+ const pendingLargeWrites = new Map();
421
424
  const preToolUse = async (input, toolUseID) => {
422
425
  if (input.hook_event_name !== 'PreToolUse') {
423
426
  return {};
@@ -431,20 +434,12 @@ export function buildGuardHooks(opts) {
431
434
  if (!large)
432
435
  return {};
433
436
  const archivePath = writeArchiveFile(opts.archiveBaseDir ?? BASE_DIR, opts.runId, toolUseId, toolName, 'input', evt.tool_input);
434
- try {
435
- writeLargeFileOutOfBand(large.filePath, large.content);
436
- }
437
- catch (err) {
438
- logger.warn({
439
- err,
440
- toolName,
441
- toolUseId,
442
- filePath: large.filePath,
443
- contentBytes: large.contentBytes,
444
- }, 'tool-output-guard: large Write out-of-band write failed; allowing native tool');
445
- return {};
446
- }
447
- stats.largeWrites += 1;
437
+ pendingLargeWrites.set(toolUseId, {
438
+ filePath: large.filePath,
439
+ content: large.content,
440
+ contentBytes: large.contentBytes,
441
+ archivePath,
442
+ });
448
443
  stats.bytesShed += Math.max(0, large.contentBytes - 400);
449
444
  logger.info({
450
445
  toolName,
@@ -452,33 +447,21 @@ export function buildGuardHooks(opts) {
452
447
  filePath: large.filePath,
453
448
  contentBytes: large.contentBytes,
454
449
  archivePath,
455
- }, 'tool-output-guard: completed large Write out-of-band');
456
- if (opts.onLargeWrite) {
457
- try {
458
- opts.onLargeWrite({
459
- toolName,
460
- toolUseId,
461
- filePath: large.filePath,
462
- contentBytes: large.contentBytes,
463
- archivePath,
464
- });
465
- }
466
- catch { /* best-effort */ }
467
- }
450
+ }, 'tool-output-guard: staged large Write with placeholder input');
468
451
  const reason = [
469
- `Clementine large-write guard already wrote ${formatBytes(large.contentBytes)} to ${large.filePath}.`,
452
+ `Clementine large-write guard staged ${formatBytes(large.contentBytes)} for ${large.filePath}.`,
470
453
  archivePath ? `Full original Write input archived at ${archivePath}.` : undefined,
471
- 'Do not retry Write. Treat the file creation as complete and continue with the remaining requested steps, such as verification or deploy.',
454
+ 'The native Write will use a small placeholder, then Clementine will restore the full content after the write succeeds. Continue with the remaining requested steps after this tool result.',
472
455
  ].filter(Boolean).join(' ');
473
456
  return {
474
457
  hookSpecificOutput: {
475
458
  hookEventName: 'PreToolUse',
476
- permissionDecision: 'deny',
459
+ permissionDecision: 'allow',
477
460
  permissionDecisionReason: reason,
478
461
  additionalContext: reason,
479
462
  updatedInput: {
480
463
  file_path: large.filePath,
481
- content: `[Clementine large-write guard wrote the full ${formatBytes(large.contentBytes)} content out-of-band. ${archivePath ? `Original input: ${archivePath}` : 'Original input was not archived.'}]`,
464
+ content: LARGE_WRITE_PLACEHOLDER,
482
465
  },
483
466
  },
484
467
  };
@@ -494,6 +477,65 @@ export function buildGuardHooks(opts) {
494
477
  const toolUseId = String(toolUseID ?? evt.tool_use_id ?? 'unknown');
495
478
  const rawOutput = evt.tool_response;
496
479
  stats.inspected += 1;
480
+ const pendingLargeWrite = toolName === 'Write' ? pendingLargeWrites.get(toolUseId) : undefined;
481
+ if (pendingLargeWrite) {
482
+ pendingLargeWrites.delete(toolUseId);
483
+ try {
484
+ writeLargeFileOutOfBand(pendingLargeWrite.filePath, pendingLargeWrite.content);
485
+ }
486
+ catch (err) {
487
+ logger.warn({
488
+ err,
489
+ toolName,
490
+ toolUseId,
491
+ filePath: pendingLargeWrite.filePath,
492
+ contentBytes: pendingLargeWrite.contentBytes,
493
+ archivePath: pendingLargeWrite.archivePath,
494
+ }, 'tool-output-guard: failed to restore large Write content after placeholder write');
495
+ return {
496
+ hookSpecificOutput: {
497
+ hookEventName: 'PostToolUse',
498
+ additionalContext: [
499
+ `Clementine could not restore the staged ${formatBytes(pendingLargeWrite.contentBytes)} Write content to ${pendingLargeWrite.filePath}.`,
500
+ pendingLargeWrite.archivePath ? `The original input is archived at ${pendingLargeWrite.archivePath}.` : undefined,
501
+ 'Retry by writing the file in a smaller way or ask the owner for the archive path.',
502
+ ].filter(Boolean).join(' '),
503
+ },
504
+ };
505
+ }
506
+ stats.largeWrites += 1;
507
+ logger.info({
508
+ toolName,
509
+ toolUseId,
510
+ filePath: pendingLargeWrite.filePath,
511
+ contentBytes: pendingLargeWrite.contentBytes,
512
+ archivePath: pendingLargeWrite.archivePath,
513
+ }, 'tool-output-guard: restored large Write content after placeholder write');
514
+ if (opts.onLargeWrite) {
515
+ try {
516
+ opts.onLargeWrite({
517
+ toolName,
518
+ toolUseId,
519
+ filePath: pendingLargeWrite.filePath,
520
+ contentBytes: pendingLargeWrite.contentBytes,
521
+ archivePath: pendingLargeWrite.archivePath,
522
+ });
523
+ }
524
+ catch { /* best-effort */ }
525
+ }
526
+ const restoredMessage = [
527
+ `File created successfully at: ${pendingLargeWrite.filePath}`,
528
+ `(Clementine restored ${formatBytes(pendingLargeWrite.contentBytes)} of large generated content after native Write used a placeholder to protect context.)`,
529
+ pendingLargeWrite.archivePath ? `Original Write input archived at: ${pendingLargeWrite.archivePath}` : undefined,
530
+ ].filter(Boolean).join(' ');
531
+ return {
532
+ hookSpecificOutput: {
533
+ hookEventName: 'PostToolUse',
534
+ additionalContext: `The full file content is present at ${pendingLargeWrite.filePath}. Do not rewrite it; continue with verification, deployment, or the next requested step.`,
535
+ updatedToolOutput: restoredMessage,
536
+ },
537
+ };
538
+ }
497
539
  const usageRatio = Math.max(opts.usageRatio ? safeRatio(opts.usageRatio) : 0, stats.compactions > 0 ? 0.75 : 0);
498
540
  const { softCap } = resolveCap(toolName, config, usageRatio);
499
541
  const originalBytes = estimateBytes(rawOutput);
package/dist/config.js CHANGED
@@ -553,9 +553,10 @@ export const TOOL_OUTPUT_GUARD = {
553
553
  hardLimitBytes: getEnvOrJsonNumber('TOOL_OUTPUT_GUARD_HARD_BYTES', json.toolOutputGuard?.hardLimitBytes, 200_000),
554
554
  adaptive: boolEnv('TOOL_OUTPUT_GUARD_ADAPTIVE', json.toolOutputGuard?.adaptive, true),
555
555
  // Agent results are especially dangerous: even a "medium" subagent report
556
- // refills the parent orchestrator after compaction. Keep the handoff tight;
556
+ // refills the parent orchestrator after compaction. Full-file `Read` calls
557
+ // are the same failure class for generated HTML/reports. Keep both tight;
557
558
  // the full result is archived by tool-output-guard.
558
- perTool: { Agent: 4_000, ...(json.toolOutputGuard?.perTool ?? {}) },
559
+ perTool: { Agent: 4_000, Read: 10_000, ...(json.toolOutputGuard?.perTool ?? {}) },
559
560
  };
560
561
  export const DEFAULT_MODEL_TIER = (getEnvOrJson('DEFAULT_MODEL_TIER', json.models?.default, 'sonnet'));
561
562
  export const MODEL = MODELS[DEFAULT_MODEL_TIER] ?? MODELS.sonnet;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.207",
3
+ "version": "1.18.209",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",