clementine-agent 1.18.207 → 1.18.208

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
  },
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.207",
3
+ "version": "1.18.208",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",