acp-kernel 0.0.82 → 0.0.83-pr.358.229

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.
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  resolvePrompts,
17
17
  thinkingTokenValue,
18
18
  viableRanges
19
- } from "./chunk-VPRKYTHW.js";
19
+ } from "./chunk-6RNVHY67.js";
20
20
  import {
21
21
  activeBlocks,
22
22
  advanceSurvival,
@@ -26,7 +26,7 @@ import {
26
26
  coveredMessageIds,
27
27
  createInitialState,
28
28
  highestActiveTier
29
- } from "./chunk-5TQA7ZQM.js";
29
+ } from "./chunk-MXL3G3BN.js";
30
30
 
31
31
  // src/refs.ts
32
32
  var REF_WIDTH = 5;
@@ -372,9 +372,29 @@ function defaultConfig(modelContextLimit, overrides = {}) {
372
372
  absorb: {
373
373
  enabled: false,
374
374
  toolName: "absorb",
375
- minToolTokens: 1e3,
375
+ // Raised 1000 → 4000 (issue #352): lossless CCR takes over large-result
376
+ // handling; absorb's forced distillation only fires above the new bar.
377
+ minToolTokens: 4e3,
376
378
  contextThresholdPct: 0,
377
379
  excludeTools: []
380
+ },
381
+ crush: {
382
+ enabled: false,
383
+ minReduction: 0.1
384
+ },
385
+ imageCompression: {
386
+ enabled: false,
387
+ minTokens: 512,
388
+ maxDimension: 1280,
389
+ quality: 80,
390
+ format: "webp"
391
+ },
392
+ ccr: {
393
+ enabled: false,
394
+ toolName: "acp_retrieve",
395
+ minToolTokens: 4e3,
396
+ excludeTools: [],
397
+ maxHeadChars: 96
378
398
  }
379
399
  };
380
400
  return {
@@ -384,7 +404,10 @@ function defaultConfig(modelContextLimit, overrides = {}) {
384
404
  nudge: { ...base.nudge, ...overrides.nudge },
385
405
  truncate: { ...base.truncate, ...overrides.truncate },
386
406
  compress: { ...base.compress, ...overrides.compress },
387
- absorb: overrides.absorb ? { ...base.absorb, ...overrides.absorb } : base.absorb
407
+ absorb: overrides.absorb ? { ...base.absorb, ...overrides.absorb } : base.absorb,
408
+ crush: overrides.crush ? { ...base.crush, ...overrides.crush } : base.crush,
409
+ imageCompression: overrides.imageCompression ? { ...base.imageCompression, ...overrides.imageCompression } : base.imageCompression,
410
+ ccr: overrides.ccr ? { ...base.ccr, ...overrides.ccr } : base.ccr
388
411
  };
389
412
  }
390
413
  function validateConfig(config) {
@@ -439,470 +462,84 @@ function validateConfig(config) {
439
462
  errors.push("rules.maxRuleChars must be >= 1");
440
463
  }
441
464
  }
442
- return errors;
443
- }
444
-
445
- // src/boundaries.ts
446
- var MESSAGE_REF_PATTERN = /^m0*(\d{1,5})$/;
447
- var BLOCK_REF_PATTERN = /^b(\d{1,9})$/;
448
- function parseBoundary(ref) {
449
- const normalized = ref.trim().toLowerCase();
450
- const messageMatch = MESSAGE_REF_PATTERN.exec(normalized);
451
- if (messageMatch) {
452
- const numericId = Number(messageMatch[1]);
453
- if (numericId >= 1 && numericId <= 99999) {
454
- return { kind: "message", numericId, raw: normalized };
465
+ if (config.crush) {
466
+ if (!Number.isFinite(config.crush.minReduction) || config.crush.minReduction <= 0 || config.crush.minReduction > 1) {
467
+ errors.push("crush.minReduction must be in (0, 1]");
455
468
  }
456
- }
457
- const blockMatch = BLOCK_REF_PATTERN.exec(normalized);
458
- if (blockMatch) {
459
- const numericId = Number(blockMatch[1]);
460
- if (numericId >= 1) return { kind: "block", numericId, raw: normalized };
461
- }
462
- return null;
463
- }
464
- var BoundaryNotFoundError = class extends Error {
465
- code = "BOUNDARY_NOT_FOUND";
466
- kind;
467
- endpoint;
468
- constructor(kind, endpoint, message) {
469
- super(message);
470
- this.name = "BoundaryNotFoundError";
471
- this.code = "BOUNDARY_NOT_FOUND";
472
- this.kind = kind;
473
- this.endpoint = endpoint;
474
- }
475
- };
476
- function resolveBoundaries(input) {
477
- const start = parseBoundary(input.startRef);
478
- const end = parseBoundary(input.endRef);
479
- if (!start || !end) {
480
- throw new Error(
481
- `Invalid boundary ref(s): startId="${input.startRef}", endId="${input.endRef}". Use mNNNNN or bN.`
482
- );
483
- }
484
- const indexByMessageId = /* @__PURE__ */ new Map();
485
- input.messages.forEach(
486
- (message, index) => indexByMessageId.set(message.id, index)
487
- );
488
- let snappedBoundaries = [];
489
- const startAnchor = resolveAnchorIndex(
490
- start,
491
- input.state,
492
- indexByMessageId,
493
- "start"
494
- );
495
- if (startAnchor.snapped) snappedBoundaries.push(startAnchor.snapped);
496
- const endAnchor = resolveAnchorIndex(
497
- end,
498
- input.state,
499
- indexByMessageId,
500
- "end"
501
- );
502
- if (endAnchor.snapped) snappedBoundaries.push(endAnchor.snapped);
503
- let startIndex = startAnchor.index;
504
- let endIndex = endAnchor.index;
505
- if (startIndex > endIndex) {
506
- [startIndex, endIndex] = [endIndex, startIndex];
507
- }
508
- const messageIds = [];
509
- for (let index = startIndex; index <= endIndex; index++) {
510
- const message = input.messages[index];
511
- if (message && !isRenderedSummaryMessage(message))
512
- messageIds.push(message.id);
513
- }
514
- const boundaryKind = start.kind === "block" || end.kind === "block" ? "block" : "message";
515
- const nestedBlockIds = [];
516
- const nestedSeen = /* @__PURE__ */ new Set();
517
- for (const block of activeBlocks(input.state)) {
518
- if (blockVisibleInRange(block, indexByMessageId, startIndex, endIndex)) {
519
- if (!nestedSeen.has(block.blockId)) {
520
- nestedSeen.add(block.blockId);
521
- nestedBlockIds.push(block.blockId);
469
+ if (config.crush.strategies) {
470
+ for (const [id, ov] of Object.entries(config.crush.strategies)) {
471
+ if (!ov || typeof ov !== "object" || Array.isArray(ov)) {
472
+ errors.push(`crush.strategies.${id} must be an object`);
473
+ continue;
474
+ }
475
+ if (ov.enabled !== void 0 && typeof ov.enabled !== "boolean") {
476
+ errors.push(`crush.strategies.${id}.enabled must be a boolean`);
477
+ }
478
+ if (ov.excludeTools !== void 0 && (!Array.isArray(ov.excludeTools) || ov.excludeTools.some((t) => typeof t !== "string"))) {
479
+ errors.push(
480
+ `crush.strategies.${id}.excludeTools must be a string array`
481
+ );
482
+ }
522
483
  }
523
484
  }
524
485
  }
525
- const protectedGaps = [];
526
- return {
527
- startIndex,
528
- endIndex,
529
- messageIds,
530
- nestedBlockIds,
531
- boundaryKind,
532
- protectedGaps,
533
- snappedBoundaries
534
- };
535
- }
536
- function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
537
- const label = endpoint === "start" ? "startId" : "endId";
538
- if (boundary.kind === "message") {
539
- const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
540
- if (!rawId) {
541
- throw new BoundaryNotFoundError(
542
- "unknown",
543
- endpoint,
544
- `${label}="${boundary.raw}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
545
- );
486
+ if (config.imageCompression) {
487
+ const ic = config.imageCompression;
488
+ if (ic.minTokens !== void 0 && (!Number.isFinite(ic.minTokens) || ic.minTokens < 0)) {
489
+ errors.push("imageCompression.minTokens must be finite and >= 0");
546
490
  }
547
- const index = indexByMessageId.get(rawId);
548
- if (index !== void 0) {
549
- return { index, snapped: null };
491
+ if (ic.maxDimension !== void 0 && (!Number.isInteger(ic.maxDimension) || ic.maxDimension < 16)) {
492
+ errors.push("imageCompression.maxDimension must be an integer >= 16");
550
493
  }
551
- const owner2 = activeOwnerAnchor(state, [rawId], indexByMessageId);
552
- if (owner2 !== null) {
553
- return {
554
- index: owner2,
555
- snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block \u2014 anchored to the active block covering it instead.`
556
- };
494
+ if (ic.quality !== void 0 && (!Number.isFinite(ic.quality) || ic.quality < 1 || ic.quality > 100)) {
495
+ errors.push("imageCompression.quality must be in [1, 100]");
557
496
  }
558
- throw new BoundaryNotFoundError(
559
- "consumed",
560
- endpoint,
561
- `${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
562
- );
563
- }
564
- const block = blockById(state, `b${boundary.numericId}`);
565
- if (!block) {
566
- throw new BoundaryNotFoundError(
567
- "unknown",
568
- endpoint,
569
- `${label}="b${boundary.numericId}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
570
- );
571
- }
572
- if (block.active) {
573
- const anchor = visibleBlockAnchor(block, indexByMessageId);
574
- if (anchor !== null) {
575
- return { index: anchor, snapped: null };
497
+ if (ic.format !== void 0 && ic.format !== "webp" && ic.format !== "jpeg" && ic.format !== "png") {
498
+ errors.push('imageCompression.format must be "webp", "jpeg", or "png"');
576
499
  }
577
500
  }
578
- const owner = activeOwnerAnchor(
579
- state,
580
- block.effectiveMessageIds,
581
- indexByMessageId
582
- );
583
- if (owner !== null) {
584
- return {
585
- index: owner,
586
- snapped: `${label}="b${boundary.numericId}" was consumed by a higher-tier block \u2014 anchored to the active block covering its content instead.`
587
- };
588
- }
589
- if (!block.active) {
590
- throw new BoundaryNotFoundError(
591
- "consumed",
592
- endpoint,
593
- `${label}="b${boundary.numericId}" not found in visible context (block distilled/consumed by a higher-tier block).`
594
- );
595
- }
596
- throw new BoundaryNotFoundError(
597
- "consumed",
598
- endpoint,
599
- `${label}="b${boundary.numericId}" is an active block but none of its content (raw messages or rendered summary) is visible in the current context \u2014 run acp_status to verify.`
600
- );
601
- }
602
- function activeOwnerAnchor(state, ownedIds, indexByMessageId) {
603
- if (ownedIds.length === 0) return null;
604
- const owned = new Set(ownedIds);
605
- let best = null;
606
- for (const block of state.blocks) {
607
- if (!block.active) continue;
608
- const inherited = inheritedContentIds(state, block);
609
- let ownsInherited = false;
610
- for (const id of owned) {
611
- if (inherited.has(id)) {
612
- ownsInherited = true;
613
- break;
614
- }
501
+ if (config.ccr) {
502
+ if (config.ccr.enabled && !config.ccr.toolName) {
503
+ errors.push("ccr.toolName must be a non-empty string when enabled");
615
504
  }
616
- if (!ownsInherited) continue;
617
- const anchor = visibleBlockAnchor(block, indexByMessageId);
618
- if (anchor === null) continue;
619
- if (best === null || anchor < best) {
620
- best = anchor;
505
+ if (!Number.isFinite(config.ccr.minToolTokens) || config.ccr.minToolTokens < 0) {
506
+ errors.push("ccr.minToolTokens must be >= 0");
507
+ }
508
+ if (!Number.isFinite(config.ccr.maxHeadChars) || config.ccr.maxHeadChars < 0) {
509
+ errors.push("ccr.maxHeadChars must be >= 0");
621
510
  }
622
511
  }
623
- return best;
512
+ return errors;
624
513
  }
625
- function inheritedContentIds(state, block) {
626
- const ids = /* @__PURE__ */ new Set();
627
- for (const childId of block.directBlockIds) {
628
- const child = blockById(state, childId);
629
- if (!child) continue;
630
- for (const id of child.effectiveMessageIds) ids.add(id);
514
+
515
+ // src/surface-config.ts
516
+ function applySectionOverrides(sections, overrides) {
517
+ const out = [];
518
+ for (const [key, text] of sections) {
519
+ const o = overrides?.[key];
520
+ if (o === null) continue;
521
+ out.push(typeof o === "string" ? o : text);
631
522
  }
632
- return ids;
633
- }
634
- function formatPaddedRef(index) {
635
- return `m${String(index).padStart(5, "0")}`;
636
- }
637
- function visibleBlockAnchor(block, indexByMessageId) {
638
- const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
639
- if (summaryIndex !== void 0) return summaryIndex;
640
- return earliestIndexOfIds(block.effectiveMessageIds, indexByMessageId);
523
+ return out;
641
524
  }
642
- function blockVisibleInRange(block, indexByMessageId, startIndex, endIndex) {
643
- const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
644
- if (summaryIndex !== void 0 && summaryIndex >= startIndex && summaryIndex <= endIndex) {
645
- return true;
646
- }
647
- const rawIndex = earliestIndexOfIds(
648
- block.effectiveMessageIds,
649
- indexByMessageId
650
- );
651
- return rawIndex !== null && rawIndex >= startIndex && rawIndex <= endIndex;
525
+ function cloneWithDescriptions(schema, overrides) {
526
+ if (Object.keys(overrides).length === 0) return schema;
527
+ return cloneNode(schema, overrides);
652
528
  }
653
- function earliestIndexOfIds(ids, indexByMessageId) {
654
- let earliest = null;
655
- for (const id of ids) {
656
- const index = indexByMessageId.get(id);
657
- if (index !== void 0 && (earliest === null || index < earliest)) {
658
- earliest = index;
529
+ function cloneNode(node, overrides) {
530
+ if (Array.isArray(node)) return node.map((item) => cloneNode(item, overrides));
531
+ if (node && typeof node === "object") {
532
+ const out = {};
533
+ for (const [name, value] of Object.entries(node)) {
534
+ const cloned = cloneNode(value, overrides);
535
+ if (Object.hasOwn(overrides, name) && cloned && typeof cloned === "object" && !Array.isArray(cloned)) {
536
+ cloned.description = overrides[name];
537
+ }
538
+ out[name] = cloned;
659
539
  }
540
+ return out;
660
541
  }
661
- return earliest;
662
- }
663
-
664
- // src/truncate-tools.ts
665
- var TRUNCATION_MARKER = "[truncated for context space]";
666
- var DEFAULTS = {
667
- minOutputTokens: 1e3,
668
- keepPrefixChars: 2e3,
669
- keepSuffixChars: 2e3,
670
- protectRecentMessages: 3
671
- };
672
- function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, options = {}) {
673
- const opts = { ...DEFAULTS, ...options };
674
- const limit = config.modelContextLimit;
675
- if (limit <= 0 || tokenCount < config.truncate.threshold * limit) {
676
- return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound: 0 };
677
- }
678
- const protectedIndex = messages.length - opts.protectRecentMessages;
679
- const findCandidates = (predicate) => {
680
- const found = [];
681
- for (let i = 0; i < protectedIndex; i++) {
682
- const message = messages[i];
683
- if (!predicate(message)) continue;
684
- const text = message.text ?? "";
685
- if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
686
- if (countTokens(text) < opts.minOutputTokens) continue;
687
- found.push(message);
688
- }
689
- return found.sort(
690
- (a, b) => countTokens(b.text ?? "") - countTokens(a.text ?? "")
691
- );
692
- };
693
- const toolResults = findCandidates((m) => m.contentType === "tool-result");
694
- const textMessages = options.includeTextMessages ? findCandidates(
695
- (m) => m.contentType === "text" && (m.role === "user" || m.role === "assistant") && !isRenderedSummaryMessage(m) && !m.text?.startsWith(SUMMARY_HEADER)
696
- ) : [];
697
- const candidatesFound = toolResults.length + textMessages.length;
698
- let truncatedCount = 0;
699
- let savedTokens = 0;
700
- let remaining = tokenCount;
701
- const targetTokens = config.truncate.threshold * limit * 0.9;
702
- const replacements = /* @__PURE__ */ new Map();
703
- const applyTo = (candidates) => {
704
- for (const candidate of candidates) {
705
- if (remaining <= targetTokens) break;
706
- const original = candidate.text ?? "";
707
- const tokens = countTokens(original);
708
- if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) {
709
- continue;
710
- }
711
- const prefix = clampPrefix(original, opts.keepPrefixChars);
712
- const suffix = clampWindow(original, original.length - opts.keepSuffixChars, original.length);
713
- const replacement = `${prefix}
714
-
715
- ...${TRUNCATION_MARKER} \u2014 original ~${tokens} tokens]...
716
-
717
- ${suffix}`;
718
- replacements.set(candidate.id, replacement);
719
- truncatedCount++;
720
- remaining -= tokens - countTokens(replacement);
721
- savedTokens += tokens - countTokens(replacement);
722
- }
723
- };
724
- applyTo(toolResults);
725
- applyTo(textMessages);
726
- if (replacements.size === 0) {
727
- return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound };
728
- }
729
- return {
730
- messages: messages.map(
731
- (m) => replacements.has(m.id) ? { ...m, text: replacements.get(m.id) } : m
732
- ),
733
- truncatedCount,
734
- savedTokens,
735
- candidatesFound
736
- };
737
- }
738
-
739
- // src/hide-consumed.ts
740
- var KEEP_LAST_ORPHANED = 2;
741
- function rangeKey(startRef, endRef) {
742
- return `${startRef}::${endRef}`;
743
- }
744
- function parseCallText(text) {
745
- const raw = text ?? "";
746
- const start = raw.indexOf("{");
747
- if (start < 0) return null;
748
- let parsed;
749
- try {
750
- parsed = JSON.parse(raw.slice(start));
751
- } catch {
752
- return null;
753
- }
754
- if (!parsed || typeof parsed !== "object") return null;
755
- const obj = parsed;
756
- let content = null;
757
- let contentWasString = false;
758
- if (Array.isArray(obj.content)) {
759
- content = obj.content;
760
- } else if (typeof obj.content === "string") {
761
- contentWasString = true;
762
- try {
763
- const inner = JSON.parse(obj.content);
764
- if (Array.isArray(inner)) content = inner;
765
- } catch {
766
- content = null;
767
- }
768
- }
769
- if (!content || content.length === 0) return null;
770
- return { prefix: raw.slice(0, start), obj, content, contentWasString };
771
- }
772
- function rewriteCompressText(text, liveKeys) {
773
- const parsed = parseCallText(text);
774
- if (!parsed) return null;
775
- const { prefix, obj, content, contentWasString } = parsed;
776
- const kept = content.filter((entry) => {
777
- if (!entry || typeof entry !== "object") return false;
778
- const e = entry;
779
- const s = typeof e.startId === "string" ? e.startId : typeof e.messageId === "string" ? e.messageId : "";
780
- const end = typeof e.endId === "string" ? e.endId : typeof e.messageId === "string" ? e.messageId : "";
781
- return liveKeys.has(rangeKey(s, end));
782
- });
783
- if (kept.length === 0) return null;
784
- return prefix + serializeCompacted(obj, kept, contentWasString).text;
785
- }
786
- var SUMMARY_STUB_CHARS = 200;
787
- function compactEntry(entry) {
788
- if (!entry || typeof entry !== "object") return entry;
789
- const e = entry;
790
- if (typeof e.summary !== "string" || e.summary.length <= SUMMARY_STUB_CHARS) return entry;
791
- return { ...e, summary: `${clampPrefix(e.summary, SUMMARY_STUB_CHARS - 1)}\u2026` };
792
- }
793
- function serializeCompacted(obj, content, contentWasString) {
794
- let changed = false;
795
- const compacted = content.map((entry) => {
796
- const out = compactEntry(entry);
797
- if (out !== entry) changed = true;
798
- return out;
799
- });
800
- const outContent = contentWasString ? JSON.stringify(compacted) : compacted;
801
- return { text: JSON.stringify({ ...obj, content: outContent }), changed };
802
- }
803
- function compactCompressText(text) {
804
- const parsed = parseCallText(text);
805
- if (!parsed) return null;
806
- const { prefix, obj, content, contentWasString } = parsed;
807
- const { text: out, changed } = serializeCompacted(obj, content, contentWasString);
808
- return changed ? prefix + out : null;
809
- }
810
- function hideConsumedCompressCalls(state, messages) {
811
- const allBlockCallIds = /* @__PURE__ */ new Set();
812
- const activeCallIds = /* @__PURE__ */ new Set();
813
- const liveRangeKeysByCallId = /* @__PURE__ */ new Map();
814
- const legacyLiveByCallId = /* @__PURE__ */ new Set();
815
- for (const block of state.blocks) {
816
- if (!block.compressCallId) continue;
817
- allBlockCallIds.add(block.compressCallId);
818
- if (!block.active) continue;
819
- activeCallIds.add(block.compressCallId);
820
- if (block.startRef === void 0 || block.endRef === void 0) {
821
- legacyLiveByCallId.add(block.compressCallId);
822
- continue;
823
- }
824
- let keys = liveRangeKeysByCallId.get(block.compressCallId);
825
- if (!keys) {
826
- keys = /* @__PURE__ */ new Set();
827
- liveRangeKeysByCallId.set(block.compressCallId, keys);
828
- }
829
- keys.add(rangeKey(block.startRef, block.endRef));
830
- }
831
- const lastOrphanedCallIds = [];
832
- for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
833
- const message = messages[i];
834
- if (message.toolName !== "compress" || message.contentType !== "tool-call") continue;
835
- const callId = message.toolCallId;
836
- if (callId && !allBlockCallIds.has(callId)) {
837
- lastOrphanedCallIds.push(callId);
838
- }
839
- }
840
- const keepCallIds = /* @__PURE__ */ new Set([...activeCallIds, ...lastOrphanedCallIds]);
841
- const hiddenCallIds = /* @__PURE__ */ new Set();
842
- for (const message of messages) {
843
- if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
844
- if (message.toolCallId) hiddenCallIds.add(message.toolCallId);
845
- }
846
- }
847
- let hidden = 0;
848
- const result = [];
849
- for (const message of messages) {
850
- if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
851
- hidden++;
852
- continue;
853
- }
854
- if (message.contentType === "tool-result" && message.toolCallId && hiddenCallIds.has(message.toolCallId)) {
855
- hidden++;
856
- continue;
857
- }
858
- if (message.toolName === "compress" && message.contentType === "tool-call" && message.toolCallId && keepCallIds.has(message.toolCallId)) {
859
- const liveKeys = liveRangeKeysByCallId.get(message.toolCallId);
860
- if (liveKeys && liveKeys.size > 0 && !legacyLiveByCallId.has(message.toolCallId)) {
861
- const rewritten = rewriteCompressText(message.text, liveKeys);
862
- if (rewritten !== null) {
863
- result.push({ ...message, text: rewritten });
864
- continue;
865
- }
866
- }
867
- const compacted = compactCompressText(message.text);
868
- if (compacted !== null) {
869
- result.push({ ...message, text: compacted });
870
- continue;
871
- }
872
- }
873
- result.push(message);
874
- }
875
- return { messages: result, hidden };
876
- }
877
-
878
- // src/surface-config.ts
879
- function applySectionOverrides(sections, overrides) {
880
- const out = [];
881
- for (const [key, text] of sections) {
882
- const o = overrides?.[key];
883
- if (o === null) continue;
884
- out.push(typeof o === "string" ? o : text);
885
- }
886
- return out;
887
- }
888
- function cloneWithDescriptions(schema, overrides) {
889
- if (Object.keys(overrides).length === 0) return schema;
890
- return cloneNode(schema, overrides);
891
- }
892
- function cloneNode(node, overrides) {
893
- if (Array.isArray(node)) return node.map((item) => cloneNode(item, overrides));
894
- if (node && typeof node === "object") {
895
- const out = {};
896
- for (const [name, value] of Object.entries(node)) {
897
- const cloned = cloneNode(value, overrides);
898
- if (Object.hasOwn(overrides, name) && cloned && typeof cloned === "object" && !Array.isArray(cloned)) {
899
- cloned.description = overrides[name];
900
- }
901
- out[name] = cloned;
902
- }
903
- return out;
904
- }
905
- return node;
542
+ return node;
906
543
  }
907
544
  function applyAcpToolOverrides(tools, overrides) {
908
545
  if (!overrides) return [...tools];
@@ -942,6 +579,7 @@ var SEARCH_CONTEXT_TOOL_NAME = "search_context";
942
579
  var ACP_STATUS_TOOL_NAME = "acp_status";
943
580
  var ACP_CACHE_TOOL_NAME = "acp_cache";
944
581
  var ABSORB_TOOL_NAME = "absorb";
582
+ var IMAGE_FULL_TOOL_NAME = "image_full";
945
583
  var ACP_TEXT_OPEN = "<acp_compress>";
946
584
  var ACP_TEXT_CLOSE = "</acp_compress>";
947
585
  var ACP_STATUS_OPEN = "<acp_status>";
@@ -1475,6 +1113,67 @@ var ABSORB_TOOL_GOOGLE = {
1475
1113
  description: ABSORB_TOOL_DESCRIPTION,
1476
1114
  parameters: ABSORB_PARAMETERS
1477
1115
  };
1116
+ var IMAGE_FULL_TOOL_DESCRIPTION = 'Restore original-resolution images for a previously downscaled message. Call when you cannot read details (text, colors, alignment) in a reduced image: pass the message ref ("mNNNNN") from the [Downscaled screenshots] note. Full resolution applies for the rest of this session.';
1117
+ var IMAGE_FULL_PARAMETERS = {
1118
+ type: "object",
1119
+ properties: {
1120
+ ref: {
1121
+ type: "string",
1122
+ description: "mNNNNN ref of the message whose image(s) should be restored to full resolution"
1123
+ }
1124
+ },
1125
+ required: ["ref"]
1126
+ };
1127
+ var RETRIEVE_TOOL_NAME = "acp_retrieve";
1128
+ var RETRIEVE_TOOL_DESCRIPTION = "Retrieve the full original text of a stored tool result. REQUIRED when you need details from a placeholder that starts with \u{1F4E6} [acp-stored #mNNNNN: pass its ref. Returns the exact original text; the placeholder stays in context. Unknown refs return not-found.";
1129
+ var RETRIEVE_PARAMETERS = {
1130
+ type: "object",
1131
+ properties: {
1132
+ ref: {
1133
+ type: "string",
1134
+ description: "mNNNNN ref shown in the \u{1F4E6} [acp-stored placeholder"
1135
+ }
1136
+ },
1137
+ required: ["ref"]
1138
+ };
1139
+ var IMAGE_FULL_TOOL = {
1140
+ name: IMAGE_FULL_TOOL_NAME,
1141
+ description: IMAGE_FULL_TOOL_DESCRIPTION,
1142
+ input_schema: IMAGE_FULL_PARAMETERS
1143
+ };
1144
+ var IMAGE_FULL_TOOL_OPENAI = {
1145
+ type: "function",
1146
+ function: {
1147
+ name: IMAGE_FULL_TOOL_NAME,
1148
+ description: IMAGE_FULL_TOOL_DESCRIPTION,
1149
+ parameters: IMAGE_FULL_PARAMETERS
1150
+ }
1151
+ };
1152
+ var IMAGE_FULL_TOOL_RESPONSES = {
1153
+ type: "function",
1154
+ name: IMAGE_FULL_TOOL_OPENAI.function.name,
1155
+ description: IMAGE_FULL_TOOL_OPENAI.function.description,
1156
+ parameters: IMAGE_FULL_TOOL_OPENAI.function.parameters
1157
+ };
1158
+ var RETRIEVE_TOOL = {
1159
+ name: RETRIEVE_TOOL_NAME,
1160
+ description: RETRIEVE_TOOL_DESCRIPTION,
1161
+ input_schema: RETRIEVE_PARAMETERS
1162
+ };
1163
+ var RETRIEVE_TOOL_OPENAI = {
1164
+ type: "function",
1165
+ function: {
1166
+ name: RETRIEVE_TOOL_NAME,
1167
+ description: RETRIEVE_TOOL_DESCRIPTION,
1168
+ parameters: RETRIEVE_PARAMETERS
1169
+ }
1170
+ };
1171
+ var RETRIEVE_TOOL_RESPONSES = {
1172
+ type: "function",
1173
+ name: RETRIEVE_TOOL_NAME,
1174
+ description: RETRIEVE_TOOL_DESCRIPTION,
1175
+ parameters: RETRIEVE_PARAMETERS
1176
+ };
1478
1177
 
1479
1178
  // src/protected.ts
1480
1179
  var ALWAYS_PROTECTED_TOOLS = ["compress", "acp_rule"];
@@ -1552,223 +1251,2268 @@ function isMessageLatestProtected(msg, latest) {
1552
1251
  return false;
1553
1252
  }
1554
1253
 
1555
- // src/absorb.ts
1556
- var ABSORB_PROMPT_MARKER = "[ACP absorb]";
1557
- var DEFAULT_ABSORB_CONFIG = {
1254
+ // src/content-store.ts
1255
+ import { createHash } from "crypto";
1256
+ function createContentStore() {
1257
+ return { version: 1, byHash: {}, byRef: {} };
1258
+ }
1259
+ function hashContent(text) {
1260
+ return createHash("sha256").update(text, "utf8").digest("hex");
1261
+ }
1262
+ function storeOriginal(store, spec) {
1263
+ if (!spec.ref || store.byRef[spec.ref]) return store;
1264
+ const hash = hashContent(spec.text);
1265
+ const entry = {
1266
+ hash,
1267
+ rawId: spec.rawId,
1268
+ kind: spec.kind,
1269
+ tokens: spec.tokens,
1270
+ chars: spec.text.length,
1271
+ head: spec.head
1272
+ };
1273
+ if (spec.toolName !== void 0) entry.toolName = spec.toolName;
1274
+ const byHash = store.byHash[hash] === void 0 ? { ...store.byHash, [hash]: spec.text } : store.byHash;
1275
+ return { ...store, byHash, byRef: { ...store.byRef, [spec.ref]: entry } };
1276
+ }
1277
+ function retrieveByRef(store, ref) {
1278
+ const entry = store.byRef[ref];
1279
+ if (!entry) return { ok: false, reason: "not-found" };
1280
+ const text = store.byHash[entry.hash];
1281
+ if (text === void 0) return { ok: false, reason: "not-found" };
1282
+ return { ok: true, text, entry };
1283
+ }
1284
+ function hasStoredRef(store, ref) {
1285
+ return store.byRef[ref] !== void 0;
1286
+ }
1287
+ function contentStoreStats(store) {
1288
+ let totalChars = 0;
1289
+ for (const text of Object.values(store.byHash)) totalChars += text.length;
1290
+ return {
1291
+ entries: Object.keys(store.byRef).length,
1292
+ uniqueContents: Object.keys(store.byHash).length,
1293
+ totalChars
1294
+ };
1295
+ }
1296
+
1297
+ // src/ccr.ts
1298
+ var RETRIEVE_TOOL_NAME2 = "acp_retrieve";
1299
+ var DEFAULT_CCR_CONFIG = {
1558
1300
  enabled: false,
1559
- toolName: ABSORB_TOOL_NAME,
1560
- minToolTokens: 1e3,
1561
- contextThresholdPct: 0,
1562
- excludeTools: []
1301
+ toolName: RETRIEVE_TOOL_NAME2,
1302
+ minToolTokens: 4e3,
1303
+ excludeTools: [],
1304
+ maxHeadChars: 96
1563
1305
  };
1564
- function resolveAbsorbConfig(config) {
1565
- return { ...DEFAULT_ABSORB_CONFIG, ...config.absorb ?? {} };
1306
+ function resolveCcrConfig(config) {
1307
+ return { ...DEFAULT_CCR_CONFIG, ...config.ccr };
1308
+ }
1309
+ var STORED_PLACEHOLDER_MARKER = "[acp-stored";
1310
+ var RETRIEVED_ID_PREFIX = "acp_retrieved_";
1311
+ var KIND_LABELS = {
1312
+ bash: "shell output",
1313
+ shell: "shell output",
1314
+ exec: "shell output",
1315
+ execute_command: "shell output",
1316
+ run: "shell output",
1317
+ terminal: "shell output",
1318
+ read: "file read",
1319
+ read_file: "file read",
1320
+ cat: "file read",
1321
+ open: "file read",
1322
+ grep: "search output",
1323
+ rg: "search output",
1324
+ search: "search output",
1325
+ glob: "search output",
1326
+ find: "search output",
1327
+ webfetch: "web fetch",
1328
+ web_fetch: "web fetch",
1329
+ fetch: "web fetch",
1330
+ curl: "web fetch"
1331
+ };
1332
+ function classifyKind(toolName) {
1333
+ if (!toolName) return "tool result";
1334
+ return KIND_LABELS[toolName.toLowerCase()] ?? "tool result";
1335
+ }
1336
+ function groupThousands(value) {
1337
+ return String(Math.max(0, Math.round(value))).replace(
1338
+ /\B(?=(\d{3})+(?!\d))/g,
1339
+ ","
1340
+ );
1566
1341
  }
1567
- function formatTokenCount(tokens) {
1568
- if (tokens < 1e3) return String(tokens);
1569
- if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
1570
- return Math.round(tokens / 1e3) + "K";
1342
+ function normalizeHead(text, maxChars) {
1343
+ const singleLine = (text || "").replace(/\s+/g, " ").trim();
1344
+ if (singleLine.length <= maxChars) return singleLine;
1345
+ return singleLine.slice(0, maxChars) + "\u2026";
1571
1346
  }
1572
- function buildAbsorbPrompt(ref, tokens, toolName = ABSORB_TOOL_NAME) {
1573
- return `${ABSORB_PROMPT_MARKER} This tool result (~${formatTokenCount(tokens)} tokens) will be REMOVED from context. Your IMMEDIATE next action: call ${toolName}({ ref: "${ref}", summary: "..." }) \u2014 summary = distilled essentials only (outcome, key values, exact paths:lines, error text verbatim, decisions). Afterwards work from your summary; do NOT re-run this tool. If the result contains nothing you need, call ${toolName} with summary "(nothing needed)".`;
1347
+ var COMMAND_FIELDS = ["command", "cmd", "script", "query", "path", "url"];
1348
+ function extractCommand(toolCallText, maxChars) {
1349
+ if (!toolCallText) return void 0;
1350
+ let parsed;
1351
+ try {
1352
+ parsed = JSON.parse(toolCallText);
1353
+ } catch {
1354
+ return void 0;
1355
+ }
1356
+ if (typeof parsed !== "object" || parsed === null) return void 0;
1357
+ const record = parsed;
1358
+ for (const field of COMMAND_FIELDS) {
1359
+ const value = record[field];
1360
+ if (typeof value === "string" && value.trim().length > 0) {
1361
+ const normalized = value.replace(/\s+/g, " ").trim();
1362
+ return normalized.length <= maxChars ? normalized : normalized.slice(0, maxChars) + "\u2026";
1363
+ }
1364
+ }
1365
+ return void 0;
1574
1366
  }
1575
- function buildAbsorbSystemPrompt(toolName = ABSORB_TOOL_NAME) {
1576
- return `INSTANT TOOL-RESULT ABSORPTION (${toolName})
1577
-
1578
- Some tool results end with a ${ABSORB_PROMPT_MARKER} instruction. When you see one, your IMMEDIATE next action must be calling ${toolName}({ ref, summary }) \u2014 distill that tool result's essentials into summary: outcome, key values, exact paths:lines, error text verbatim, decisions. The original output is then removed from context; your ${toolName} summary becomes the only durable record of it, so distill carefully. Never call another tool or answer the user before absorbing a marked result. Do not re-run the original tool afterwards \u2014 work from your summary. ${toolName} calls are ordinary context: the regular compression system may fold them later like any other message.`;
1367
+ function buildStoredPlaceholder(input) {
1368
+ const title = input.command ?? input.head;
1369
+ const titlePart = title ? ` \`${title}\`` : "";
1370
+ return `\u{1F4E6} ${STORED_PLACEHOLDER_MARKER} #${input.ref} \xB7 ${input.kind} \xB7 ${groupThousands(input.tokens)} tok]${titlePart}
1371
+ \u2192 ${input.retrieveToolName}("${input.ref}") returns the full text`;
1579
1372
  }
1580
- function isAcpOrConfiguredTool(toolName, cfg) {
1581
- if (!toolName) return false;
1582
- if (toolName === cfg.toolName) return true;
1583
- return ACP_TOOL_NAMES.has(toolName);
1373
+ var LEADING_TAG_RE = new RegExp("^\\x3cacp [^>]*>[^\\x3c]*\\x3c\\/acp>\\n?");
1374
+ function stripLeadingTag(text) {
1375
+ return text.replace(LEADING_TAG_RE, "");
1584
1376
  }
1585
- function isAbsorbCandidate(msg, config, latest) {
1586
- if (msg.contentType !== "tool-result" || !msg.toolCallId) return false;
1587
- const cfg = resolveAbsorbConfig(config);
1588
- if (isAcpOrConfiguredTool(msg.toolName, cfg)) return false;
1589
- if (isMessageProtected(msg, config)) return false;
1590
- if (latest && isMessageLatestProtected(msg, latest)) return false;
1591
- for (const pattern of cfg.excludeTools) {
1592
- if (msg.toolName && matchToolPattern(msg.toolName, pattern)) return false;
1593
- }
1594
- return true;
1377
+ function isStoredPlaceholderText(text) {
1378
+ return stripLeadingTag(text).includes(STORED_PLACEHOLDER_MARKER);
1595
1379
  }
1596
- function hideAbsorbedMessages(messages, state) {
1597
- const records = state.absorbed ?? [];
1598
- if (records.length === 0) return messages;
1599
- const hidden = /* @__PURE__ */ new Set();
1600
- for (const record of records) {
1601
- if (record.callMessageId) hidden.add(record.callMessageId);
1602
- if (record.resultMessageId) hidden.add(record.resultMessageId);
1603
- }
1604
- return messages.filter((msg) => !hidden.has(msg.id));
1380
+ function retrievedMessageId(ref) {
1381
+ return RETRIEVED_ID_PREFIX + ref;
1605
1382
  }
1606
- function appendAbsorbPrompts(messages, state, config, tokenCount, countTokens) {
1607
- const cfg = resolveAbsorbConfig(config);
1608
- if (!cfg.enabled) return { messages, promptedCount: 0 };
1609
- const limit = config.modelContextLimit;
1610
- if (cfg.contextThresholdPct > 0 && limit > 0 && tokenCount < cfg.contextThresholdPct * limit) {
1611
- return { messages, promptedCount: 0 };
1383
+ function isRetrievedMessage(message) {
1384
+ return message.id.startsWith(RETRIEVED_ID_PREFIX) && message.role === "system" && message.contentType === "text";
1385
+ }
1386
+ function buildRetrievalInjection(ref, entry, text) {
1387
+ const header = `[acp-retrieved #${ref} \xB7 ${entry.kind} \xB7 ${groupThousands(entry.tokens)} tok]`;
1388
+ return {
1389
+ ackText: `retrieved ${ref}: ${groupThousands(entry.tokens)} tok (${entry.chars} chars)`,
1390
+ injection: {
1391
+ id: retrievedMessageId(ref),
1392
+ role: "system",
1393
+ contentType: "text",
1394
+ text: `${header}
1395
+ ${text}`
1396
+ }
1397
+ };
1398
+ }
1399
+ function applyRetrieve(input) {
1400
+ const ref = input.ref.trim();
1401
+ const found = retrieveByRef(input.store, ref);
1402
+ if (!found.ok) {
1403
+ return {
1404
+ ok: false,
1405
+ reason: "not-found",
1406
+ ackText: `retrieve ${ref}: not found \u2014 no stored original for this ref`
1407
+ };
1612
1408
  }
1613
- const absorbedIds = /* @__PURE__ */ new Set();
1614
- for (const record of state.absorbed ?? []) {
1615
- if (record.resultMessageId) absorbedIds.add(record.resultMessageId);
1409
+ const built = buildRetrievalInjection(ref, found.entry, found.text);
1410
+ return {
1411
+ ok: true,
1412
+ text: found.text,
1413
+ ackText: built.ackText,
1414
+ injection: built.injection,
1415
+ entry: found.entry
1416
+ };
1417
+ }
1418
+ function storeLargeResults(input) {
1419
+ const cfg = resolveCcrConfig(input.config);
1420
+ if (!cfg.enabled)
1421
+ return { messages: input.messages, store: input.store, storedCount: 0 };
1422
+ let current = input.store;
1423
+ let storedCount = 0;
1424
+ const callById = /* @__PURE__ */ new Map();
1425
+ for (const message of input.messages) {
1426
+ if (message.contentType === "tool-call" && message.toolCallId) {
1427
+ callById.set(message.toolCallId, message);
1428
+ }
1616
1429
  }
1617
- let promptedCount = 0;
1618
- const latest = collectLatestProtected(messages, config);
1619
- const out = messages.map((msg) => {
1620
- if (!isAbsorbCandidate(msg, config, latest)) return msg;
1621
- if (absorbedIds.has(msg.id)) return msg;
1622
- const text = msg.text ?? "";
1623
- if (text.includes(ABSORB_PROMPT_MARKER)) return msg;
1624
- const tokens = countTokens(text);
1625
- if (tokens < cfg.minToolTokens) return msg;
1626
- const ref = refForRaw(state.messageRefs, msg.id);
1627
- if (!ref || ref === BLOCKED_REF) return msg;
1628
- promptedCount++;
1430
+ const updated = input.messages.map((message) => {
1431
+ if (message.contentType !== "tool-result") return message;
1432
+ const text = message.text ?? "";
1433
+ if (text.length === 0) return message;
1434
+ if (isStoredPlaceholderText(text)) return message;
1435
+ if (!message.toolCallId) return message;
1436
+ const toolName = message.toolName;
1437
+ if (toolName && (ACP_TOOL_NAMES.has(toolName) || toolName === cfg.toolName)) {
1438
+ return message;
1439
+ }
1440
+ if (toolName && cfg.excludeTools.some((pattern) => matchToolPattern(toolName, pattern))) {
1441
+ return message;
1442
+ }
1443
+ if (isMessageProtected(message, input.config)) return message;
1444
+ const tokens = input.countTokens(text);
1445
+ if (tokens < cfg.minToolTokens) return message;
1446
+ const ref = refForRaw(input.state.messageRefs, message.id);
1447
+ if (!ref || ref === BLOCKED_REF) return message;
1448
+ if (hasStoredRef(current, ref)) return message;
1449
+ const kind = classifyKind(toolName);
1450
+ const head = normalizeHead(text, cfg.maxHeadChars);
1451
+ const command = extractCommand(
1452
+ callById.get(message.toolCallId)?.text,
1453
+ cfg.maxHeadChars
1454
+ );
1455
+ current = storeOriginal(current, {
1456
+ ref,
1457
+ rawId: message.id,
1458
+ text,
1459
+ kind,
1460
+ toolName,
1461
+ tokens,
1462
+ head
1463
+ });
1464
+ storedCount += 1;
1629
1465
  return {
1630
- ...msg,
1631
- text: text + "\n\n" + buildAbsorbPrompt(ref, tokens, cfg.toolName)
1466
+ ...message,
1467
+ text: buildStoredPlaceholder({
1468
+ ref,
1469
+ kind,
1470
+ tokens,
1471
+ head,
1472
+ command,
1473
+ retrieveToolName: cfg.toolName
1474
+ })
1632
1475
  };
1633
1476
  });
1634
- return { messages: out, promptedCount };
1477
+ return { messages: updated, store: current, storedCount };
1635
1478
  }
1636
- function parseAbsorbInput(input, callId, onWarn) {
1637
- let obj = null;
1638
- if (typeof input === "string") {
1639
- try {
1640
- const parsed = JSON.parse(input);
1641
- if (parsed && typeof parsed === "object") {
1642
- obj = parsed;
1643
- }
1644
- } catch {
1645
- obj = null;
1479
+ var ccrStoreNode = {
1480
+ name: "ccr-store",
1481
+ enabled: (_io, ctx) => resolveCcrConfig(ctx.config).enabled,
1482
+ run(io, ctx) {
1483
+ const applied = storeLargeResults({
1484
+ messages: io.messages,
1485
+ state: io.state,
1486
+ store: ctx.contentStore,
1487
+ config: ctx.config,
1488
+ countTokens: ctx.countTokens
1489
+ });
1490
+ const effect = {
1491
+ store: applied.store,
1492
+ storedCount: applied.storedCount
1493
+ };
1494
+ const stats = applied.storedCount > 0 ? {
1495
+ ...io.state.stats,
1496
+ storedCount: (io.state.stats.storedCount ?? 0) + applied.storedCount
1497
+ } : io.state.stats;
1498
+ return {
1499
+ ...io,
1500
+ messages: applied.messages,
1501
+ state: stats === io.state.stats ? io.state : { ...io.state, stats },
1502
+ effects: { ...io.effects, ccr: effect }
1503
+ };
1504
+ }
1505
+ };
1506
+ function storeCoveredOriginals(store, messages, state, blockIds, countTokens, maxHeadChars = DEFAULT_CCR_CONFIG.maxHeadChars) {
1507
+ const covered = /* @__PURE__ */ new Set();
1508
+ for (const block of state.blocks) {
1509
+ if (!block.active || !blockIds.includes(block.blockId)) continue;
1510
+ for (const id of block.effectiveMessageIds) covered.add(id);
1511
+ }
1512
+ let current = store;
1513
+ for (const message of messages) {
1514
+ if (!covered.has(message.id)) continue;
1515
+ if (message.contentType === "reasoning") continue;
1516
+ const text = stripLeadingTag(message.text ?? "");
1517
+ if (text.length === 0) continue;
1518
+ const ref = refForRaw(state.messageRefs, message.id);
1519
+ if (!ref || ref === BLOCKED_REF) continue;
1520
+ if (isStoredPlaceholderText(text) && hasStoredRef(current, ref)) continue;
1521
+ current = storeOriginal(current, {
1522
+ ref,
1523
+ rawId: message.id,
1524
+ text,
1525
+ kind: classifyKind(message.toolName),
1526
+ toolName: message.toolName,
1527
+ tokens: countTokens(text),
1528
+ head: normalizeHead(text, maxHeadChars)
1529
+ });
1530
+ }
1531
+ return current;
1532
+ }
1533
+ function noteRetrieval(state) {
1534
+ return {
1535
+ ...state,
1536
+ stats: {
1537
+ ...state.stats,
1538
+ retrievalCount: (state.stats.retrievalCount ?? 0) + 1
1539
+ }
1540
+ };
1541
+ }
1542
+
1543
+ // src/boundaries.ts
1544
+ var MESSAGE_REF_PATTERN = /^m0*(\d{1,5})$/;
1545
+ var BLOCK_REF_PATTERN = /^b(\d{1,9})$/;
1546
+ function parseBoundary(ref) {
1547
+ const normalized = ref.trim().toLowerCase();
1548
+ const messageMatch = MESSAGE_REF_PATTERN.exec(normalized);
1549
+ if (messageMatch) {
1550
+ const numericId = Number(messageMatch[1]);
1551
+ if (numericId >= 1 && numericId <= 99999) {
1552
+ return { kind: "message", numericId, raw: normalized };
1646
1553
  }
1647
- } else if (input && typeof input === "object") {
1648
- obj = input;
1649
1554
  }
1650
- if (!obj) {
1651
- onWarn?.(`[acp-absorb-input] rejected: not an object (${typeof input})`);
1652
- return null;
1555
+ const blockMatch = BLOCK_REF_PATTERN.exec(normalized);
1556
+ if (blockMatch) {
1557
+ const numericId = Number(blockMatch[1]);
1558
+ if (numericId >= 1) return { kind: "block", numericId, raw: normalized };
1653
1559
  }
1654
- const ref = pickString(obj, "ref", "messageId", "of");
1655
- const summary = pickString(obj, "summary", "content");
1656
- if (typeof ref !== "string" || typeof summary !== "string") {
1657
- onWarn?.(
1658
- `[acp-absorb-input] rejected: need ref (string) + summary (string); keys: ${Object.keys(obj).join(",")}`
1560
+ return null;
1561
+ }
1562
+ var BoundaryNotFoundError = class extends Error {
1563
+ code = "BOUNDARY_NOT_FOUND";
1564
+ kind;
1565
+ endpoint;
1566
+ constructor(kind, endpoint, message) {
1567
+ super(message);
1568
+ this.name = "BoundaryNotFoundError";
1569
+ this.code = "BOUNDARY_NOT_FOUND";
1570
+ this.kind = kind;
1571
+ this.endpoint = endpoint;
1572
+ }
1573
+ };
1574
+ function resolveBoundaries(input) {
1575
+ const start = parseBoundary(input.startRef);
1576
+ const end = parseBoundary(input.endRef);
1577
+ if (!start || !end) {
1578
+ throw new Error(
1579
+ `Invalid boundary ref(s): startId="${input.startRef}", endId="${input.endRef}". Use mNNNNN or bN.`
1659
1580
  );
1660
- return null;
1661
1581
  }
1582
+ const indexByMessageId = /* @__PURE__ */ new Map();
1583
+ input.messages.forEach(
1584
+ (message, index) => indexByMessageId.set(message.id, index)
1585
+ );
1586
+ let snappedBoundaries = [];
1587
+ const startAnchor = resolveAnchorIndex(
1588
+ start,
1589
+ input.state,
1590
+ indexByMessageId,
1591
+ "start"
1592
+ );
1593
+ if (startAnchor.snapped) snappedBoundaries.push(startAnchor.snapped);
1594
+ const endAnchor = resolveAnchorIndex(
1595
+ end,
1596
+ input.state,
1597
+ indexByMessageId,
1598
+ "end"
1599
+ );
1600
+ if (endAnchor.snapped) snappedBoundaries.push(endAnchor.snapped);
1601
+ let startIndex = startAnchor.index;
1602
+ let endIndex = endAnchor.index;
1603
+ if (startIndex > endIndex) {
1604
+ [startIndex, endIndex] = [endIndex, startIndex];
1605
+ }
1606
+ const messageIds = [];
1607
+ for (let index = startIndex; index <= endIndex; index++) {
1608
+ const message = input.messages[index];
1609
+ if (message && !isRenderedSummaryMessage(message) && !isRetrievedMessage(message))
1610
+ messageIds.push(message.id);
1611
+ }
1612
+ const boundaryKind = start.kind === "block" || end.kind === "block" ? "block" : "message";
1613
+ const nestedBlockIds = [];
1614
+ const nestedSeen = /* @__PURE__ */ new Set();
1615
+ for (const block of activeBlocks(input.state)) {
1616
+ if (blockVisibleInRange(block, indexByMessageId, startIndex, endIndex)) {
1617
+ if (!nestedSeen.has(block.blockId)) {
1618
+ nestedSeen.add(block.blockId);
1619
+ nestedBlockIds.push(block.blockId);
1620
+ }
1621
+ }
1622
+ }
1623
+ const protectedGaps = [];
1662
1624
  return {
1663
- ref: ref.trim(),
1664
- summary,
1665
- ...callId ? { absorbCallId: callId } : {}
1625
+ startIndex,
1626
+ endIndex,
1627
+ messageIds,
1628
+ nestedBlockIds,
1629
+ boundaryKind,
1630
+ protectedGaps,
1631
+ snappedBoundaries
1666
1632
  };
1667
1633
  }
1668
- function pickString(obj, ...keys) {
1669
- for (const key of keys) {
1670
- const value = obj[key];
1671
- if (typeof value === "string") return value;
1634
+ function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
1635
+ const label = endpoint === "start" ? "startId" : "endId";
1636
+ if (boundary.kind === "message") {
1637
+ const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
1638
+ if (!rawId) {
1639
+ throw new BoundaryNotFoundError(
1640
+ "unknown",
1641
+ endpoint,
1642
+ `${label}="${boundary.raw}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
1643
+ );
1644
+ }
1645
+ const index = indexByMessageId.get(rawId);
1646
+ if (index !== void 0) {
1647
+ return { index, snapped: null };
1648
+ }
1649
+ const owner2 = activeOwnerAnchor(state, [rawId], indexByMessageId);
1650
+ if (owner2 !== null) {
1651
+ return {
1652
+ index: owner2,
1653
+ snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block \u2014 anchored to the active block covering it instead.`
1654
+ };
1655
+ }
1656
+ throw new BoundaryNotFoundError(
1657
+ "consumed",
1658
+ endpoint,
1659
+ `${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
1660
+ );
1661
+ }
1662
+ const block = blockById(state, `b${boundary.numericId}`);
1663
+ if (!block) {
1664
+ throw new BoundaryNotFoundError(
1665
+ "unknown",
1666
+ endpoint,
1667
+ `${label}="b${boundary.numericId}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
1668
+ );
1669
+ }
1670
+ if (block.active) {
1671
+ const anchor = visibleBlockAnchor(block, indexByMessageId);
1672
+ if (anchor !== null) {
1673
+ return { index: anchor, snapped: null };
1674
+ }
1675
+ }
1676
+ const owner = activeOwnerAnchor(
1677
+ state,
1678
+ block.effectiveMessageIds,
1679
+ indexByMessageId
1680
+ );
1681
+ if (owner !== null) {
1682
+ return {
1683
+ index: owner,
1684
+ snapped: `${label}="b${boundary.numericId}" was consumed by a higher-tier block \u2014 anchored to the active block covering its content instead.`
1685
+ };
1686
+ }
1687
+ if (!block.active) {
1688
+ throw new BoundaryNotFoundError(
1689
+ "consumed",
1690
+ endpoint,
1691
+ `${label}="b${boundary.numericId}" not found in visible context (block distilled/consumed by a higher-tier block).`
1692
+ );
1693
+ }
1694
+ throw new BoundaryNotFoundError(
1695
+ "consumed",
1696
+ endpoint,
1697
+ `${label}="b${boundary.numericId}" is an active block but none of its content (raw messages or rendered summary) is visible in the current context \u2014 run acp_status to verify.`
1698
+ );
1699
+ }
1700
+ function activeOwnerAnchor(state, ownedIds, indexByMessageId) {
1701
+ if (ownedIds.length === 0) return null;
1702
+ const owned = new Set(ownedIds);
1703
+ let best = null;
1704
+ for (const block of state.blocks) {
1705
+ if (!block.active) continue;
1706
+ const inherited = inheritedContentIds(state, block);
1707
+ let ownsInherited = false;
1708
+ for (const id of owned) {
1709
+ if (inherited.has(id)) {
1710
+ ownsInherited = true;
1711
+ break;
1712
+ }
1713
+ }
1714
+ if (!ownsInherited) continue;
1715
+ const anchor = visibleBlockAnchor(block, indexByMessageId);
1716
+ if (anchor === null) continue;
1717
+ if (best === null || anchor < best) {
1718
+ best = anchor;
1719
+ }
1720
+ }
1721
+ return best;
1722
+ }
1723
+ function inheritedContentIds(state, block) {
1724
+ const ids = /* @__PURE__ */ new Set();
1725
+ for (const childId of block.directBlockIds) {
1726
+ const child = blockById(state, childId);
1727
+ if (!child) continue;
1728
+ for (const id of child.effectiveMessageIds) ids.add(id);
1729
+ }
1730
+ return ids;
1731
+ }
1732
+ function formatPaddedRef(index) {
1733
+ return `m${String(index).padStart(5, "0")}`;
1734
+ }
1735
+ function visibleBlockAnchor(block, indexByMessageId) {
1736
+ const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
1737
+ if (summaryIndex !== void 0) return summaryIndex;
1738
+ return earliestIndexOfIds(block.effectiveMessageIds, indexByMessageId);
1739
+ }
1740
+ function blockVisibleInRange(block, indexByMessageId, startIndex, endIndex) {
1741
+ const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
1742
+ if (summaryIndex !== void 0 && summaryIndex >= startIndex && summaryIndex <= endIndex) {
1743
+ return true;
1744
+ }
1745
+ const rawIndex = earliestIndexOfIds(
1746
+ block.effectiveMessageIds,
1747
+ indexByMessageId
1748
+ );
1749
+ return rawIndex !== null && rawIndex >= startIndex && rawIndex <= endIndex;
1750
+ }
1751
+ function earliestIndexOfIds(ids, indexByMessageId) {
1752
+ let earliest = null;
1753
+ for (const id of ids) {
1754
+ const index = indexByMessageId.get(id);
1755
+ if (index !== void 0 && (earliest === null || index < earliest)) {
1756
+ earliest = index;
1757
+ }
1758
+ }
1759
+ return earliest;
1760
+ }
1761
+
1762
+ // src/truncate-tools.ts
1763
+ var TRUNCATION_MARKER = "[truncated for context space]";
1764
+ var DEFAULTS = {
1765
+ minOutputTokens: 1e3,
1766
+ keepPrefixChars: 2e3,
1767
+ keepSuffixChars: 2e3,
1768
+ protectRecentMessages: 3
1769
+ };
1770
+ function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, options = {}) {
1771
+ const opts = { ...DEFAULTS, ...options };
1772
+ const limit = config.modelContextLimit;
1773
+ if (limit <= 0 || tokenCount < config.truncate.threshold * limit) {
1774
+ return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound: 0 };
1775
+ }
1776
+ const protectedIndex = messages.length - opts.protectRecentMessages;
1777
+ const findCandidates = (predicate) => {
1778
+ const found = [];
1779
+ for (let i = 0; i < protectedIndex; i++) {
1780
+ const message = messages[i];
1781
+ if (!predicate(message)) continue;
1782
+ const text = message.text ?? "";
1783
+ if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
1784
+ if (countTokens(text) < opts.minOutputTokens) continue;
1785
+ found.push(message);
1786
+ }
1787
+ return found.sort(
1788
+ (a, b) => countTokens(b.text ?? "") - countTokens(a.text ?? "")
1789
+ );
1790
+ };
1791
+ const toolResults = findCandidates((m) => m.contentType === "tool-result");
1792
+ const textMessages = options.includeTextMessages ? findCandidates(
1793
+ (m) => m.contentType === "text" && (m.role === "user" || m.role === "assistant") && !isRenderedSummaryMessage(m) && !m.text?.startsWith(SUMMARY_HEADER)
1794
+ ) : [];
1795
+ const candidatesFound = toolResults.length + textMessages.length;
1796
+ let truncatedCount = 0;
1797
+ let savedTokens = 0;
1798
+ let remaining = tokenCount;
1799
+ const targetTokens = config.truncate.threshold * limit * 0.9;
1800
+ const replacements = /* @__PURE__ */ new Map();
1801
+ const applyTo = (candidates) => {
1802
+ for (const candidate of candidates) {
1803
+ if (remaining <= targetTokens) break;
1804
+ const original = candidate.text ?? "";
1805
+ const tokens = countTokens(original);
1806
+ if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) {
1807
+ continue;
1808
+ }
1809
+ const prefix = clampPrefix(original, opts.keepPrefixChars);
1810
+ const suffix = clampWindow(original, original.length - opts.keepSuffixChars, original.length);
1811
+ const replacement = `${prefix}
1812
+
1813
+ ...${TRUNCATION_MARKER} \u2014 original ~${tokens} tokens]...
1814
+
1815
+ ${suffix}`;
1816
+ replacements.set(candidate.id, replacement);
1817
+ truncatedCount++;
1818
+ remaining -= tokens - countTokens(replacement);
1819
+ savedTokens += tokens - countTokens(replacement);
1820
+ }
1821
+ };
1822
+ applyTo(toolResults);
1823
+ applyTo(textMessages);
1824
+ if (replacements.size === 0) {
1825
+ return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound };
1826
+ }
1827
+ return {
1828
+ messages: messages.map(
1829
+ (m) => replacements.has(m.id) ? { ...m, text: replacements.get(m.id) } : m
1830
+ ),
1831
+ truncatedCount,
1832
+ savedTokens,
1833
+ candidatesFound
1834
+ };
1835
+ }
1836
+
1837
+ // src/hide-consumed.ts
1838
+ var KEEP_LAST_ORPHANED = 2;
1839
+ function rangeKey(startRef, endRef) {
1840
+ return `${startRef}::${endRef}`;
1841
+ }
1842
+ function parseCallText(text) {
1843
+ const raw = text ?? "";
1844
+ const start = raw.indexOf("{");
1845
+ if (start < 0) return null;
1846
+ let parsed;
1847
+ try {
1848
+ parsed = JSON.parse(raw.slice(start));
1849
+ } catch {
1850
+ return null;
1851
+ }
1852
+ if (!parsed || typeof parsed !== "object") return null;
1853
+ const obj = parsed;
1854
+ let content = null;
1855
+ let contentWasString = false;
1856
+ if (Array.isArray(obj.content)) {
1857
+ content = obj.content;
1858
+ } else if (typeof obj.content === "string") {
1859
+ contentWasString = true;
1860
+ try {
1861
+ const inner = JSON.parse(obj.content);
1862
+ if (Array.isArray(inner)) content = inner;
1863
+ } catch {
1864
+ content = null;
1865
+ }
1866
+ }
1867
+ if (!content || content.length === 0) return null;
1868
+ return { prefix: raw.slice(0, start), obj, content, contentWasString };
1869
+ }
1870
+ function rewriteCompressText(text, liveKeys) {
1871
+ const parsed = parseCallText(text);
1872
+ if (!parsed) return null;
1873
+ const { prefix, obj, content, contentWasString } = parsed;
1874
+ const kept = content.filter((entry) => {
1875
+ if (!entry || typeof entry !== "object") return false;
1876
+ const e = entry;
1877
+ const s = typeof e.startId === "string" ? e.startId : typeof e.messageId === "string" ? e.messageId : "";
1878
+ const end = typeof e.endId === "string" ? e.endId : typeof e.messageId === "string" ? e.messageId : "";
1879
+ return liveKeys.has(rangeKey(s, end));
1880
+ });
1881
+ if (kept.length === 0) return null;
1882
+ return prefix + serializeCompacted(obj, kept, contentWasString).text;
1883
+ }
1884
+ var SUMMARY_STUB_CHARS = 200;
1885
+ function compactEntry(entry) {
1886
+ if (!entry || typeof entry !== "object") return entry;
1887
+ const e = entry;
1888
+ if (typeof e.summary !== "string" || e.summary.length <= SUMMARY_STUB_CHARS) return entry;
1889
+ return { ...e, summary: `${clampPrefix(e.summary, SUMMARY_STUB_CHARS - 1)}\u2026` };
1890
+ }
1891
+ function serializeCompacted(obj, content, contentWasString) {
1892
+ let changed = false;
1893
+ const compacted = content.map((entry) => {
1894
+ const out = compactEntry(entry);
1895
+ if (out !== entry) changed = true;
1896
+ return out;
1897
+ });
1898
+ const outContent = contentWasString ? JSON.stringify(compacted) : compacted;
1899
+ return { text: JSON.stringify({ ...obj, content: outContent }), changed };
1900
+ }
1901
+ function compactCompressText(text) {
1902
+ const parsed = parseCallText(text);
1903
+ if (!parsed) return null;
1904
+ const { prefix, obj, content, contentWasString } = parsed;
1905
+ const { text: out, changed } = serializeCompacted(obj, content, contentWasString);
1906
+ return changed ? prefix + out : null;
1907
+ }
1908
+ function hideConsumedCompressCalls(state, messages) {
1909
+ const allBlockCallIds = /* @__PURE__ */ new Set();
1910
+ const activeCallIds = /* @__PURE__ */ new Set();
1911
+ const liveRangeKeysByCallId = /* @__PURE__ */ new Map();
1912
+ const legacyLiveByCallId = /* @__PURE__ */ new Set();
1913
+ for (const block of state.blocks) {
1914
+ if (!block.compressCallId) continue;
1915
+ allBlockCallIds.add(block.compressCallId);
1916
+ if (!block.active) continue;
1917
+ activeCallIds.add(block.compressCallId);
1918
+ if (block.startRef === void 0 || block.endRef === void 0) {
1919
+ legacyLiveByCallId.add(block.compressCallId);
1920
+ continue;
1921
+ }
1922
+ let keys = liveRangeKeysByCallId.get(block.compressCallId);
1923
+ if (!keys) {
1924
+ keys = /* @__PURE__ */ new Set();
1925
+ liveRangeKeysByCallId.set(block.compressCallId, keys);
1926
+ }
1927
+ keys.add(rangeKey(block.startRef, block.endRef));
1928
+ }
1929
+ const lastOrphanedCallIds = [];
1930
+ for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
1931
+ const message = messages[i];
1932
+ if (message.toolName !== "compress" || message.contentType !== "tool-call") continue;
1933
+ const callId = message.toolCallId;
1934
+ if (callId && !allBlockCallIds.has(callId)) {
1935
+ lastOrphanedCallIds.push(callId);
1936
+ }
1937
+ }
1938
+ const keepCallIds = /* @__PURE__ */ new Set([...activeCallIds, ...lastOrphanedCallIds]);
1939
+ const hiddenCallIds = /* @__PURE__ */ new Set();
1940
+ for (const message of messages) {
1941
+ if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
1942
+ if (message.toolCallId) hiddenCallIds.add(message.toolCallId);
1943
+ }
1944
+ }
1945
+ let hidden = 0;
1946
+ const result = [];
1947
+ for (const message of messages) {
1948
+ if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
1949
+ hidden++;
1950
+ continue;
1951
+ }
1952
+ if (message.contentType === "tool-result" && message.toolCallId && hiddenCallIds.has(message.toolCallId)) {
1953
+ hidden++;
1954
+ continue;
1955
+ }
1956
+ if (message.toolName === "compress" && message.contentType === "tool-call" && message.toolCallId && keepCallIds.has(message.toolCallId)) {
1957
+ const liveKeys = liveRangeKeysByCallId.get(message.toolCallId);
1958
+ if (liveKeys && liveKeys.size > 0 && !legacyLiveByCallId.has(message.toolCallId)) {
1959
+ const rewritten = rewriteCompressText(message.text, liveKeys);
1960
+ if (rewritten !== null) {
1961
+ result.push({ ...message, text: rewritten });
1962
+ continue;
1963
+ }
1964
+ }
1965
+ const compacted = compactCompressText(message.text);
1966
+ if (compacted !== null) {
1967
+ result.push({ ...message, text: compacted });
1968
+ continue;
1969
+ }
1970
+ }
1971
+ result.push(message);
1972
+ }
1973
+ return { messages: result, hidden };
1974
+ }
1975
+
1976
+ // src/absorb.ts
1977
+ var ABSORB_PROMPT_MARKER = "[ACP absorb]";
1978
+ var DEFAULT_ABSORB_CONFIG = {
1979
+ enabled: false,
1980
+ toolName: ABSORB_TOOL_NAME,
1981
+ // Raised 1000 → 4000 (issue #352): lossless CCR takes over large-result
1982
+ // handling; absorb's forced distillation only fires above the new bar.
1983
+ minToolTokens: 4e3,
1984
+ contextThresholdPct: 0,
1985
+ excludeTools: []
1986
+ };
1987
+ function resolveAbsorbConfig(config) {
1988
+ return { ...DEFAULT_ABSORB_CONFIG, ...config.absorb ?? {} };
1989
+ }
1990
+ function formatTokenCount(tokens) {
1991
+ if (tokens < 1e3) return String(tokens);
1992
+ if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
1993
+ return Math.round(tokens / 1e3) + "K";
1994
+ }
1995
+ function buildAbsorbPrompt(ref, tokens, toolName = ABSORB_TOOL_NAME) {
1996
+ return `${ABSORB_PROMPT_MARKER} This tool result (~${formatTokenCount(tokens)} tokens) will be REMOVED from context. Your IMMEDIATE next action: call ${toolName}({ ref: "${ref}", summary: "..." }) \u2014 summary = distilled essentials only (outcome, key values, exact paths:lines, error text verbatim, decisions). Afterwards work from your summary; do NOT re-run this tool. If the result contains nothing you need, call ${toolName} with summary "(nothing needed)".`;
1997
+ }
1998
+ function buildAbsorbSystemPrompt(toolName = ABSORB_TOOL_NAME) {
1999
+ return `INSTANT TOOL-RESULT ABSORPTION (${toolName})
2000
+
2001
+ Some tool results end with a ${ABSORB_PROMPT_MARKER} instruction. When you see one, your IMMEDIATE next action must be calling ${toolName}({ ref, summary }) \u2014 distill that tool result's essentials into summary: outcome, key values, exact paths:lines, error text verbatim, decisions. The original output is then removed from context; your ${toolName} summary becomes the only durable record of it, so distill carefully. Never call another tool or answer the user before absorbing a marked result. Do not re-run the original tool afterwards \u2014 work from your summary. ${toolName} calls are ordinary context: the regular compression system may fold them later like any other message.`;
2002
+ }
2003
+ function isAcpOrConfiguredTool(toolName, cfg) {
2004
+ if (!toolName) return false;
2005
+ if (toolName === cfg.toolName) return true;
2006
+ return ACP_TOOL_NAMES.has(toolName);
2007
+ }
2008
+ function isAbsorbCandidate(msg, config, latest) {
2009
+ if (msg.contentType !== "tool-result" || !msg.toolCallId) return false;
2010
+ if (isStoredPlaceholderText(msg.text ?? "")) return false;
2011
+ const cfg = resolveAbsorbConfig(config);
2012
+ if (isAcpOrConfiguredTool(msg.toolName, cfg)) return false;
2013
+ if (isMessageProtected(msg, config)) return false;
2014
+ if (latest && isMessageLatestProtected(msg, latest)) return false;
2015
+ for (const pattern of cfg.excludeTools) {
2016
+ if (msg.toolName && matchToolPattern(msg.toolName, pattern)) return false;
2017
+ }
2018
+ return true;
2019
+ }
2020
+ function hideAbsorbedMessages(messages, state) {
2021
+ const records = state.absorbed ?? [];
2022
+ if (records.length === 0) return messages;
2023
+ const hidden = /* @__PURE__ */ new Set();
2024
+ for (const record of records) {
2025
+ if (record.callMessageId) hidden.add(record.callMessageId);
2026
+ if (record.resultMessageId) hidden.add(record.resultMessageId);
2027
+ }
2028
+ return messages.filter((msg) => !hidden.has(msg.id));
2029
+ }
2030
+ function appendAbsorbPrompts(messages, state, config, tokenCount, countTokens) {
2031
+ const cfg = resolveAbsorbConfig(config);
2032
+ if (!cfg.enabled) return { messages, promptedCount: 0 };
2033
+ const limit = config.modelContextLimit;
2034
+ if (cfg.contextThresholdPct > 0 && limit > 0 && tokenCount < cfg.contextThresholdPct * limit) {
2035
+ return { messages, promptedCount: 0 };
2036
+ }
2037
+ const absorbedIds = /* @__PURE__ */ new Set();
2038
+ for (const record of state.absorbed ?? []) {
2039
+ if (record.resultMessageId) absorbedIds.add(record.resultMessageId);
2040
+ }
2041
+ let promptedCount = 0;
2042
+ const latest = collectLatestProtected(messages, config);
2043
+ const out = messages.map((msg) => {
2044
+ if (!isAbsorbCandidate(msg, config, latest)) return msg;
2045
+ if (absorbedIds.has(msg.id)) return msg;
2046
+ const text = msg.text ?? "";
2047
+ if (text.includes(ABSORB_PROMPT_MARKER)) return msg;
2048
+ const tokens = countTokens(text);
2049
+ if (tokens < cfg.minToolTokens) return msg;
2050
+ const ref = refForRaw(state.messageRefs, msg.id);
2051
+ if (!ref || ref === BLOCKED_REF) return msg;
2052
+ promptedCount++;
2053
+ return {
2054
+ ...msg,
2055
+ text: text + "\n\n" + buildAbsorbPrompt(ref, tokens, cfg.toolName)
2056
+ };
2057
+ });
2058
+ return { messages: out, promptedCount };
2059
+ }
2060
+ function parseAbsorbInput(input, callId, onWarn) {
2061
+ let obj = null;
2062
+ if (typeof input === "string") {
2063
+ try {
2064
+ const parsed = JSON.parse(input);
2065
+ if (parsed && typeof parsed === "object") {
2066
+ obj = parsed;
2067
+ }
2068
+ } catch {
2069
+ obj = null;
2070
+ }
2071
+ } else if (input && typeof input === "object") {
2072
+ obj = input;
2073
+ }
2074
+ if (!obj) {
2075
+ onWarn?.(`[acp-absorb-input] rejected: not an object (${typeof input})`);
2076
+ return null;
2077
+ }
2078
+ const ref = pickString(obj, "ref", "messageId", "of");
2079
+ const summary = pickString(obj, "summary", "content");
2080
+ if (typeof ref !== "string" || typeof summary !== "string") {
2081
+ onWarn?.(
2082
+ `[acp-absorb-input] rejected: need ref (string) + summary (string); keys: ${Object.keys(obj).join(",")}`
2083
+ );
2084
+ return null;
2085
+ }
2086
+ return {
2087
+ ref: ref.trim(),
2088
+ summary,
2089
+ ...callId ? { absorbCallId: callId } : {}
2090
+ };
2091
+ }
2092
+ function pickString(obj, ...keys) {
2093
+ for (const key of keys) {
2094
+ const value = obj[key];
2095
+ if (typeof value === "string") return value;
2096
+ }
2097
+ return void 0;
2098
+ }
2099
+ function applyAbsorb(input) {
2100
+ const countTokens = input.countTokens ?? ((text) => Math.ceil(text.length / 4));
2101
+ const summary = input.summary?.trim() ?? "";
2102
+ if (!summary) {
2103
+ return {
2104
+ state: input.state,
2105
+ ok: false,
2106
+ resultText: "absorb failed: summary is empty \u2014 provide the distilled key info of the tool result."
2107
+ };
2108
+ }
2109
+ const cfg = resolveAbsorbConfig(input.config);
2110
+ const rawId = rawForRef(input.state.messageRefs, input.ref.trim());
2111
+ if (!rawId) {
2112
+ return {
2113
+ state: input.state,
2114
+ ok: false,
2115
+ resultText: `absorb failed: ref ${input.ref} does not exist in this session (it may be hidden, already compressed, or stale).`
2116
+ };
2117
+ }
2118
+ const existing = (input.state.absorbed ?? []).find(
2119
+ (record2) => record2.resultMessageId === rawId
2120
+ );
2121
+ if (existing) {
2122
+ return {
2123
+ state: input.state,
2124
+ ok: true,
2125
+ resultText: `already absorbed (${input.ref}) \u2014 no change.`
2126
+ };
2127
+ }
2128
+ const target = input.messages.find((m) => m.id === rawId);
2129
+ if (!target) {
2130
+ return {
2131
+ state: input.state,
2132
+ ok: false,
2133
+ resultText: `absorb failed: ref ${input.ref} is not visible in this session (hidden or compressed).`
2134
+ };
2135
+ }
2136
+ if (target.contentType !== "tool-result") {
2137
+ return {
2138
+ state: input.state,
2139
+ ok: false,
2140
+ resultText: `absorb failed: ref ${input.ref} is a ${target.contentType}, not a tool result.`
2141
+ };
2142
+ }
2143
+ if (isAcpOrConfiguredTool(target.toolName, cfg)) {
2144
+ return {
2145
+ state: input.state,
2146
+ ok: false,
2147
+ resultText: `absorb failed: ${target.toolName} is an ACP-managed tool result \u2014 it is not absorbable.`
2148
+ };
2149
+ }
2150
+ if (isMessageProtected(target, input.config) || isMessageLatestProtected(
2151
+ target,
2152
+ collectLatestProtected(input.messages, input.config)
2153
+ )) {
2154
+ return {
2155
+ state: input.state,
2156
+ ok: false,
2157
+ resultText: `absorb failed: ${target.toolName} is a protected tool \u2014 its results must stay visible.`
2158
+ };
2159
+ }
2160
+ if (!target.toolCallId) {
2161
+ return {
2162
+ state: input.state,
2163
+ ok: false,
2164
+ resultText: `absorb failed: ref ${input.ref} has no tool-call id \u2014 cannot pair it for hiding.`
2165
+ };
2166
+ }
2167
+ const call = input.messages.find(
2168
+ (m) => m.contentType === "tool-call" && m.toolCallId === target.toolCallId
2169
+ );
2170
+ const tokens = countTokens(target.text ?? "");
2171
+ const summaryTokens = countTokens(summary);
2172
+ const record = {
2173
+ toolCallId: target.toolCallId,
2174
+ callMessageId: call?.id ?? "",
2175
+ resultMessageId: target.id,
2176
+ ...input.absorbCallId ? { absorbCallId: input.absorbCallId } : {},
2177
+ summary,
2178
+ tokensReclaimed: tokens,
2179
+ createdAt: Date.now()
2180
+ };
2181
+ const state = {
2182
+ ...input.state,
2183
+ absorbed: [...input.state.absorbed ?? [], record],
2184
+ stats: {
2185
+ ...input.state.stats,
2186
+ absorbedTokens: (input.state.stats.absorbedTokens ?? 0) + tokens
2187
+ }
2188
+ };
2189
+ const bloat = summaryTokens >= tokens && tokens > 0 ? ` WARNING: your summary (~${formatTokenCount(summaryTokens)} tokens) is not smaller than the original (~${formatTokenCount(tokens)} tokens) \u2014 distill harder next time.` : "";
2190
+ return {
2191
+ state,
2192
+ ok: true,
2193
+ resultText: `absorbed ${input.ref} (~${formatTokenCount(tokens)} tokens \u2192 summary ~${formatTokenCount(summaryTokens)}). The original tool output is now hidden; your summary is the durable record.${bloat}`
2194
+ };
2195
+ }
2196
+
2197
+ // src/crush.ts
2198
+ var DEFAULT_CRUSH_CONFIG = {
2199
+ enabled: false,
2200
+ minReduction: 0.1
2201
+ };
2202
+ var MAX_INPUT_CHARS = 2e6;
2203
+ var MAX_DEPTH = 12;
2204
+ var MAX_FOLDS = 1e3;
2205
+ var RUN_MIN = 4;
2206
+ var HOIST_MIN = 4;
2207
+ var CODE_KEEP_RATIO = 0.97;
2208
+ var CRUSH_KEY = "__acp_crush";
2209
+ function isPlainObject(v) {
2210
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
2211
+ const p = Object.getPrototypeOf(v);
2212
+ return p === null || p === Object.prototype;
2213
+ }
2214
+ function classifyCrushText(text) {
2215
+ const trimmed = text.trim();
2216
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]"))
2217
+ return "json";
2218
+ if (detectLanguage(text) !== null) return "code";
2219
+ return "log";
2220
+ }
2221
+ function crushText(text, options = {}) {
2222
+ const countTokens = options.countTokens ?? defaultCountTokens;
2223
+ const minReduction = options.minReduction ?? DEFAULT_CRUSH_CONFIG.minReduction;
2224
+ const meta = options.meta ?? {};
2225
+ if (text.length === 0 || text.length > MAX_INPUT_CHARS) return null;
2226
+ const rawTok = countTokens(text);
2227
+ if (rawTok <= 0) return null;
2228
+ const kind = classifyCrushText(text);
2229
+ const wanted = options.plugins ?? registeredPlugins();
2230
+ for (const plugin of wanted) {
2231
+ let res;
2232
+ try {
2233
+ if (!Array.isArray(plugin.kinds) || !plugin.kinds.includes(kind))
2234
+ continue;
2235
+ res = plugin.run(text, meta);
2236
+ } catch {
2237
+ continue;
2238
+ }
2239
+ if (!res || typeof res.text !== "string" || res.text === "" || res.text === text)
2240
+ continue;
2241
+ const newTok = countTokens(res.text);
2242
+ const reduction = (rawTok - newTok) / rawTok;
2243
+ if (reduction < minReduction) continue;
2244
+ if (kind === "log" && res.lossy && !errorLinesSurvive(text, res.text))
2245
+ continue;
2246
+ const out = {
2247
+ text: res.text,
2248
+ strategy: plugin.id,
2249
+ lossy: res.lossy
2250
+ };
2251
+ if (res.stats) out.stats = res.stats;
2252
+ return out;
2253
+ }
2254
+ return null;
2255
+ }
2256
+ function canonOf(v, cache2) {
2257
+ if (!isPlainObject(v) && !Array.isArray(v))
2258
+ return JSON.stringify(v) ?? "null";
2259
+ const key = v;
2260
+ const hit = cache2.get(key);
2261
+ if (hit !== void 0) return hit;
2262
+ let s;
2263
+ if (Array.isArray(v)) {
2264
+ s = `[${v.map((e) => canonOf(e, cache2)).join(",")}]`;
2265
+ } else {
2266
+ const entries = Object.entries(v).map(([k, val]) => `${JSON.stringify(k)}:${canonOf(val, cache2)}`).sort();
2267
+ s = `{${entries.join(",")}}`;
2268
+ }
2269
+ cache2.set(key, s);
2270
+ return s;
2271
+ }
2272
+ function strLen(v) {
2273
+ return JSON.stringify(v).length;
2274
+ }
2275
+ function compactValue(v, depth, stats, cache2) {
2276
+ if (depth > MAX_DEPTH || stats.folds >= MAX_FOLDS) return v;
2277
+ if (Array.isArray(v)) return compactArray(v, depth, stats, cache2);
2278
+ if (isPlainObject(v)) {
2279
+ let changed = false;
2280
+ const out = {};
2281
+ for (const [k, val] of Object.entries(v)) {
2282
+ const nv = compactValue(val, depth + 1, stats, cache2);
2283
+ out[k] = nv;
2284
+ if (nv !== val) changed = true;
2285
+ }
2286
+ return changed ? out : v;
2287
+ }
2288
+ return v;
2289
+ }
2290
+ function compactArray(arr, depth, stats, cache2) {
2291
+ const n = arr.length;
2292
+ if (n < RUN_MIN && n < HOIST_MIN)
2293
+ return arr.map((e) => compactValue(e, depth + 1, stats, cache2));
2294
+ if (stats.folds >= MAX_FOLDS) return arr;
2295
+ const plainStats = { folds: 0 };
2296
+ const plain = arr.map((e) => compactValue(e, depth + 1, plainStats, cache2));
2297
+ let winner = plain;
2298
+ let winnerLen = strLen(plain);
2299
+ let winnerFolds = plainStats.folds;
2300
+ if (n >= RUN_MIN) {
2301
+ const rf = buildRunFold(arr, depth, cache2);
2302
+ if (rf && rf.len < winnerLen) {
2303
+ winner = rf.value;
2304
+ winnerLen = rf.len;
2305
+ winnerFolds = rf.folds;
2306
+ }
2307
+ }
2308
+ if (n >= HOIST_MIN && arr.every((e) => isPlainObject(e))) {
2309
+ const hc = buildConstHoist(arr, depth, cache2);
2310
+ if (hc && hc.len < winnerLen) {
2311
+ winner = hc.value;
2312
+ winnerLen = hc.len;
2313
+ winnerFolds = hc.folds;
2314
+ }
2315
+ }
2316
+ stats.folds += winnerFolds;
2317
+ return winner;
2318
+ }
2319
+ function isRunMarker(v) {
2320
+ return isPlainObject(v) && v[CRUSH_KEY] === "identical-run";
2321
+ }
2322
+ function buildRunFold(arr, depth, cache2) {
2323
+ const n = arr.length;
2324
+ const canon = arr.map((e) => canonOf(e, cache2));
2325
+ const segs = [];
2326
+ let runs = 0;
2327
+ let i = 0;
2328
+ while (i < n) {
2329
+ let j = i;
2330
+ while (j + 1 < n && canon[j + 1] === canon[i]) j++;
2331
+ const runLen = j - i + 1;
2332
+ if (runLen >= RUN_MIN) {
2333
+ segs.push({ [CRUSH_KEY]: "identical-run", count: runLen, item: arr[i] });
2334
+ runs++;
2335
+ } else {
2336
+ for (let k = i; k <= j; k++) segs.push(arr[k]);
2337
+ }
2338
+ i = j + 1;
2339
+ }
2340
+ if (runs === 0) return null;
2341
+ const inner = { folds: 0 };
2342
+ const final = segs.map(
2343
+ (s) => isRunMarker(s) ? { ...s, item: compactValue(s.item, depth + 1, inner, cache2) } : compactValue(s, depth + 1, inner, cache2)
2344
+ );
2345
+ return { value: final, len: strLen(final), folds: runs + inner.folds };
2346
+ }
2347
+ function buildConstHoist(arr, depth, cache2) {
2348
+ const n = arr.length;
2349
+ const keys = [];
2350
+ const seen = /* @__PURE__ */ new Set();
2351
+ for (const e of arr) {
2352
+ for (const k of Object.keys(e)) {
2353
+ if (!seen.has(k)) {
2354
+ seen.add(k);
2355
+ keys.push(k);
2356
+ }
2357
+ }
2358
+ }
2359
+ const first = arr[0];
2360
+ if (!first) return null;
2361
+ const constKeys = [];
2362
+ for (const k of keys) {
2363
+ if (!(k in first)) continue;
2364
+ const c0 = canonOf(first[k], cache2);
2365
+ let constant = true;
2366
+ for (let i = 1; i < n; i++) {
2367
+ const row = arr[i];
2368
+ if (!(k in row) || canonOf(row[k], cache2) !== c0) {
2369
+ constant = false;
2370
+ break;
2371
+ }
2372
+ }
2373
+ if (constant) constKeys.push(k);
2374
+ }
2375
+ if (constKeys.length === 0) return null;
2376
+ const constSet = new Set(constKeys);
2377
+ const constObj = {};
2378
+ for (const k of constKeys) constObj[k] = first[k];
2379
+ const items = arr.map((e) => {
2380
+ const o = {};
2381
+ for (const [k, v] of Object.entries(e)) {
2382
+ if (!constSet.has(k)) o[k] = v;
2383
+ }
2384
+ return o;
2385
+ });
2386
+ const hasVarying = items.some((o) => Object.keys(o).length > 0);
2387
+ const inner = { folds: 0 };
2388
+ const env = {
2389
+ [CRUSH_KEY]: "rows",
2390
+ rows: n,
2391
+ const: compactValue(constObj, depth + 1, inner, cache2)
2392
+ };
2393
+ if (hasVarying)
2394
+ env.items = items.map((it) => compactValue(it, depth + 1, inner, cache2));
2395
+ const len = strLen(env);
2396
+ if (len >= strLen(arr)) return null;
2397
+ return { value: env, len, folds: 1 + inner.folds };
2398
+ }
2399
+ function crushJson(text) {
2400
+ const parsed = JSON.parse(text);
2401
+ const cache2 = /* @__PURE__ */ new Map();
2402
+ const stats = { folds: 0 };
2403
+ const out = compactValue(parsed, 0, stats, cache2);
2404
+ if (stats.folds === 0) return null;
2405
+ const s = JSON.stringify(out);
2406
+ return s.length < text.length ? s : null;
2407
+ }
2408
+ function detectLanguage(text) {
2409
+ const sample = text.split(/\r?\n/).slice(0, 300);
2410
+ let py = 0;
2411
+ let js = 0;
2412
+ for (const line of sample) {
2413
+ if (/^\s*#!.*python/.test(line)) py += 4;
2414
+ if (/^\s*(async\s+)?def\s+\w/.test(line) || /^\s*class\s+\w/.test(line))
2415
+ py += 2;
2416
+ if (/\bfrom\s+['"]/.test(line) || /^\s*import\s+['"]/.test(line)) js += 2;
2417
+ else if (/^\s*(import|from)\s+[A-Za-z_]\w*/.test(line)) py += 1;
2418
+ if (/^\s*(function\b|const\s|let\s|var\s)/.test(line)) js += 2;
2419
+ if (/=>/.test(line)) js += 1;
2420
+ if (/^\s*(public|private|protected|static|final|void|return)\b/.test(line))
2421
+ js += 1;
2422
+ if (/;\s*$/.test(line) && line.trim().length > 0) js += 1;
2423
+ if (/\bself\./.test(line) || /^\s*(elif|except|yield)\b/.test(line))
2424
+ py += 1;
2425
+ if (/\bconsole\.log\b|\brequire\s*\(|module\.exports|\bawait\s/.test(line))
2426
+ js += 1;
2427
+ }
2428
+ const MARGIN = 1.5;
2429
+ if (py >= 3 && py > js * MARGIN) return "python";
2430
+ if (js >= 4 && js > py * MARGIN) return "js";
2431
+ return null;
2432
+ }
2433
+ function finalizeTrimmed(original, emitted) {
2434
+ if (emitted.length >= original.split(/\r?\n/).length) return null;
2435
+ const out = emitted.join("\n") + (original.endsWith("\n") ? "\n" : "");
2436
+ return out.length < original.length * CODE_KEEP_RATIO ? out : null;
2437
+ }
2438
+ function trimPython(text) {
2439
+ const lines = text.split(/\r?\n/);
2440
+ const emitted = [];
2441
+ let pending = 0;
2442
+ const flush = () => {
2443
+ if (pending > 0) {
2444
+ emitted.push(
2445
+ `# [acp-crush: elided ${pending} line${pending === 1 ? "" : "s"}]`
2446
+ );
2447
+ pending = 0;
2448
+ }
2449
+ };
2450
+ let state = "code";
2451
+ let quote = "";
2452
+ let isDocstring = false;
2453
+ let blankRun = 0;
2454
+ let atModuleStart = true;
2455
+ let pendingDefClass = false;
2456
+ let docstringSlot = false;
2457
+ const classifySig = (codePart) => {
2458
+ const t = codePart.trim();
2459
+ if (/^(async\s+)?(def|class)\b/.test(t)) pendingDefClass = true;
2460
+ if (t.endsWith(":")) {
2461
+ docstringSlot = pendingDefClass;
2462
+ pendingDefClass = false;
2463
+ } else if (t.length > 0 && !t.startsWith("@")) {
2464
+ docstringSlot = false;
2465
+ }
2466
+ atModuleStart = false;
2467
+ };
2468
+ for (let li = 0; li < lines.length; li++) {
2469
+ const line = lines[li];
2470
+ if (state === "triple") {
2471
+ const close = findTripleClose(line, quote);
2472
+ if (close === -1) {
2473
+ if (isDocstring) pending++;
2474
+ else emitted.push(line);
2475
+ continue;
2476
+ }
2477
+ state = "code";
2478
+ const rest = line.slice(close + 3).trim();
2479
+ if (rest.length > 0) {
2480
+ flush();
2481
+ emitted.push(line);
2482
+ classifySig(rest);
2483
+ } else {
2484
+ if (isDocstring) pending++;
2485
+ else emitted.push(line);
2486
+ }
2487
+ isDocstring = false;
2488
+ docstringSlot = false;
2489
+ pendingDefClass = false;
2490
+ atModuleStart = false;
2491
+ blankRun = 0;
2492
+ continue;
2493
+ }
2494
+ if (line.trim().length === 0) {
2495
+ blankRun++;
2496
+ if (blankRun >= 3) {
2497
+ flush();
2498
+ emitted.push("");
2499
+ blankRun = 1;
2500
+ }
2501
+ continue;
2502
+ }
2503
+ if (li === 0 && line.startsWith("#!")) {
2504
+ flush();
2505
+ emitted.push(line);
2506
+ blankRun = 0;
2507
+ continue;
2508
+ }
2509
+ const m = line.match(/^\s*("""|''')/);
2510
+ if (m) {
2511
+ flush();
2512
+ const q = m[1];
2513
+ const opensHere = line.indexOf(q);
2514
+ const closeOnSameLine = findTripleClose(line.slice(opensHere + 3), q);
2515
+ if (closeOnSameLine !== -1) {
2516
+ const restAfter = line.slice(opensHere + 3 + closeOnSameLine + 3).trim();
2517
+ if (restAfter.length > 0) {
2518
+ emitted.push(line);
2519
+ classifySig(restAfter);
2520
+ } else if (atModuleStart || docstringSlot) {
2521
+ pending++;
2522
+ atModuleStart = false;
2523
+ docstringSlot = false;
2524
+ pendingDefClass = false;
2525
+ } else {
2526
+ emitted.push(line);
2527
+ }
2528
+ blankRun = 0;
2529
+ continue;
2530
+ }
2531
+ state = "triple";
2532
+ quote = q;
2533
+ isDocstring = atModuleStart || docstringSlot;
2534
+ atModuleStart = false;
2535
+ docstringSlot = false;
2536
+ if (!isDocstring) emitted.push(line);
2537
+ blankRun = 0;
2538
+ continue;
2539
+ }
2540
+ const probe = findCommentOrTriple(line);
2541
+ if (probe.kind === "comment-full") {
2542
+ pending++;
2543
+ continue;
2544
+ }
2545
+ if (probe.kind === "triple-mid") {
2546
+ flush();
2547
+ state = "triple";
2548
+ quote = probe.quote;
2549
+ isDocstring = false;
2550
+ emitted.push(line);
2551
+ classifySig(line.slice(0, probe.idx));
2552
+ blankRun = 0;
2553
+ continue;
2554
+ }
2555
+ flush();
2556
+ emitted.push(line);
2557
+ classifySig(line);
2558
+ blankRun = 0;
2559
+ }
2560
+ if (state === "triple") return null;
2561
+ flush();
2562
+ return finalizeTrimmed(text, emitted);
2563
+ }
2564
+ function findTripleClose(line, quote) {
2565
+ let i = 0;
2566
+ while (i < line.length) {
2567
+ if (line[i] === "\\") {
2568
+ i += 2;
2569
+ continue;
2570
+ }
2571
+ if (line.startsWith(quote, i)) return i;
2572
+ i++;
2573
+ }
2574
+ return -1;
2575
+ }
2576
+ function findCommentOrTriple(line) {
2577
+ let i = 0;
2578
+ let inStr = null;
2579
+ while (i < line.length) {
2580
+ const c = line[i];
2581
+ if (inStr) {
2582
+ if (c === "\\") i += 2;
2583
+ else if (c === inStr) inStr = null;
2584
+ else i++;
2585
+ continue;
2586
+ }
2587
+ if (c === "'" || c === '"') {
2588
+ inStr = c;
2589
+ i++;
2590
+ continue;
2591
+ }
2592
+ if (c === "#") {
2593
+ return {
2594
+ kind: line.slice(0, i).trim().length === 0 ? "comment-full" : "none",
2595
+ idx: i,
2596
+ quote: ""
2597
+ };
2598
+ }
2599
+ if (line.startsWith('"""', i) || line.startsWith("'''", i)) {
2600
+ return { kind: "triple-mid", idx: i, quote: line.slice(i, i + 3) };
2601
+ }
2602
+ i++;
2603
+ }
2604
+ return { kind: "none", idx: -1, quote: "" };
2605
+ }
2606
+ function scanTemplateRest(line, from, st) {
2607
+ let m = st.mode;
2608
+ let q = st.quote;
2609
+ let d = st.depth;
2610
+ let i = from;
2611
+ while (i < line.length) {
2612
+ const c = line[i];
2613
+ if (m === "str") {
2614
+ if (c === "\\") i += 2;
2615
+ else if (c === q) m = "expr";
2616
+ else i++;
2617
+ continue;
2618
+ }
2619
+ if (m === "expr") {
2620
+ if (c === "'" || c === '"') {
2621
+ q = c;
2622
+ m = "str";
2623
+ i++;
2624
+ continue;
2625
+ }
2626
+ if (c === "`")
2627
+ return {
2628
+ mode: m,
2629
+ quote: q,
2630
+ depth: d,
2631
+ closed: false,
2632
+ bail: true,
2633
+ consumed: 0
2634
+ };
2635
+ if (c === "{") d++;
2636
+ else if (c === "}") {
2637
+ d--;
2638
+ if (d === 0) m = "tpl";
2639
+ }
2640
+ i++;
2641
+ continue;
2642
+ }
2643
+ if (c === "\\") {
2644
+ i += 2;
2645
+ continue;
2646
+ }
2647
+ if (c === "`")
2648
+ return {
2649
+ mode: "tpl",
2650
+ quote: "",
2651
+ depth: 0,
2652
+ closed: true,
2653
+ bail: false,
2654
+ consumed: i - from + 1
2655
+ };
2656
+ if (c === "$" && line[i + 1] === "{") {
2657
+ m = "expr";
2658
+ d = 1;
2659
+ i += 2;
2660
+ continue;
2661
+ }
2662
+ i++;
2663
+ }
2664
+ return {
2665
+ mode: m,
2666
+ quote: q,
2667
+ depth: d,
2668
+ closed: false,
2669
+ bail: false,
2670
+ consumed: 0
2671
+ };
2672
+ }
2673
+ function regexAllowedAfter(prev) {
2674
+ if (prev === null) return true;
2675
+ if (/[A-Za-z0-9_]/.test(prev)) return false;
2676
+ return !".'\"`) ]".includes(prev);
2677
+ }
2678
+ function prevSigChar(line, i) {
2679
+ for (let j = i - 1; j >= 0; j--) {
2680
+ const c = line[j];
2681
+ if (c === " " || c === " ") continue;
2682
+ return c;
2683
+ }
2684
+ return null;
2685
+ }
2686
+ function scanRegexLiteral(line, i) {
2687
+ let j = i + 1;
2688
+ let inClass = false;
2689
+ while (j < line.length) {
2690
+ const c = line[j];
2691
+ if (c === "\\") {
2692
+ j += 2;
2693
+ continue;
2694
+ }
2695
+ if (inClass) {
2696
+ if (c === "]") inClass = false;
2697
+ } else if (c === "[") {
2698
+ inClass = true;
2699
+ } else if (c === "/") {
2700
+ let f = j + 1;
2701
+ while (f < line.length && /[a-z]/i.test(line[f])) f++;
2702
+ return f;
2703
+ }
2704
+ j++;
2705
+ }
2706
+ return null;
2707
+ }
2708
+ function scanJsCodeLine(line) {
2709
+ let i = 0;
2710
+ let inStr = null;
2711
+ while (i < line.length) {
2712
+ const c = line[i];
2713
+ if (inStr) {
2714
+ if (c === "\\") i += 2;
2715
+ else if (c === inStr) inStr = null;
2716
+ else i++;
2717
+ continue;
2718
+ }
2719
+ if (c === "'" || c === '"') {
2720
+ inStr = c;
2721
+ i++;
2722
+ continue;
2723
+ }
2724
+ if (c === "`") {
2725
+ const r = scanTemplateRest(line, i, { mode: "tpl", quote: "", depth: 0 });
2726
+ if (r.bail) return { kind: "code", bail: true };
2727
+ if (!r.closed)
2728
+ return {
2729
+ kind: "template-open",
2730
+ tmpl: { mode: r.mode, quote: r.quote, depth: r.depth }
2731
+ };
2732
+ i += r.consumed;
2733
+ continue;
2734
+ }
2735
+ if (c === "/") {
2736
+ const nxt = line[i + 1];
2737
+ if (nxt === "/") {
2738
+ return {
2739
+ kind: line.slice(0, i).trim().length === 0 ? "comment-full" : "code"
2740
+ };
2741
+ }
2742
+ if (nxt !== "*") {
2743
+ if (regexAllowedAfter(prevSigChar(line, i))) {
2744
+ const end = scanRegexLiteral(line, i);
2745
+ if (end !== null) {
2746
+ i = end;
2747
+ continue;
2748
+ }
2749
+ }
2750
+ i++;
2751
+ continue;
2752
+ }
2753
+ const close = line.indexOf("*/", i + 2);
2754
+ const before = line.slice(0, i).trim().length > 0;
2755
+ if (close === -1)
2756
+ return { kind: before ? "block-open-mid" : "block-open-only" };
2757
+ const rest = line.slice(close + 2).trim();
2758
+ if (rest.length === 0) return { kind: before ? "code" : "comment-full" };
2759
+ i = close + 2;
2760
+ continue;
2761
+ }
2762
+ i++;
2763
+ }
2764
+ return { kind: "code" };
2765
+ }
2766
+ function trimJsTs(text) {
2767
+ const lines = text.split(/\r?\n/);
2768
+ const emitted = [];
2769
+ let pending = 0;
2770
+ const flush = () => {
2771
+ if (pending > 0) {
2772
+ emitted.push(
2773
+ `// [acp-crush: elided ${pending} line${pending === 1 ? "" : "s"}]`
2774
+ );
2775
+ pending = 0;
2776
+ }
2777
+ };
2778
+ let state = "code";
2779
+ let tmpl = { mode: "tpl", quote: "", depth: 0 };
2780
+ let blankRun = 0;
2781
+ for (const line of lines) {
2782
+ if (state === "block") {
2783
+ const close = line.indexOf("*/");
2784
+ if (close === -1) {
2785
+ pending++;
2786
+ continue;
2787
+ }
2788
+ state = "code";
2789
+ if (line.slice(close + 2).trim().length === 0) pending++;
2790
+ else {
2791
+ flush();
2792
+ emitted.push(line);
2793
+ }
2794
+ blankRun = 0;
2795
+ continue;
2796
+ }
2797
+ if (state === "template") {
2798
+ const r = scanTemplateRest(line, 0, tmpl);
2799
+ if (r.bail) return null;
2800
+ tmpl = { mode: r.mode, quote: r.quote, depth: r.depth };
2801
+ if (r.closed) state = "code";
2802
+ emitted.push(line);
2803
+ blankRun = 0;
2804
+ continue;
2805
+ }
2806
+ if (line.trim().length === 0) {
2807
+ blankRun++;
2808
+ if (blankRun >= 3) {
2809
+ flush();
2810
+ emitted.push("");
2811
+ blankRun = 1;
2812
+ }
2813
+ continue;
2814
+ }
2815
+ const scan = scanJsCodeLine(line);
2816
+ if (scan.bail) return null;
2817
+ if (scan.kind === "comment-full") {
2818
+ pending++;
2819
+ continue;
2820
+ }
2821
+ if (scan.kind === "block-open-only") {
2822
+ pending++;
2823
+ state = "block";
2824
+ continue;
2825
+ }
2826
+ if (scan.kind === "block-open-mid") {
2827
+ flush();
2828
+ state = "block";
2829
+ emitted.push(line);
2830
+ blankRun = 0;
2831
+ continue;
2832
+ }
2833
+ if (scan.kind === "template-open") {
2834
+ flush();
2835
+ state = "template";
2836
+ tmpl = scan.tmpl ?? { mode: "tpl", quote: "", depth: 0 };
2837
+ emitted.push(line);
2838
+ blankRun = 0;
2839
+ continue;
2840
+ }
2841
+ flush();
2842
+ emitted.push(line);
2843
+ blankRun = 0;
2844
+ }
2845
+ if (state !== "code") return null;
2846
+ flush();
2847
+ return finalizeTrimmed(text, emitted);
2848
+ }
2849
+ function crushCode(text) {
2850
+ const lang = detectLanguage(text);
2851
+ if (lang === null) return null;
2852
+ if (lang === "python") return trimPython(text);
2853
+ return trimJsTs(text);
2854
+ }
2855
+ var LOG_MIN_LINES = 50;
2856
+ var LOG_MAX_TOTAL_LINES = 100;
2857
+ var LOG_MAX_ERRORS = 20;
2858
+ var LOG_ERROR_CONTEXT = 3;
2859
+ var LOG_MAX_WARNINGS = 5;
2860
+ var LOG_MAX_STACK_TRACES = 3;
2861
+ var LOG_STACK_MAX_LINES = 20;
2862
+ var LOG_TRACE_HEAD_FRAMES = 3;
2863
+ var LOG_TRACE_APP_FRAMES = 5;
2864
+ var LOG_CLASSIFIED_GATE = 5;
2865
+ var LOG_SUMMARY_GATE = 3;
2866
+ var LEVEL_PATTERNS = [
2867
+ ["error", /\b(ERROR|FATAL|CRITICAL)\b/i],
2868
+ ["fail", /\b(FAIL|FAILED)\b/i],
2869
+ ["warn", /\b(WARN|WARNING)\b/i],
2870
+ ["info", /\bINFO\b/i],
2871
+ ["debug", /\bDEBUG\b/i],
2872
+ ["trace", /\bTRACE\b/i]
2873
+ ];
2874
+ function classifyLevel(line) {
2875
+ for (const [level, re] of LEVEL_PATTERNS) {
2876
+ if (re.test(line)) return level;
2877
+ }
2878
+ return "unknown";
2879
+ }
2880
+ function isLogSummaryLine(line) {
2881
+ if (line.startsWith("===") || line.startsWith("---")) return true;
2882
+ let d = 0;
2883
+ while (d < line.length && line.charCodeAt(d) >= 48 && line.charCodeAt(d) <= 57)
2884
+ d++;
2885
+ if (d > 0 && line[d] === " ") {
2886
+ const rest = line.slice(d + 1);
2887
+ if (rest.startsWith("passed") || rest.startsWith("failed") || rest.startsWith("skipped") || rest.startsWith("error") || rest.startsWith("warning"))
2888
+ return true;
2889
+ }
2890
+ for (const prefix of [
2891
+ "Test ",
2892
+ "Tests ",
2893
+ "Tests:",
2894
+ "Test:",
2895
+ "Suite ",
2896
+ "Suites ",
2897
+ "Suites:",
2898
+ "Suite:"
2899
+ ]) {
2900
+ if (line.startsWith(prefix)) {
2901
+ const m = line.slice(prefix.length).match(/\S/);
2902
+ if (m !== null && m[0].charCodeAt(0) >= 48 && m[0].charCodeAt(0) <= 57)
2903
+ return true;
2904
+ }
2905
+ }
2906
+ if (line.startsWith("TOTAL") || line.startsWith("Total") || line.startsWith("Summary"))
2907
+ return true;
2908
+ for (const prefix of ["Build", "Compile", "Test"]) {
2909
+ if (line.startsWith(prefix) && (line.includes("succeeded") || line.includes("failed") || line.includes("complete")))
2910
+ return true;
2911
+ }
2912
+ return false;
2913
+ }
2914
+ function isDigitChar(c) {
2915
+ return c >= "0" && c <= "9";
2916
+ }
2917
+ function isAsciiAlnum(c) {
2918
+ const n = c.charCodeAt(0);
2919
+ return n >= 97 && n <= 122 || n >= 65 && n <= 90 || n >= 48 && n <= 57;
2920
+ }
2921
+ function hasLineColSuffix(s) {
2922
+ for (let i = 0; i + 1 < s.length; i++) {
2923
+ if (s[i] === ":" && isDigitChar(s[i + 1])) {
2924
+ let j = i + 1;
2925
+ while (j < s.length && isDigitChar(s[j])) j++;
2926
+ if (j + 1 < s.length && s[j] === ":" && isDigitChar(s[j + 1]))
2927
+ return true;
2928
+ }
2929
+ }
2930
+ return false;
2931
+ }
2932
+ function isPythonFileFrame(s) {
2933
+ return s.startsWith('File "') && s.includes('", line ') && s.length > 0 && isDigitChar(s[s.length - 1]);
2934
+ }
2935
+ function isJsAtFrame(s) {
2936
+ return s.startsWith("at ") && s.includes("(") && s.includes(")") && hasLineColSuffix(s);
2937
+ }
2938
+ function isJavaAtFrame(s) {
2939
+ if (!s.startsWith("at ") || !s.includes("(")) return false;
2940
+ const open = s.indexOf("(");
2941
+ const body = s.slice(3, open);
2942
+ if (body.length === 0) return false;
2943
+ for (const c of body) {
2944
+ if (!(isAsciiAlnum(c) || c === "." || c === "_" || c === "$" || c === "/"))
2945
+ return false;
2946
+ }
2947
+ return true;
2948
+ }
2949
+ function isRustPanicOpener(s) {
2950
+ return s.startsWith("thread '") && s.includes("panicked at");
2951
+ }
2952
+ function isGoroutineHeader(line) {
2953
+ if (!line.startsWith("goroutine ")) return false;
2954
+ const rest = line.slice(10);
2955
+ let d = 0;
2956
+ while (d < rest.length && isDigitChar(rest[d])) d++;
2957
+ return d > 0 && rest.slice(d).startsWith(" [");
2958
+ }
2959
+ function isGoPanicOpener(line) {
2960
+ return line.startsWith("panic: ") || line.startsWith("fatal error: ") || isGoroutineHeader(line);
2961
+ }
2962
+ function isGoFileFrame(line) {
2963
+ return line.startsWith(" ") && line.includes(".go:") && line.includes(" +0x");
2964
+ }
2965
+ function isGoCallFrame(line) {
2966
+ if (line.startsWith("created by ")) return true;
2967
+ if (line.startsWith(" ") || line.startsWith(" ") || !line.endsWith(")"))
2968
+ return false;
2969
+ const open = line.indexOf("(");
2970
+ if (open === -1) return false;
2971
+ const symbol = line.slice(0, open);
2972
+ if (symbol.length === 0 || !symbol.includes(".")) return false;
2973
+ for (const c of symbol) {
2974
+ if (!(isAsciiAlnum(c) || c === "." || c === "_" || c === "/" || c === "*"))
2975
+ return false;
2976
+ }
2977
+ return true;
2978
+ }
2979
+ function isDotnetFrame(s) {
2980
+ return s.startsWith("at ") && s.includes(") in ") && s.includes(":line ");
2981
+ }
2982
+ function isDotnetExceptionHead(trimmed) {
2983
+ const colon = trimmed.indexOf(":");
2984
+ if (colon === -1) return false;
2985
+ const head = trimmed.slice(0, colon);
2986
+ if (!head.endsWith("Exception") || !head.includes(".")) return false;
2987
+ for (const c of head) {
2988
+ if (!(isAsciiAlnum(c) || c === "." || c === "_" || c === "`" || c === "+"))
2989
+ return false;
2990
+ }
2991
+ return true;
2992
+ }
2993
+ function isRustBacktraceFrame(s) {
2994
+ const t = s.trimStart();
2995
+ let i = 0;
2996
+ while (i < t.length && isDigitChar(t[i])) i++;
2997
+ if (i === 0 || t[i] !== ":") return false;
2998
+ i++;
2999
+ while (i < t.length && t[i] === " ") i++;
3000
+ const rest = t.slice(i);
3001
+ if (!rest.startsWith("0x")) return false;
3002
+ let h = 0;
3003
+ for (const c of rest.slice(2)) {
3004
+ if (c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F")
3005
+ h++;
3006
+ else break;
3007
+ }
3008
+ return h > 0;
3009
+ }
3010
+ function isJavaMoreSummary(trimmed) {
3011
+ if (!trimmed.startsWith("... ")) return false;
3012
+ const rest = trimmed.slice(4);
3013
+ let d = 0;
3014
+ while (d < rest.length && isDigitChar(rest[d])) d++;
3015
+ return d > 0 && rest.slice(d).trim() === "more";
3016
+ }
3017
+ function traceFlavorFor(line) {
3018
+ const t = line.trimStart();
3019
+ if (t.startsWith("Traceback (most recent call last)") || isPythonFileFrame(t))
3020
+ return "py";
3021
+ if (t.startsWith("Unhandled exception.") || isDotnetFrame(t)) return "dotnet";
3022
+ if (isJsAtFrame(t)) return "js";
3023
+ if (isJavaAtFrame(t)) return "java";
3024
+ if (t.startsWith("--> ") && hasLineColSuffix(t)) return "rust-error";
3025
+ if (isRustPanicOpener(t) || t.startsWith("stack backtrace:") || isRustBacktraceFrame(line))
3026
+ return "rust-backtrace";
3027
+ if (isGoPanicOpener(line)) return "go-panic";
3028
+ return null;
3029
+ }
3030
+ function traceTerminates(flavor, line, linesSoFar) {
3031
+ const t = line.trimStart();
3032
+ switch (flavor) {
3033
+ case "py": {
3034
+ const indentedOrBlank = line.startsWith(" ") || line.startsWith(" ") || line.length === 0;
3035
+ const continuation = t.startsWith("Traceback") || t.startsWith("File ") || t.startsWith("During handling") || t.startsWith("The above exception");
3036
+ if (indentedOrBlank || continuation) return false;
3037
+ return !(t.length > 0 && t[0] >= "A" && t[0] <= "Z");
3038
+ }
3039
+ case "js":
3040
+ return !t.startsWith("at ") && line.length !== 0;
3041
+ case "java": {
3042
+ const chain = t.startsWith("Caused by:") || t.startsWith("Suppressed:") || isJavaMoreSummary(t);
3043
+ return !t.startsWith("at ") && !chain && line.length !== 0;
3044
+ }
3045
+ case "dotnet": {
3046
+ if (line.length === 0) return false;
3047
+ const continues = t.startsWith("at ") || t.startsWith("--->") || t.startsWith("--- End of") || isDotnetExceptionHead(t);
3048
+ return !continues;
3049
+ }
3050
+ case "rust-error":
3051
+ return !t.startsWith("--> ") && line.length !== 0;
3052
+ case "rust-backtrace": {
3053
+ if (line.length === 0 || linesSoFar === 1) return false;
3054
+ const isFrame = t.length > 0 && isDigitChar(t[0]);
3055
+ const continuation = line.startsWith(" ") || line.startsWith(" ") || t.startsWith("stack backtrace:") || t.startsWith("note: run with");
3056
+ return !isFrame && !continuation;
3057
+ }
3058
+ case "go-panic": {
3059
+ if (line.length === 0) return false;
3060
+ const continues = line.startsWith(" ") || isGoroutineHeader(line) || isGoCallFrame(line) || line.startsWith("panic: ") || line.startsWith("fatal error: ") || line.startsWith("[signal ");
3061
+ return !continues;
3062
+ }
3063
+ }
3064
+ }
3065
+ function scoreLogLine(l) {
3066
+ const base = l.level === "error" || l.level === "fail" ? 1 : l.level === "warn" ? 0.5 : l.level === "info" || l.level === "unknown" ? 0.1 : l.level === "debug" ? 0.05 : 0.02;
3067
+ return Math.min(base + (l.isStack ? 0.3 : 0) + (l.isSummary ? 0.4 : 0), 1);
3068
+ }
3069
+ function parseLogLines(lines) {
3070
+ const out = new Array(lines.length);
3071
+ let active = null;
3072
+ let traceLines = 0;
3073
+ for (let i = 0; i < lines.length; i++) {
3074
+ const line = lines[i];
3075
+ const entry = {
3076
+ i,
3077
+ content: line,
3078
+ level: classifyLevel(line),
3079
+ isStack: false,
3080
+ isSummary: isLogSummaryLine(line),
3081
+ score: 0
3082
+ };
3083
+ if (active !== null) {
3084
+ const flavor = active;
3085
+ if (traceLines >= LOG_STACK_MAX_LINES || traceTerminates(flavor, line, traceLines)) {
3086
+ const capHit = traceLines >= LOG_STACK_MAX_LINES;
3087
+ active = null;
3088
+ traceLines = 0;
3089
+ const nf = traceFlavorFor(line);
3090
+ if (nf !== null) {
3091
+ active = nf;
3092
+ traceLines = 1;
3093
+ entry.isStack = true;
3094
+ } else if (capHit && !traceTerminates(flavor, line, 2)) {
3095
+ active = flavor;
3096
+ traceLines = 1;
3097
+ entry.isStack = true;
3098
+ }
3099
+ } else {
3100
+ entry.isStack = true;
3101
+ traceLines++;
3102
+ }
3103
+ } else {
3104
+ const f = traceFlavorFor(line);
3105
+ if (f !== null) {
3106
+ active = f;
3107
+ traceLines = 1;
3108
+ entry.isStack = true;
3109
+ }
3110
+ }
3111
+ entry.score = scoreLogLine(entry);
3112
+ out[i] = entry;
3113
+ }
3114
+ return out;
3115
+ }
3116
+ function selectWithFirstLast(arr, maxCount) {
3117
+ if (arr.length <= maxCount) return arr.slice();
3118
+ const out = [];
3119
+ const seen = /* @__PURE__ */ new Set();
3120
+ const push = (l) => {
3121
+ if (seen.add(l.i)) out.push(l);
3122
+ };
3123
+ push(arr[0]);
3124
+ push(arr[arr.length - 1]);
3125
+ if (out.length < maxCount) {
3126
+ const byScore = arr.slice().sort((a, b) => b.score - a.score || a.i - b.i);
3127
+ for (const l of byScore) {
3128
+ if (!seen.has(l.i)) {
3129
+ push(l);
3130
+ if (out.length >= maxCount) break;
3131
+ }
3132
+ }
3133
+ }
3134
+ return out;
3135
+ }
3136
+ function normalizeForDedupe(content) {
3137
+ let splitAt = content.length;
3138
+ for (let i = 0; i < content.length; i++) {
3139
+ if (content[i] === ":" || content[i] === "=") {
3140
+ splitAt = i;
3141
+ break;
3142
+ }
3143
+ }
3144
+ const suffix = content.slice(splitAt).replace(/\d+/g, "N").replace(/0x[0-9a-fA-F]+/g, "ADDR").replace(/\/[\w/]+\//g, "/PATH/");
3145
+ return content.slice(0, splitAt) + suffix;
3146
+ }
3147
+ var RUNTIME_FRAME_PREFIXES = [
3148
+ "at java.",
3149
+ "at jdk.",
3150
+ "at sun.",
3151
+ "at javax.",
3152
+ "at scala.",
3153
+ "at System.",
3154
+ "at Microsoft.",
3155
+ "runtime.",
3156
+ "created by runtime."
3157
+ ];
3158
+ var RUNTIME_FRAME_MARKERS = [
3159
+ "site-packages/",
3160
+ "/usr/lib/python",
3161
+ "lib/python3.",
3162
+ "node:internal/",
3163
+ "node_modules/",
3164
+ "(internal/",
3165
+ "core::",
3166
+ "std::",
3167
+ "alloc::",
3168
+ "rust_begin_unwind",
3169
+ "__rust_",
3170
+ "/rustc/",
3171
+ "/usr/local/go/src/",
3172
+ "/libexec/src/runtime/"
3173
+ ];
3174
+ function isFrameLine(content) {
3175
+ const t = content.trimStart();
3176
+ return t.startsWith("at ") || t.startsWith('File "') && t.includes('", line ') || isRustBacktraceFrame(content) || isGoFileFrame(content) || isGoCallFrame(content);
3177
+ }
3178
+ function isChainHeadLine(content) {
3179
+ const t = content.trimStart();
3180
+ return t.startsWith("Caused by:") || t.startsWith("Suppressed:") || t.startsWith("... ") || t.startsWith("--->") || t.startsWith("--- End of") || t.startsWith("During handling") || t.startsWith("The above exception");
3181
+ }
3182
+ function isRuntimeFrame(content) {
3183
+ const t = content.trimStart();
3184
+ return RUNTIME_FRAME_PREFIXES.some((p) => t.startsWith(p)) || RUNTIME_FRAME_MARKERS.some((m) => content.includes(m));
3185
+ }
3186
+ function collapseTraceFrames(stack, headFrames, appFrames) {
3187
+ const kept = [];
3188
+ const dropped = /* @__PURE__ */ new Set();
3189
+ let framesSeen = 0;
3190
+ let appKept = 0;
3191
+ let runStart = -1;
3192
+ let runLen = 0;
3193
+ let prevDropped = false;
3194
+ const flushRun = () => {
3195
+ if (runStart !== -1) {
3196
+ kept.push({
3197
+ i: runStart,
3198
+ content: ` [... ${runLen} frames collapsed]`,
3199
+ level: "unknown",
3200
+ isStack: true,
3201
+ isSummary: false,
3202
+ score: 0.8
3203
+ });
3204
+ runStart = -1;
3205
+ runLen = 0;
3206
+ }
3207
+ };
3208
+ for (const line of stack) {
3209
+ if (isFrameLine(line.content) && !isChainHeadLine(line.content)) {
3210
+ framesSeen++;
3211
+ const runtime = isRuntimeFrame(line.content);
3212
+ const keep = framesSeen <= headFrames || !runtime && appKept < appFrames;
3213
+ if (keep) {
3214
+ if (!runtime) appKept++;
3215
+ flushRun();
3216
+ kept.push(line);
3217
+ prevDropped = false;
3218
+ } else {
3219
+ if (runStart === -1) runStart = line.i;
3220
+ runLen++;
3221
+ dropped.add(line.i);
3222
+ prevDropped = true;
3223
+ }
3224
+ } else if (prevDropped && (line.content.startsWith(" ") || line.content.startsWith(" ")) && !isChainHeadLine(line.content)) {
3225
+ runLen++;
3226
+ dropped.add(line.i);
3227
+ } else {
3228
+ flushRun();
3229
+ kept.push(line);
3230
+ prevDropped = false;
3231
+ }
1672
3232
  }
1673
- return void 0;
3233
+ flushRun();
3234
+ return { kept, dropped };
1674
3235
  }
1675
- function applyAbsorb(input) {
1676
- const countTokens = input.countTokens ?? ((text) => Math.ceil(text.length / 4));
1677
- const summary = input.summary?.trim() ?? "";
1678
- if (!summary) {
1679
- return {
1680
- state: input.state,
1681
- ok: false,
1682
- resultText: "absorb failed: summary is empty \u2014 provide the distilled key info of the tool result."
1683
- };
3236
+ function selectLogLines(all) {
3237
+ const errors = [];
3238
+ const fails = [];
3239
+ const warnings = [];
3240
+ const summaries = [];
3241
+ const stacks = [];
3242
+ let current = [];
3243
+ for (const l of all) {
3244
+ if (l.level === "error") errors.push(l);
3245
+ else if (l.level === "fail") fails.push(l);
3246
+ else if (l.level === "warn") warnings.push(l);
3247
+ if (l.isStack) current.push(l);
3248
+ else if (current.length > 0) {
3249
+ stacks.push(current);
3250
+ current = [];
3251
+ }
3252
+ if (l.isSummary) summaries.push(l);
3253
+ }
3254
+ if (current.length > 0) stacks.push(current);
3255
+ const selected = /* @__PURE__ */ new Map();
3256
+ for (const l of selectWithFirstLast(errors, LOG_MAX_ERRORS))
3257
+ selected.set(l.i, l);
3258
+ for (const l of selectWithFirstLast(fails, LOG_MAX_ERRORS))
3259
+ selected.set(l.i, l);
3260
+ const seenWarn = /* @__PURE__ */ new Set();
3261
+ const dedupedWarnings = [];
3262
+ for (const w of warnings) {
3263
+ const key = normalizeForDedupe(w.content);
3264
+ if (!seenWarn.has(key)) {
3265
+ seenWarn.add(key);
3266
+ dedupedWarnings.push(w);
3267
+ }
3268
+ }
3269
+ for (const w of dedupedWarnings.slice(0, LOG_MAX_WARNINGS))
3270
+ selected.set(w.i, w);
3271
+ for (const stack of stacks.slice(0, LOG_MAX_STACK_TRACES)) {
3272
+ if (stack.length > LOG_STACK_MAX_LINES) {
3273
+ const collapsed = collapseTraceFrames(
3274
+ stack,
3275
+ LOG_TRACE_HEAD_FRAMES,
3276
+ LOG_TRACE_APP_FRAMES
3277
+ );
3278
+ for (const i of collapsed.dropped) selected.delete(i);
3279
+ for (const l of collapsed.kept.slice(0, LOG_STACK_MAX_LINES))
3280
+ selected.set(l.i, l);
3281
+ } else {
3282
+ for (const l of stack) selected.set(l.i, l);
3283
+ }
1684
3284
  }
1685
- const cfg = resolveAbsorbConfig(input.config);
1686
- const rawId = rawForRef(input.state.messageRefs, input.ref.trim());
1687
- if (!rawId) {
1688
- return {
1689
- state: input.state,
1690
- ok: false,
1691
- resultText: `absorb failed: ref ${input.ref} does not exist in this session (it may be hidden, already compressed, or stale).`
1692
- };
3285
+ for (const s of summaries) selected.set(s.i, s);
3286
+ for (const idx of Array.from(selected.keys())) {
3287
+ const lo = Math.max(0, idx - LOG_ERROR_CONTEXT);
3288
+ const hi = Math.min(all.length, idx + LOG_ERROR_CONTEXT + 1);
3289
+ for (let i = lo; i < hi; i++) {
3290
+ if (i !== idx && !selected.has(i)) selected.set(i, all[i]);
3291
+ }
1693
3292
  }
1694
- const existing = (input.state.absorbed ?? []).find(
1695
- (record2) => record2.resultMessageId === rawId
3293
+ let ordered = Array.from(selected.values());
3294
+ if (ordered.length > LOG_MAX_TOTAL_LINES) {
3295
+ ordered.sort((a, b) => b.score - a.score || a.i - b.i);
3296
+ ordered = ordered.slice(0, LOG_MAX_TOTAL_LINES);
3297
+ ordered.sort((a, b) => a.i - b.i);
3298
+ }
3299
+ return ordered;
3300
+ }
3301
+ function formatLogOutput(selected, all) {
3302
+ const count = (lv) => all.reduce((n, l) => l.level === lv ? n + 1 : n, 0);
3303
+ const output = selected.map((l) => l.content);
3304
+ const omitted = all.length - selected.length;
3305
+ if (omitted > 0) {
3306
+ const parts = [];
3307
+ const e = count("error");
3308
+ const f = count("fail");
3309
+ const w = count("warn");
3310
+ const inf = count("info");
3311
+ if (e > 0) parts.push(`${e} ERROR`);
3312
+ if (f > 0) parts.push(`${f} FAIL`);
3313
+ if (w > 0) parts.push(`${w} WARN`);
3314
+ if (inf > 0) parts.push(`${inf} INFO`);
3315
+ if (parts.length > 0)
3316
+ output.push(`[${omitted} lines omitted: ${parts.join(", ")}]`);
3317
+ }
3318
+ return output.join("\n");
3319
+ }
3320
+ var LOG_FORMAT_TABLE = [
3321
+ [
3322
+ "pytest",
3323
+ [
3324
+ "=== FAILURES",
3325
+ "=== ERRORS",
3326
+ "=== test session",
3327
+ "=== short test summary",
3328
+ "PASSED [",
3329
+ "FAILED [",
3330
+ "ERROR [",
3331
+ "SKIPPED [",
3332
+ "collected "
3333
+ ]
3334
+ ],
3335
+ ["npm", ["npm ERR!", "npm WARN", "npm info", "npm http"]],
3336
+ ["cargo", ["Compiling ", "Finished ", "Running ", "warning: ", "error[E"]],
3337
+ ["jest", ["PASS ", "FAIL ", "Test Suites:"]],
3338
+ ["make", ["make[", "make:", "gcc ", "g++ ", "clang "]]
3339
+ ];
3340
+ function detectLogFormat(lines) {
3341
+ const sample = lines.slice(0, 100);
3342
+ let best = "generic";
3343
+ let bestScore = 0;
3344
+ for (const [name, pats] of LOG_FORMAT_TABLE) {
3345
+ let score = 0;
3346
+ for (const line of sample) {
3347
+ if (pats.some((p) => line.includes(p))) score++;
3348
+ }
3349
+ if (score > bestScore) {
3350
+ bestScore = score;
3351
+ best = name;
3352
+ }
3353
+ }
3354
+ return best;
3355
+ }
3356
+ function crushLog(text) {
3357
+ const lines = text.split(/\r?\n/);
3358
+ if (lines.length < LOG_MIN_LINES) return null;
3359
+ let classified = 0;
3360
+ let summaries = 0;
3361
+ let traces = 0;
3362
+ for (const line of lines) {
3363
+ const lv = classifyLevel(line);
3364
+ if (lv !== "unknown") classified++;
3365
+ if (isLogSummaryLine(line)) summaries++;
3366
+ if (traceFlavorFor(line) !== null) traces++;
3367
+ }
3368
+ if (!(classified >= LOG_CLASSIFIED_GATE || traces >= 1 || summaries >= LOG_SUMMARY_GATE || detectLogFormat(lines) !== "generic"))
3369
+ return null;
3370
+ const parsed = parseLogLines(lines);
3371
+ const selected = selectLogLines(parsed);
3372
+ if (selected.length >= lines.length) return null;
3373
+ const out = formatLogOutput(selected, parsed);
3374
+ if (out.length >= text.length) return null;
3375
+ return out;
3376
+ }
3377
+ function errorLinesSurvive(input, output) {
3378
+ const kept = new Set(
3379
+ output.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0)
1696
3380
  );
1697
- if (existing) {
1698
- return {
1699
- state: input.state,
1700
- ok: true,
1701
- resultText: `already absorbed (${input.ref}) \u2014 no change.`
1702
- };
3381
+ for (const line of input.split(/\r?\n/)) {
3382
+ const level = classifyLevel(line);
3383
+ if (level !== "error" && level !== "fail") continue;
3384
+ const t = line.trim();
3385
+ if (t.length > 0 && !kept.has(t)) return false;
1703
3386
  }
1704
- const target = input.messages.find((m) => m.id === rawId);
1705
- if (!target) {
1706
- return {
1707
- state: input.state,
1708
- ok: false,
1709
- resultText: `absorb failed: ref ${input.ref} is not visible in this session (hidden or compressed).`
1710
- };
3387
+ return true;
3388
+ }
3389
+ var jsonFoldPlugin = {
3390
+ id: "json-fold",
3391
+ kinds: ["json"],
3392
+ run(text) {
3393
+ const out = crushJson(text);
3394
+ return out ? { text: out, lossy: false } : null;
1711
3395
  }
1712
- if (target.contentType !== "tool-result") {
1713
- return {
1714
- state: input.state,
1715
- ok: false,
1716
- resultText: `absorb failed: ref ${input.ref} is a ${target.contentType}, not a tool result.`
1717
- };
3396
+ };
3397
+ var codeTrimPlugin = {
3398
+ id: "code-trim",
3399
+ kinds: ["code"],
3400
+ run(text) {
3401
+ const out = crushCode(text);
3402
+ return out ? { text: out, lossy: true } : null;
1718
3403
  }
1719
- if (isAcpOrConfiguredTool(target.toolName, cfg)) {
1720
- return {
1721
- state: input.state,
1722
- ok: false,
1723
- resultText: `absorb failed: ${target.toolName} is an ACP-managed tool result \u2014 it is not absorbable.`
1724
- };
3404
+ };
3405
+ var logSelectPlugin = {
3406
+ id: "log-select",
3407
+ kinds: ["log"],
3408
+ run(text) {
3409
+ const out = crushLog(text);
3410
+ return out ? { text: out, lossy: true } : null;
1725
3411
  }
1726
- if (isMessageProtected(target, input.config) || isMessageLatestProtected(
1727
- target,
1728
- collectLatestProtected(input.messages, input.config)
1729
- )) {
1730
- return {
1731
- state: input.state,
1732
- ok: false,
1733
- resultText: `absorb failed: ${target.toolName} is a protected tool \u2014 its results must stay visible.`
1734
- };
3412
+ };
3413
+ var builtInPlugins = [
3414
+ jsonFoldPlugin,
3415
+ codeTrimPlugin,
3416
+ logSelectPlugin
3417
+ ];
3418
+ var crushRegistry = /* @__PURE__ */ new Map();
3419
+ for (const p of builtInPlugins) crushRegistry.set(p.id, p);
3420
+ function registerCrushPlugin(def) {
3421
+ crushRegistry.set(def.id, def);
3422
+ }
3423
+ function unregisterCrushPlugin(id) {
3424
+ crushRegistry.delete(id);
3425
+ }
3426
+ function listCrushPlugins() {
3427
+ return Array.from(crushRegistry.values()).map((p) => ({
3428
+ id: p.id,
3429
+ kinds: p.kinds
3430
+ }));
3431
+ }
3432
+ function resetCrushPlugins() {
3433
+ crushRegistry.clear();
3434
+ for (const p of builtInPlugins) crushRegistry.set(p.id, p);
3435
+ }
3436
+ function registeredPlugins() {
3437
+ return Array.from(crushRegistry.values());
3438
+ }
3439
+ function resolveCrushConfig(config) {
3440
+ return { ...DEFAULT_CRUSH_CONFIG, ...config.crush ?? {} };
3441
+ }
3442
+ function effectivePlugins(crush, meta) {
3443
+ const toolName = meta.toolName;
3444
+ return registeredPlugins().filter((p) => {
3445
+ const ov = crush.strategies?.[p.id];
3446
+ if (ov?.enabled === false) return false;
3447
+ if (toolName && ov?.excludeTools?.some((pat) => matchToolPattern(toolName, pat)))
3448
+ return false;
3449
+ return true;
3450
+ });
3451
+ }
3452
+ function evaluateToolResult(input) {
3453
+ const { absorb, crush, tokenCount, modelContextLimit, meta = {} } = input;
3454
+ const countTokens = input.countTokens ?? defaultCountTokens;
3455
+ const text = input.text;
3456
+ const rawTokens = countTokens(text);
3457
+ if (absorb.contextThresholdPct > 0 && modelContextLimit > 0 && tokenCount < absorb.contextThresholdPct * modelContextLimit) {
3458
+ return { kind: "skip", text, rawTokens };
1735
3459
  }
1736
- if (!target.toolCallId) {
1737
- return {
1738
- state: input.state,
1739
- ok: false,
1740
- resultText: `absorb failed: ref ${input.ref} has no tool-call id \u2014 cannot pair it for hiding.`
1741
- };
3460
+ if (rawTokens < absorb.minToolTokens) {
3461
+ return { kind: "skip", text, rawTokens };
1742
3462
  }
1743
- const call = input.messages.find(
1744
- (m) => m.contentType === "tool-call" && m.toolCallId === target.toolCallId
1745
- );
1746
- const tokens = countTokens(target.text ?? "");
1747
- const summaryTokens = countTokens(summary);
1748
- const record = {
1749
- toolCallId: target.toolCallId,
1750
- callMessageId: call?.id ?? "",
1751
- resultMessageId: target.id,
1752
- ...input.absorbCallId ? { absorbCallId: input.absorbCallId } : {},
1753
- summary,
1754
- tokensReclaimed: tokens,
1755
- createdAt: Date.now()
1756
- };
1757
- const state = {
1758
- ...input.state,
1759
- absorbed: [...input.state.absorbed ?? [], record],
1760
- stats: {
1761
- ...input.state.stats,
1762
- absorbedTokens: (input.state.stats.absorbedTokens ?? 0) + tokens
1763
- }
1764
- };
1765
- const bloat = summaryTokens >= tokens && tokens > 0 ? ` WARNING: your summary (~${formatTokenCount(summaryTokens)} tokens) is not smaller than the original (~${formatTokenCount(tokens)} tokens) \u2014 distill harder next time.` : "";
3463
+ const out = crushText(text, {
3464
+ minReduction: crush.minReduction,
3465
+ countTokens,
3466
+ meta,
3467
+ plugins: effectivePlugins(crush, meta)
3468
+ });
3469
+ if (!out) return { kind: "distill", text, rawTokens };
3470
+ const newTokens = countTokens(out.text);
1766
3471
  return {
1767
- state,
1768
- ok: true,
1769
- resultText: `absorbed ${input.ref} (~${formatTokenCount(tokens)} tokens \u2192 summary ~${formatTokenCount(summaryTokens)}). The original tool output is now hidden; your summary is the durable record.${bloat}`
3472
+ kind: newTokens < absorb.minToolTokens ? "crushed" : "distill",
3473
+ text: out.text,
3474
+ rawTokens,
3475
+ newTokens,
3476
+ reduction: (rawTokens - newTokens) / rawTokens,
3477
+ strategy: out.strategy,
3478
+ lossy: out.lossy
1770
3479
  };
1771
3480
  }
3481
+ function applyCrushToMessages(messages, config, tokenCount, countTokens) {
3482
+ const absorb = resolveAbsorbConfig(config);
3483
+ const crush = resolveCrushConfig(config);
3484
+ let crushedCount = 0;
3485
+ let distilledCount = 0;
3486
+ let changed = false;
3487
+ const out = [];
3488
+ for (const msg of messages) {
3489
+ if (!isAbsorbCandidate(msg, config)) {
3490
+ out.push(msg);
3491
+ continue;
3492
+ }
3493
+ const text = msg.text ?? "";
3494
+ const markerAt = text.indexOf(ABSORB_PROMPT_MARKER);
3495
+ const payload = markerAt >= 0 ? text.slice(0, markerAt) : text;
3496
+ const ev = evaluateToolResult({
3497
+ text: payload,
3498
+ absorb,
3499
+ crush,
3500
+ tokenCount,
3501
+ modelContextLimit: config.modelContextLimit,
3502
+ meta: { toolName: msg.toolName },
3503
+ countTokens
3504
+ });
3505
+ if (ev.kind === "skip") {
3506
+ out.push(msg);
3507
+ continue;
3508
+ }
3509
+ changed = true;
3510
+ if (ev.kind === "crushed") crushedCount++;
3511
+ else distilledCount++;
3512
+ out.push({ ...msg, text: ev.text });
3513
+ }
3514
+ return { messages: changed ? out : messages, crushedCount, distilledCount };
3515
+ }
1772
3516
 
1773
3517
  // src/filter/registry.ts
1774
3518
  var registry = /* @__PURE__ */ new Map();
@@ -2403,18 +4147,30 @@ function mergeRangesToThreshold(ranges, minChars) {
2403
4147
  const result = [];
2404
4148
  let batch = [];
2405
4149
  let batchChars = 0;
2406
- for (const r of ranges) {
2407
- batch.push(r);
2408
- batchChars += rangeChars(r);
2409
- if (batchChars >= minChars) {
2410
- result.push(mergeBatch(batch));
4150
+ const closeBatch = () => {
4151
+ if (batch.length > 0) {
4152
+ if (batchChars >= minChars) result.push(mergeBatch(batch));
2411
4153
  batch = [];
2412
4154
  batchChars = 0;
2413
4155
  }
4156
+ };
4157
+ for (const r of ranges) {
4158
+ const prev = batch[batch.length - 1];
4159
+ if (prev && prev.endIndex !== void 0 && r.startIndex !== void 0 && r.startIndex > prev.endIndex + 1) {
4160
+ closeBatch();
4161
+ }
4162
+ batch.push(r);
4163
+ batchChars += rangeChars(r);
4164
+ if (batchChars >= minChars) closeBatch();
2414
4165
  }
2415
4166
  if (batch.length > 0 && result.length > 0) {
2416
4167
  const prev = result[result.length - 1];
2417
- result[result.length - 1] = mergeBatch([prev, ...batch]);
4168
+ const contiguous = prev.endIndex === void 0 || batch[0].startIndex === void 0 || batch[0].startIndex <= prev.endIndex + 1;
4169
+ if (contiguous) {
4170
+ result[result.length - 1] = mergeBatch([prev, ...batch]);
4171
+ } else if (batchChars >= minChars) {
4172
+ result.push(mergeBatch(batch));
4173
+ }
2418
4174
  }
2419
4175
  return result;
2420
4176
  }
@@ -2672,10 +4428,12 @@ function createCore(ports = {}) {
2672
4428
  `[acp-kernel] Config validation warnings: ${configErrors.join("; ")}. Thresholds may not fire correctly.`
2673
4429
  );
2674
4430
  }
4431
+ const contentStore = input.contentStore ?? createContentStore();
2675
4432
  const ctx = {
2676
4433
  config: input.config,
2677
4434
  tokenCount: input.tokenCount,
2678
- countTokens
4435
+ countTokens,
4436
+ contentStore
2679
4437
  };
2680
4438
  const initial = {
2681
4439
  messages: input.messages,
@@ -2685,14 +4443,19 @@ function createCore(ports = {}) {
2685
4443
  const strategy = input.renderTags ?? "all";
2686
4444
  const nodes = buildNodes(strategy);
2687
4445
  const result = runPipeline(nodes, initial, ctx);
4446
+ const ccrEffect = result.effects.ccr;
2688
4447
  return {
2689
4448
  messages: result.messages,
2690
4449
  state: result.state,
2691
4450
  nudge: result.effects.nudge,
2692
4451
  terminalEscape: result.effects.terminalEscape,
2693
- truncationSkipped: result.effects.truncationSkipped
4452
+ truncationSkipped: result.effects.truncationSkipped,
4453
+ contentStore: ccrEffect?.store ?? contentStore
2694
4454
  };
2695
4455
  }
4456
+ function retrieve(store, ref) {
4457
+ return applyRetrieve({ store, ref });
4458
+ }
2696
4459
  function decompress(blockId, state) {
2697
4460
  return blockById(state, blockId);
2698
4461
  }
@@ -2712,7 +4475,12 @@ function createCore(ports = {}) {
2712
4475
  activeBlocks: active.length,
2713
4476
  totalBlocks: state.blocks.length,
2714
4477
  tokensCompressed: state.stats.tokensCompressed,
2715
- breakdown: { active: active.length, total: state.blocks.length }
4478
+ breakdown: {
4479
+ active: active.length,
4480
+ total: state.blocks.length,
4481
+ storedMessages: state.stats.storedCount ?? 0,
4482
+ retrievals: state.stats.retrievalCount ?? 0
4483
+ }
2716
4484
  };
2717
4485
  }
2718
4486
  function defaultNodes() {
@@ -2723,7 +4491,9 @@ function createCore(ports = {}) {
2723
4491
  assignRefsNode,
2724
4492
  syncBlocksNode,
2725
4493
  pruneNode,
4494
+ ccrStoreNode,
2726
4495
  absorbHideNode,
4496
+ crushNode,
2727
4497
  absorbPromptNode,
2728
4498
  filterNode,
2729
4499
  hideCompressCallsNode,
@@ -2736,6 +4506,7 @@ function createCore(ports = {}) {
2736
4506
  }
2737
4507
  return {
2738
4508
  processTurn,
4509
+ retrieve,
2739
4510
  applyCompression,
2740
4511
  defaultNodes,
2741
4512
  decompress,
@@ -2752,7 +4523,9 @@ var assignRefsNode = {
2752
4523
  const refResult = assignRefs(io.messages, {
2753
4524
  existing: io.state.messageRefs,
2754
4525
  nextIndex: highestUsedIndex(io.state.messageRefs) + 1,
2755
- isProtected: protectedFn
4526
+ isProtected: protectedFn,
4527
+ // Ephemeral retrieval injections never consume a ref slot.
4528
+ shouldSkip: (m) => m.id.startsWith(RETRIEVED_ID_PREFIX)
2756
4529
  });
2757
4530
  return { ...io, state: { ...io.state, messageRefs: refResult.map } };
2758
4531
  }
@@ -2796,6 +4569,27 @@ var absorbPromptNode = {
2796
4569
  };
2797
4570
  }
2798
4571
  };
4572
+ var crushNode = {
4573
+ name: "crush",
4574
+ enabled: (_io, ctx) => ctx.config.crush?.enabled === true && ctx.config.absorb?.enabled === true,
4575
+ run(io, ctx) {
4576
+ const applied = applyCrushToMessages(
4577
+ io.messages,
4578
+ ctx.config,
4579
+ ctx.tokenCount,
4580
+ ctx.countTokens
4581
+ );
4582
+ return {
4583
+ ...io,
4584
+ messages: applied.messages,
4585
+ effects: {
4586
+ ...io.effects,
4587
+ crushCount: applied.crushedCount,
4588
+ crushDistilledCount: applied.distilledCount
4589
+ }
4590
+ };
4591
+ }
4592
+ };
2799
4593
  var filterNode = {
2800
4594
  name: "filter",
2801
4595
  enabled: (_io, ctx) => !!ctx.config.messageFilters?.enabled && listMessageFilters().length > 0,
@@ -4732,6 +6526,294 @@ function formatRulesList(rules) {
4732
6526
  return rules.map((rule, i) => `${i + 1}. [${rule.id}] ${rule.text}`).join("\n");
4733
6527
  }
4734
6528
 
6529
+ // src/image-compress.ts
6530
+ var DEFAULT_IMAGE_COMPRESSION_CONFIG = {
6531
+ enabled: false,
6532
+ minTokens: 512,
6533
+ maxDimension: 1280,
6534
+ quality: 80,
6535
+ format: "webp"
6536
+ };
6537
+ function resolveImageCompressionConfig(config) {
6538
+ return { ...DEFAULT_IMAGE_COMPRESSION_CONFIG, ...config.imageCompression ?? {} };
6539
+ }
6540
+ var PIXEL_IMAGE_FALLBACK_TOKENS = 16384;
6541
+ var TILE_EDGE = 512;
6542
+ var TILE_SHORT_SIDE = 768;
6543
+ var TILE_MAX_EDGE = 2048;
6544
+ function byteAt(b, off) {
6545
+ return b[off] ?? 0;
6546
+ }
6547
+ function u16be(b, off) {
6548
+ return byteAt(b, off) << 8 | byteAt(b, off + 1);
6549
+ }
6550
+ function u16le(b, off) {
6551
+ return byteAt(b, off) | byteAt(b, off + 1) << 8;
6552
+ }
6553
+ function u32be(b, off) {
6554
+ return (byteAt(b, off) << 24 | byteAt(b, off + 1) << 16 | byteAt(b, off + 2) << 8 | byteAt(b, off + 3)) >>> 0;
6555
+ }
6556
+ function i32le(b, off) {
6557
+ return byteAt(b, off) | byteAt(b, off + 1) << 8 | byteAt(b, off + 2) << 16 | byteAt(b, off + 3) << 24;
6558
+ }
6559
+ function parseImageDimensions(bytes) {
6560
+ const b = bytes;
6561
+ if (b.length < 10) return void 0;
6562
+ if (b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) {
6563
+ if (b.length < 24) return void 0;
6564
+ const w = u32be(b, 16);
6565
+ const h = u32be(b, 20);
6566
+ return w > 0 && h > 0 ? { width: w, height: h } : void 0;
6567
+ }
6568
+ if (b[0] === 71 && b[1] === 73 && b[2] === 70) {
6569
+ const w = u16le(b, 6);
6570
+ const h = u16le(b, 8);
6571
+ return w > 0 && h > 0 ? { width: w, height: h } : void 0;
6572
+ }
6573
+ if (b.length >= 26 && b[0] === 66 && b[1] === 77) {
6574
+ const w = i32le(b, 18);
6575
+ const h = Math.abs(i32le(b, 22));
6576
+ return w > 0 && h > 0 ? { width: w, height: h } : void 0;
6577
+ }
6578
+ if (b.length >= 20 && b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) {
6579
+ if (b[12] === 86 && b[13] === 80 && b[14] === 56 && b[15] === 88) {
6580
+ if (b.length < 30) return void 0;
6581
+ const w = byteAt(b, 24) | byteAt(b, 25) << 8 | byteAt(b, 26) << 16;
6582
+ const h = byteAt(b, 27) | byteAt(b, 28) << 8 | byteAt(b, 29) << 16;
6583
+ return w > 0 && h > 0 ? { width: w + 1, height: h + 1 } : void 0;
6584
+ }
6585
+ if (b[12] === 86 && b[13] === 80 && b[14] === 56 && b[15] === 76) {
6586
+ if (b.length < 25) return void 0;
6587
+ if (b[20] !== 47) return void 0;
6588
+ const w = ((byteAt(b, 22) & 63) << 8 | byteAt(b, 21)) + 1;
6589
+ const h = (byteAt(b, 24) & 15) << 10 | byteAt(b, 23) << 2 | (byteAt(b, 22) & 192) >> 6;
6590
+ return w > 0 && h > 0 ? { width: w, height: h + 1 } : void 0;
6591
+ }
6592
+ if (b[12] === 86 && b[13] === 80 && b[14] === 56 && b[15] === 32) {
6593
+ if (b.length < 30) return void 0;
6594
+ if (b[23] !== 157 || b[24] !== 1 || b[25] !== 42) return void 0;
6595
+ const w = u16le(b, 26) & 16383;
6596
+ const h = u16le(b, 28) & 16383;
6597
+ return w > 0 && h > 0 ? { width: w, height: h } : void 0;
6598
+ }
6599
+ return void 0;
6600
+ }
6601
+ if (b[0] === 255 && b[1] === 216) {
6602
+ let off = 2;
6603
+ while (off + 4 <= b.length) {
6604
+ if (b[off] !== 255) {
6605
+ off += 1;
6606
+ continue;
6607
+ }
6608
+ const marker = byteAt(b, off + 1);
6609
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 217) {
6610
+ off += 2;
6611
+ continue;
6612
+ }
6613
+ if (marker === 218) break;
6614
+ const segLen = u16be(b, off + 2);
6615
+ if (segLen < 2) break;
6616
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
6617
+ if (off + 9 > b.length) break;
6618
+ const h = u16be(b, off + 5);
6619
+ const w = u16be(b, off + 7);
6620
+ if (w > 0 && h > 0) return { width: w, height: h };
6621
+ break;
6622
+ }
6623
+ off += 2 + segLen;
6624
+ }
6625
+ }
6626
+ return void 0;
6627
+ }
6628
+ var HEADER_SCAN_CHARS = 64;
6629
+ var JPEG_SCAN_CHARS = 35e4;
6630
+ function parseImageDimensionsFromBase64(b64) {
6631
+ if (typeof b64 !== "string" || b64.length === 0) return void 0;
6632
+ const head = parseImageDimensions(Buffer.from(b64.slice(0, HEADER_SCAN_CHARS), "base64"));
6633
+ if (head) return head;
6634
+ if (b64.startsWith("/9j/") && b64.length > HEADER_SCAN_CHARS) {
6635
+ return parseImageDimensions(Buffer.from(b64.slice(0, JPEG_SCAN_CHARS), "base64"));
6636
+ }
6637
+ return void 0;
6638
+ }
6639
+ function pixelTileEstimate(width, height) {
6640
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
6641
+ return PIXEL_IMAGE_FALLBACK_TOKENS;
6642
+ }
6643
+ let sw = width;
6644
+ let sh = height;
6645
+ const shortSide = Math.min(sw, sh);
6646
+ if (shortSide > 0 && shortSide < TILE_SHORT_SIDE) {
6647
+ const s = TILE_SHORT_SIDE / shortSide;
6648
+ sw *= s;
6649
+ sh *= s;
6650
+ }
6651
+ const longSide = Math.max(sw, sh);
6652
+ if (longSide > TILE_MAX_EDGE) {
6653
+ const s = TILE_MAX_EDGE / longSide;
6654
+ sw *= s;
6655
+ sh *= s;
6656
+ }
6657
+ const tiles = Math.ceil(sw / TILE_EDGE) * Math.ceil(sh / TILE_EDGE);
6658
+ return 85 + 170 * tiles;
6659
+ }
6660
+ function estimateImageTokens(meta) {
6661
+ const billing = meta.billing ?? "bytes";
6662
+ if (billing === "bytes") return Math.ceil(meta.base64Length / 4);
6663
+ const w = meta.width;
6664
+ const h = meta.height;
6665
+ if (w && h) return pixelTileEstimate(w, h);
6666
+ if (meta.base64) {
6667
+ const dims = parseImageDimensionsFromBase64(meta.base64);
6668
+ if (dims) return pixelTileEstimate(dims.width, dims.height);
6669
+ }
6670
+ return PIXEL_IMAGE_FALLBACK_TOKENS;
6671
+ }
6672
+ function createHeuristicClassifier(options = {}) {
6673
+ const aspectRatioMin = options.aspectRatioMin ?? 1.4;
6674
+ const aspectRatioMax = options.aspectRatioMax ?? 2.6;
6675
+ const minShortSide = options.minShortSide ?? 720;
6676
+ return {
6677
+ name: "heuristic-v1",
6678
+ isScreenshotLike(meta) {
6679
+ const w = meta.width;
6680
+ const h = meta.height;
6681
+ if (!w || !h || w <= 0 || h <= 0) return false;
6682
+ const shortSide = Math.min(w, h);
6683
+ if (shortSide < minShortSide) return false;
6684
+ const ratio = Math.max(w, h) / shortSide;
6685
+ return ratio >= aspectRatioMin && ratio <= aspectRatioMax;
6686
+ }
6687
+ };
6688
+ }
6689
+ var DEFAULT_SCREENSHOT_CLASSIFIER = createHeuristicClassifier();
6690
+ function decideImageRoute(meta, config, classifier = DEFAULT_SCREENSHOT_CLASSIFIER) {
6691
+ const cfg = resolveImageCompressionConfig(config);
6692
+ const estimatedTokens = estimateImageTokens(meta);
6693
+ if (!cfg.enabled) return { action: "pass", reason: "disabled", estimatedTokens };
6694
+ if (!meta.mediaType.startsWith("image/")) {
6695
+ return { action: "pass", reason: "not-an-image", estimatedTokens };
6696
+ }
6697
+ if (estimatedTokens < cfg.minTokens) {
6698
+ return { action: "pass", reason: "below-min-tokens", estimatedTokens };
6699
+ }
6700
+ if (!classifier.isScreenshotLike(meta)) {
6701
+ return { action: "pass", reason: "not-screenshot-like", estimatedTokens };
6702
+ }
6703
+ return {
6704
+ action: "downsample",
6705
+ reason: "downsample",
6706
+ estimatedTokens,
6707
+ recipe: { maxDimension: cfg.maxDimension, quality: cfg.quality, format: cfg.format }
6708
+ };
6709
+ }
6710
+ var IMAGE_FULL_FAILURE_MARKER = "[image_full FAILED:";
6711
+ function buildImageFullSystemNote(count) {
6712
+ return `[Downscaled screenshots: ${count} image(s) were reduced before entering context. If you cannot read details (text, colors, alignment) in a reduced image, call ${IMAGE_FULL_TOOL_NAME} with that message's ref ("mNNNNN") to restore the original resolution.]`;
6713
+ }
6714
+ function parseImageFullInput(input, callId, onWarn) {
6715
+ let obj = null;
6716
+ if (typeof input === "string") {
6717
+ try {
6718
+ const parsed = JSON.parse(input);
6719
+ if (parsed && typeof parsed === "object") obj = parsed;
6720
+ } catch {
6721
+ obj = null;
6722
+ }
6723
+ } else if (input && typeof input === "object") {
6724
+ obj = input;
6725
+ }
6726
+ if (!obj) {
6727
+ onWarn?.(`[acp-image-full-input] rejected: not an object (${typeof input})`);
6728
+ return null;
6729
+ }
6730
+ let ref;
6731
+ for (const key of ["ref", "messageId", "of"]) {
6732
+ const value = obj[key];
6733
+ if (typeof value === "string") {
6734
+ ref = value;
6735
+ break;
6736
+ }
6737
+ }
6738
+ if (typeof ref !== "string") {
6739
+ onWarn?.(
6740
+ `[acp-image-full-input] rejected: need ref (string); keys: ${Object.keys(obj).join(",")}`
6741
+ );
6742
+ return null;
6743
+ }
6744
+ const trimmed = ref.trim();
6745
+ if (!trimmed) {
6746
+ onWarn?.("[acp-image-full-input] rejected: ref is empty");
6747
+ return null;
6748
+ }
6749
+ return { ref: trimmed, ...callId ? { callId } : {} };
6750
+ }
6751
+ function recordImageShrink(state, record) {
6752
+ const bytesSaved = Math.max(0, record.originalBytes - record.shrunkBytes);
6753
+ const tokensSaved = Math.max(0, record.tokensBefore - record.tokensAfter);
6754
+ return {
6755
+ ...state,
6756
+ imageShrinks: [...state.imageShrinks ?? [], record],
6757
+ stats: {
6758
+ ...state.stats,
6759
+ imagesShrunk: (state.stats.imagesShrunk ?? 0) + 1,
6760
+ imageBytesSaved: (state.stats.imageBytesSaved ?? 0) + bytesSaved,
6761
+ imageTokensSaved: (state.stats.imageTokensSaved ?? 0) + tokensSaved
6762
+ }
6763
+ };
6764
+ }
6765
+ function isImageFullRestored(state, ref) {
6766
+ return (state.imageFullRestored ?? []).includes(ref);
6767
+ }
6768
+ function imageShrinksForRef(state, ref) {
6769
+ return (state.imageShrinks ?? []).filter((r) => r.ref === ref);
6770
+ }
6771
+ function applyImageFull(input) {
6772
+ const cfg = resolveImageCompressionConfig(input.config);
6773
+ const ref = input.ref.trim();
6774
+ if (!cfg.enabled) {
6775
+ return {
6776
+ state: input.state,
6777
+ ok: false,
6778
+ resultText: `${IMAGE_FULL_FAILURE_MARKER} image compression is disabled in this session \u2014 no image was ever downscaled]`
6779
+ };
6780
+ }
6781
+ const rawId = rawForRef(input.state.messageRefs, ref);
6782
+ if (!rawId) {
6783
+ return {
6784
+ state: input.state,
6785
+ ok: false,
6786
+ resultText: `${IMAGE_FULL_FAILURE_MARKER} ref ${ref} does not exist in this session]`
6787
+ };
6788
+ }
6789
+ if (isImageFullRestored(input.state, ref)) {
6790
+ return {
6791
+ state: input.state,
6792
+ ok: true,
6793
+ resultText: `already restored (${ref}) \u2014 full resolution stays in effect for the rest of this session.`
6794
+ };
6795
+ }
6796
+ const shrinks = imageShrinksForRef(input.state, ref);
6797
+ if (shrinks.length === 0) {
6798
+ return {
6799
+ state: input.state,
6800
+ ok: false,
6801
+ resultText: `${IMAGE_FULL_FAILURE_MARKER} no downscaled image is recorded for ${ref} \u2014 it was passed through untouched or never carried an image]`
6802
+ };
6803
+ }
6804
+ return {
6805
+ state: {
6806
+ ...input.state,
6807
+ imageFullRestored: [...input.state.imageFullRestored ?? [], ref]
6808
+ },
6809
+ ok: true,
6810
+ resultText: `restored original-resolution image(s) for ${ref} (${shrinks.length} image${shrinks.length === 1 ? "" : "s"}); full resolution applies for the rest of this session.`
6811
+ };
6812
+ }
6813
+ function resetImageFullState(state) {
6814
+ return { ...state, imageFullRestored: [], imageShrinks: [] };
6815
+ }
6816
+
4735
6817
  // src/parse-compress-input.ts
4736
6818
  function parseCompressArgs(input, opts) {
4737
6819
  const callId = opts?.callId;
@@ -5260,7 +7342,8 @@ function rebuildCompressionState(state, messages, config, ports = {}) {
5260
7342
  const core = createCore({ countTokens: ports.countTokens ?? defaultCountTokens });
5261
7343
  const refResult = assignRefs(messages, {
5262
7344
  existing: state.messageRefs,
5263
- nextIndex: highestUsedIndex(state.messageRefs) + 1
7345
+ nextIndex: highestUsedIndex(state.messageRefs) + 1,
7346
+ shouldSkip: (m) => m.id.startsWith(RETRIEVED_ID_PREFIX)
5264
7347
  });
5265
7348
  let working = { ...state, messageRefs: refResult.map };
5266
7349
  const invocations = collectCompressInvocations(messages);
@@ -5672,10 +7755,27 @@ export {
5672
7755
  DECOMPRESS_TOOL_RESPONSES,
5673
7756
  DEFAULT_ABSORB_CONFIG,
5674
7757
  DEFAULT_ALGORITHM,
7758
+ DEFAULT_CCR_CONFIG,
7759
+ DEFAULT_CRUSH_CONFIG,
7760
+ DEFAULT_IMAGE_COMPRESSION_CONFIG,
5675
7761
  DEFAULT_ROLE_WEIGHTS,
5676
7762
  DEFAULT_RULE_LIMITS,
7763
+ DEFAULT_SCREENSHOT_CLASSIFIER,
5677
7764
  HOW_TO_COMPRESS_RULES,
7765
+ IMAGE_FULL_FAILURE_MARKER,
7766
+ IMAGE_FULL_TOOL,
7767
+ IMAGE_FULL_TOOL_DESCRIPTION,
7768
+ IMAGE_FULL_TOOL_NAME,
7769
+ IMAGE_FULL_TOOL_OPENAI,
7770
+ IMAGE_FULL_TOOL_RESPONSES,
5678
7771
  LEAN_HOW_TO_COMPRESS,
7772
+ PIXEL_IMAGE_FALLBACK_TOKENS,
7773
+ RETRIEVED_ID_PREFIX,
7774
+ RETRIEVE_TOOL,
7775
+ RETRIEVE_TOOL_DESCRIPTION,
7776
+ RETRIEVE_TOOL_NAME,
7777
+ RETRIEVE_TOOL_OPENAI,
7778
+ RETRIEVE_TOOL_RESPONSES,
5679
7779
  RULES_USAGE_PROMPT,
5680
7780
  RULE_TOOL_NAME,
5681
7781
  SEARCH_CONTEXT_TOOL,
@@ -5683,6 +7783,7 @@ export {
5683
7783
  SEARCH_CONTEXT_TOOL_NAME,
5684
7784
  SEARCH_CONTEXT_TOOL_OPENAI,
5685
7785
  SEARCH_CONTEXT_TOOL_RESPONSES,
7786
+ STORED_PLACEHOLDER_MARKER,
5686
7787
  SUMMARY_HEADER,
5687
7788
  TIER2_DISTILL_RULES,
5688
7789
  TIER3_CONDENSE_RULES,
@@ -5697,7 +7798,10 @@ export {
5697
7798
  appendAbsorbPrompts,
5698
7799
  applyAbsorb,
5699
7800
  applyAcpToolOverrides,
7801
+ applyCrushToMessages,
7802
+ applyImageFull,
5700
7803
  applyMessageFilters,
7804
+ applyRetrieve,
5701
7805
  applySectionOverrides,
5702
7806
  assignRefs,
5703
7807
  blockById,
@@ -5709,26 +7813,38 @@ export {
5709
7813
  buildCompressHybridSystemPrompt,
5710
7814
  buildCompressSystemPrompt,
5711
7815
  buildCompressTextSystemPrompt,
7816
+ buildImageFullSystemNote,
5712
7817
  buildRecap,
5713
7818
  buildRestoredContentPreview,
7819
+ buildRetrievalInjection,
5714
7820
  buildStatusReport,
7821
+ buildStoredPlaceholder,
5715
7822
  builtinSource,
7823
+ ccrStoreNode,
7824
+ classifyCrushText,
7825
+ classifyKind,
5716
7826
  clearDocFeatures,
5717
7827
  clearMessageFilters,
5718
7828
  clearRules,
5719
7829
  cloneWithDescriptions,
7830
+ codeTrimPlugin,
5720
7831
  collectBlockContent,
5721
7832
  collectLatestProtected,
5722
7833
  computeFoldEconomics,
7834
+ contentStoreStats,
5723
7835
  countMessageTokens,
5724
7836
  coveredMessageIds,
5725
7837
  createBpeTokenizer,
7838
+ createContentStore,
5726
7839
  createCore,
5727
7840
  createDirPackSource,
7841
+ createHeuristicClassifier,
5728
7842
  createInitialState,
5729
7843
  createPackResolver,
5730
7844
  createRenderRefsNode,
7845
+ crushText,
5731
7846
  deactivateBlock,
7847
+ decideImageRoute,
5732
7848
  decomposeSample,
5733
7849
  defaultConfig,
5734
7850
  defaultCountTokens,
@@ -5738,7 +7854,10 @@ export {
5738
7854
  docCacheInfo,
5739
7855
  docFeatures,
5740
7856
  emptyRefMap,
7857
+ estimateImageTokens,
5741
7858
  estimateTokensFast,
7859
+ evaluateToolResult,
7860
+ extractCommand,
5742
7861
  findActiveAncestor,
5743
7862
  findBlocksOverlappingMessages,
5744
7863
  formatCacheReport,
@@ -5748,35 +7867,52 @@ export {
5748
7867
  formatRulesList,
5749
7868
  getMessageFilter,
5750
7869
  getSearchAlgorithm,
7870
+ hasStoredRef,
7871
+ hashContent,
5751
7872
  hideAbsorbedMessages,
5752
7873
  hideConsumedCompressCalls,
5753
7874
  highestActiveTier,
5754
7875
  highestUsedIndex,
7876
+ imageShrinksForRef,
5755
7877
  indexToRef,
5756
7878
  isAbsorbCandidate,
7879
+ isImageFullRestored,
5757
7880
  isMessageLatestProtected,
5758
7881
  isMessageProtected,
5759
7882
  isRenderedSummaryMessage,
7883
+ isRetrievedMessage,
7884
+ isStoredPlaceholderText,
5760
7885
  isSummaryMessageId,
5761
7886
  isValidPackName,
7887
+ jsonFoldPlugin,
5762
7888
  leanPack,
7889
+ listCrushPlugins,
5763
7890
  listMessageFilters,
5764
7891
  listRules,
5765
7892
  listSearchAlgorithms,
7893
+ logSelectPlugin,
5766
7894
  makeIO,
5767
7895
  matchSession,
5768
7896
  matchToolPattern,
5769
7897
  messageDocs,
7898
+ normalizeHead,
7899
+ noteRetrieval,
5770
7900
  parseAbsorbInput,
5771
7901
  parseBlockIdArg,
5772
7902
  parseBoundary,
5773
7903
  parseCompressArgs,
5774
7904
  parseCompressInput,
7905
+ parseImageDimensions,
7906
+ parseImageDimensionsFromBase64,
7907
+ parseImageFullInput,
7908
+ pixelTileEstimate,
5775
7909
  prune,
5776
7910
  rawForRef,
5777
7911
  rebuildCompressionState,
7912
+ recordImageShrink,
5778
7913
  refForRaw,
5779
7914
  refToIndex,
7915
+ registerCrushPlugin,
5780
7916
  registerMessageFilter,
5781
7917
  registerSearchAlgorithm,
5782
7918
  removeRule,
@@ -5785,21 +7921,32 @@ export {
5785
7921
  renderNudgeText,
5786
7922
  renderRefsNode,
5787
7923
  renderVisibleRefs,
7924
+ resetCrushPlugins,
7925
+ resetImageFullState,
5788
7926
  resolveAbsorbConfig,
5789
7927
  resolveBlockSpan,
5790
7928
  resolveBoundaries,
7929
+ resolveCcrConfig,
7930
+ resolveCrushConfig,
7931
+ resolveImageCompressionConfig,
5791
7932
  resolvePrompts,
5792
7933
  resolveRuleLimits,
5793
7934
  resolveTransformChannel,
7935
+ retrieveByRef,
7936
+ retrievedMessageId,
5794
7937
  runPipeline,
5795
7938
  sanitizePackSurface,
5796
7939
  searchBlocks,
5797
7940
  searchBlocksAsync,
5798
7941
  setDocCacheCap,
7942
+ storeCoveredOriginals,
7943
+ storeLargeResults,
7944
+ storeOriginal,
5799
7945
  summarizeFoldEconomics,
5800
7946
  summaryMessageId,
5801
7947
  syncBlocks,
5802
7948
  truncateLargeToolOutputs,
7949
+ unregisterCrushPlugin,
5803
7950
  validateConfig,
5804
7951
  viableRanges,
5805
7952
  visibleBlockAnchor