acp-kernel 0.0.73-pr.303.169 → 0.0.73-pr.305.170

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.
@@ -1 +1 @@
1
- {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkC,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgBhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUvD,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AA2DD,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CAyU7D;AA4iCD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkC,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgBhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUvD,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAEhB,MAAM,EAEN,WAAW,EAGX,iBAAiB,EAEjB,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,KAAK;IACpB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACxD,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAAC;IACvE,YAAY,IAAI,YAAY,EAAE,CAAC;IAC/B,UAAU,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,gBAAgB,GACtB,gBAAgB,GAAG,SAAS,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,CACJ,KAAK,EAAE,gBAAgB,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,EAAE,CAAC;IACJ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnC;AA2DD,wBAAgB,UAAU,CAAC,KAAK,GAAE,KAAU,GAAG,eAAe,CA+U7D;AA0lCD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,EACzB,SAAS,GAAE,OAAO,CAAC,MAAM,CAAM,GAC9B,MAAM,CA+CR;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAuDvD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,wBAAgB,aAAa,CAC3B,iBAAiB,EAAE,MAAM,EACzB,SAAS,GAAE,OAAO,CAAC,MAAM,CAAM,GAC9B,MAAM,CA+CR;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA8DvD"}
@@ -1 +1 @@
1
- {"version":3,"file":"hide-consumed.d.ts","sourceRoot":"","sources":["../src/hide-consumed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAYhE,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAClB;AA8FD,wBAAgB,yBAAyB,CACrC,KAAK,EAAE,gBAAgB,EACvB,QAAQ,EAAE,WAAW,EAAE,GACxB,kBAAkB,CAwFpB"}
1
+ {"version":3,"file":"hide-consumed.d.ts","sourceRoot":"","sources":["../src/hide-consumed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAYhE,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAClB;AA8FD,wBAAgB,yBAAyB,CACrC,KAAK,EAAE,gBAAgB,EACvB,QAAQ,EAAE,WAAW,EAAE,GACxB,kBAAkB,CAwFpB"}
package/dist/index.js CHANGED
@@ -296,6 +296,7 @@ function syncBlocks(messages, state) {
296
296
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
297
297
  stats: { ...state.stats },
298
298
  absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
299
+ terminalStreak: state.terminalStreak,
299
300
  nextBlockId: state.nextBlockId,
300
301
  nextRunId: state.nextRunId
301
302
  };
@@ -353,7 +354,7 @@ function defaultConfig(modelContextLimit, overrides = {}) {
353
354
  tier2GrowthMultiplier: 1.5
354
355
  },
355
356
  promotionThreshold: 5,
356
- truncate: { threshold: 0.95 },
357
+ truncate: { threshold: 0.95, terminalEscapeAfter: 3 },
357
358
  compress: {
358
359
  minCompressRange: 5e3,
359
360
  maxSummaryLength: 2e4,
@@ -405,6 +406,9 @@ function validateConfig(config) {
405
406
  if (config.truncate.threshold <= 0 || config.truncate.threshold > 1) {
406
407
  errors.push("truncate.threshold must be in (0, 1]");
407
408
  }
409
+ if (config.truncate.terminalEscapeAfter !== void 0 && (!Number.isInteger(config.truncate.terminalEscapeAfter) || config.truncate.terminalEscapeAfter < 0)) {
410
+ errors.push("truncate.terminalEscapeAfter must be an integer >= 0");
411
+ }
408
412
  for (const tier of [config.tiers.tier2Trigger, config.tiers.tier3Trigger]) {
409
413
  if (tier < 1) errors.push("tier triggers must be >= 1");
410
414
  }
@@ -654,47 +658,89 @@ var DEFAULTS = {
654
658
  };
655
659
  function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, options = {}) {
656
660
  const opts = { ...DEFAULTS, ...options };
657
- if (config.modelContextLimit <= 0) return { messages, truncatedCount: 0, savedTokens: 0 };
658
- const threshold = config.truncate.threshold * config.modelContextLimit;
659
- if (tokenCount < threshold) return { messages, truncatedCount: 0, savedTokens: 0 };
660
- const protectedIndex = messages.length - opts.protectRecentMessages;
661
- const candidates = [];
662
- for (let index = 0; index < messages.length; index++) {
663
- if (index >= protectedIndex) break;
664
- const message = messages[index];
665
- if (message.contentType !== "tool-result") continue;
666
- const text = message.text ?? "";
667
- if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
668
- const tokens = countTokens(text);
669
- if (tokens < opts.minOutputTokens) continue;
670
- candidates.push({ index, tokens });
661
+ const limit = config.modelContextLimit;
662
+ if (limit <= 0 || tokenCount < config.truncate.threshold * limit) {
663
+ return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound: 0 };
671
664
  }
672
- if (candidates.length === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
673
- candidates.sort((left, right) => right.tokens - left.tokens);
674
- const targetTokens = threshold * 0.9;
675
- let savedTokens = 0;
676
- const edits = /* @__PURE__ */ new Map();
665
+ const protectedIndex = messages.length - opts.protectRecentMessages;
666
+ const findCandidates = (predicate) => {
667
+ const found = [];
668
+ for (let i = 0; i < protectedIndex; i++) {
669
+ const message = messages[i];
670
+ if (!predicate(message)) continue;
671
+ const text = message.text ?? "";
672
+ if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
673
+ if (countTokens(text) < opts.minOutputTokens) continue;
674
+ found.push(message);
675
+ }
676
+ return found.sort(
677
+ (a, b) => countTokens(b.text ?? "") - countTokens(a.text ?? "")
678
+ );
679
+ };
680
+ const toolResults = findCandidates((m) => m.contentType === "tool-result");
681
+ const textMessages = options.includeTextMessages ? findCandidates(
682
+ (m) => m.contentType === "text" && (m.role === "user" || m.role === "assistant") && !isRenderedSummaryMessage(m) && !m.text?.startsWith(SUMMARY_HEADER)
683
+ ) : [];
684
+ const candidatesFound = toolResults.length + textMessages.length;
677
685
  let truncatedCount = 0;
678
- for (const candidate of candidates) {
679
- if (tokenCount - savedTokens <= targetTokens) break;
680
- const original = messages[candidate.index].text ?? "";
681
- if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) continue;
682
- const prefix = original.slice(0, opts.keepPrefixChars);
683
- const suffix = original.slice(-opts.keepSuffixChars);
684
- const replacement = prefix + `
686
+ let savedTokens = 0;
687
+ let remaining = tokenCount;
688
+ const targetTokens = config.truncate.threshold * limit * 0.9;
689
+ const replacements = /* @__PURE__ */ new Map();
690
+ const applyTo = (candidates) => {
691
+ for (const candidate of candidates) {
692
+ if (remaining <= targetTokens) break;
693
+ const original = candidate.text ?? "";
694
+ const tokens = countTokens(original);
695
+ if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) {
696
+ continue;
697
+ }
698
+ const prefix = original.slice(0, opts.keepPrefixChars);
699
+ const suffix = original.slice(-opts.keepSuffixChars);
700
+ const replacement = `${prefix}
685
701
 
686
- ...${TRUNCATION_MARKER} \u2014 original ~${candidate.tokens} tokens]...
702
+ ...${TRUNCATION_MARKER} \u2014 original ~${tokens} tokens]...
687
703
 
688
- ` + suffix;
689
- edits.set(candidate.index, replacement);
690
- savedTokens += candidate.tokens - countTokens(replacement);
691
- truncatedCount++;
692
- }
693
- if (truncatedCount === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
694
- const updated = messages.map(
695
- (message, index) => edits.has(index) ? { ...message, text: edits.get(index) } : message
696
- );
697
- return { messages: updated, truncatedCount, savedTokens };
704
+ ${suffix}`;
705
+ replacements.set(candidate.id, replacement);
706
+ truncatedCount++;
707
+ remaining -= tokens - countTokens(replacement);
708
+ savedTokens += tokens - countTokens(replacement);
709
+ }
710
+ };
711
+ applyTo(toolResults);
712
+ applyTo(textMessages);
713
+ if (replacements.size === 0) {
714
+ return { messages, truncatedCount: 0, savedTokens: 0, candidatesFound };
715
+ }
716
+ return {
717
+ messages: messages.map(
718
+ (m) => replacements.has(m.id) ? { ...m, text: replacements.get(m.id) } : m
719
+ ),
720
+ truncatedCount,
721
+ savedTokens,
722
+ candidatesFound
723
+ };
724
+ }
725
+
726
+ // src/truncate.ts
727
+ function isHighSurrogate(c) {
728
+ return c >= 55296 && c <= 56319;
729
+ }
730
+ function isLowSurrogate(c) {
731
+ return c >= 56320 && c <= 57343;
732
+ }
733
+ function clampPrefix(text, maxUnits) {
734
+ const cut = Math.min(maxUnits, text.length);
735
+ if (cut > 0 && isHighSurrogate(text.charCodeAt(cut - 1))) return text.slice(0, cut - 1);
736
+ return text.slice(0, cut);
737
+ }
738
+ function clampWindow(text, start, end) {
739
+ let s = Math.max(0, Math.min(start, text.length));
740
+ let e = Math.min(text.length, Math.max(s, end));
741
+ if (s > 0 && isLowSurrogate(text.charCodeAt(s))) s += 1;
742
+ if (e > s && isHighSurrogate(text.charCodeAt(e - 1))) e -= 1;
743
+ return text.slice(s, Math.max(s, e));
698
744
  }
699
745
 
700
746
  // src/hide-consumed.ts
@@ -749,7 +795,7 @@ function compactEntry(entry) {
749
795
  if (!entry || typeof entry !== "object") return entry;
750
796
  const e = entry;
751
797
  if (typeof e.summary !== "string" || e.summary.length <= SUMMARY_STUB_CHARS) return entry;
752
- return { ...e, summary: `${e.summary.slice(0, SUMMARY_STUB_CHARS - 1)}\u2026` };
798
+ return { ...e, summary: `${clampPrefix(e.summary, SUMMARY_STUB_CHARS - 1)}\u2026` };
753
799
  }
754
800
  function serializeCompacted(obj, content, contentWasString) {
755
801
  let changed = false;
@@ -2323,6 +2369,7 @@ function createCore(ports = {}) {
2323
2369
  state.nudge.lastPerMessageNudgeTokens = 0;
2324
2370
  state.nudge.lastNudgeShownTokens = 0;
2325
2371
  state.nudge.lastShownByTier = {};
2372
+ state.terminalStreak = 0;
2326
2373
  }
2327
2374
  return {
2328
2375
  state,
@@ -2352,7 +2399,9 @@ function createCore(ports = {}) {
2352
2399
  return {
2353
2400
  messages: result.messages,
2354
2401
  state: result.state,
2355
- nudge: result.effects.nudge
2402
+ nudge: result.effects.nudge,
2403
+ terminalEscape: result.effects.terminalEscape,
2404
+ truncationSkipped: result.effects.truncationSkipped
2356
2405
  };
2357
2406
  }
2358
2407
  function decompress(blockId, state) {
@@ -2545,18 +2594,49 @@ var emergencyTruncateNode = {
2545
2594
  name: "emergency-truncate",
2546
2595
  run(io, ctx) {
2547
2596
  const usage = ctx.config.modelContextLimit > 0 ? ctx.tokenCount / ctx.config.modelContextLimit : 0;
2548
- if (usage < ctx.config.truncate.threshold) return io;
2597
+ const prevStreak = io.state.terminalStreak ?? 0;
2598
+ if (usage < ctx.config.truncate.threshold) {
2599
+ return prevStreak > 0 ? { ...io, state: { ...io.state, terminalStreak: 0 } } : io;
2600
+ }
2549
2601
  const trunc = truncateLargeToolOutputs(
2550
2602
  io.messages,
2551
2603
  ctx.tokenCount,
2552
2604
  ctx.config,
2553
2605
  ctx.countTokens,
2554
- { protectRecentMessages: ctx.config.preserveRecentMessages }
2606
+ {
2607
+ protectRecentMessages: ctx.config.preserveRecentMessages,
2608
+ includeTextMessages: true
2609
+ }
2555
2610
  );
2611
+ const nudge = io.effects.nudge;
2612
+ const minBenefit = nudge?.breakdown.minPressureBenefit ?? 0;
2613
+ const maxPending = nudge?.breakdown.maxPending ?? 0;
2614
+ const noViableCompression = nudge !== void 0 && (minBenefit > 0 ? maxPending < minBenefit : maxPending <= 0);
2615
+ const stuck = noViableCompression && trunc.savedTokens <= 0;
2616
+ const streak = stuck ? prevStreak + 1 : 0;
2617
+ const escapeAfter = ctx.config.truncate.terminalEscapeAfter ?? 3;
2618
+ const triggered = stuck && escapeAfter > 0 && streak >= escapeAfter;
2619
+ const effects = {
2620
+ ...io.effects,
2621
+ truncatedCount: trunc.truncatedCount
2622
+ };
2623
+ if (trunc.savedTokens <= 0) {
2624
+ effects.truncationSkipped = trunc.candidatesFound === 0 ? `emergency-truncate ran at ${Math.round(usage * 100)}% usage but found no truncatable content (no oversized tool-result or text message outside the last ${ctx.config.preserveRecentMessages} messages)` : `emergency-truncate found ${trunc.candidatesFound} candidate(s) at ${Math.round(usage * 100)}% usage but none were large enough to save tokens`;
2625
+ }
2626
+ if (triggered) {
2627
+ effects.terminalEscape = {
2628
+ message: `Usage at ${Math.round(usage * 100)}% (${ctx.tokenCount}/${ctx.config.modelContextLimit} tokens) persists with nothing compressible above the benefit floor and no truncatable content: compression cannot reduce this context below the limit. Start a new session or use native compaction.`,
2629
+ usage,
2630
+ tokenCount: ctx.tokenCount,
2631
+ modelContextLimit: ctx.config.modelContextLimit,
2632
+ stuckEvents: streak
2633
+ };
2634
+ }
2556
2635
  return {
2557
2636
  ...io,
2558
2637
  messages: trunc.messages,
2559
- effects: { ...io.effects, truncatedCount: trunc.truncatedCount }
2638
+ state: { ...io.state, terminalStreak: streak },
2639
+ effects
2560
2640
  };
2561
2641
  }
2562
2642
  };
@@ -2649,7 +2729,8 @@ function applySingleRange(input) {
2649
2729
  for (const m of input.messages) {
2650
2730
  if (!m.id) continue;
2651
2731
  if (m.contentType === "reasoning") reasoningIds.add(m.id);
2652
- if (m.role === "assistant" && m.contentType === "tool-call") callIds.add(m.id);
2732
+ if (m.role === "assistant" && m.contentType === "tool-call")
2733
+ callIds.add(m.id);
2653
2734
  }
2654
2735
  const withdrawIds = /* @__PURE__ */ new Set();
2655
2736
  let splitTurnCount = 0;
@@ -2920,6 +3001,7 @@ function decideNudge(input) {
2920
3001
  const t1Eff = tiers[1]?.pending ?? 0;
2921
3002
  const t2Pen = tiers[2]?.pending ?? 0;
2922
3003
  const t3Pen = tiers[3]?.pending ?? 0;
3004
+ const maxPending = Math.max(0, t1Eff, t2Pen, t3Pen);
2923
3005
  const firstSightMassReady = state.nudge.lastNudgeShownTokens === 0 && baseline === 0 && usage >= config.nudge.minContextLimitPct && Math.max(t1Eff, t2Pen, t3Pen) >= nudgeGrowthTokens;
2924
3006
  const growthReady = firstSightMassReady || growthSinceReference >= growthFloor;
2925
3007
  const t2Count = tiers[2]?.targetBlocks.length ?? 0;
@@ -2992,10 +3074,6 @@ function decideNudge(input) {
2992
3074
  (t) => ((tiers[t]?.pending ?? 0) >= nudgeGrowthTokens || countReady(t)) && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor
2993
3075
  ).map((t) => `T${t} (cadence)`);
2994
3076
  const blockedHint = blocked.length > 0 ? `, blocked: ${blocked.join(", ")}` : "";
2995
- const maxPending = Math.max(
2996
- 0,
2997
- ...Object.values(tiers).map((t) => t.pending)
2998
- );
2999
3077
  const pendingShort = maxPending < nudgeGrowthTokens;
3000
3078
  const growthShort = growthSinceReference < growthFloor;
3001
3079
  const parts = [];
@@ -3039,7 +3117,8 @@ function decideNudge(input) {
3039
3117
  minPressureBenefit,
3040
3118
  pendingT1: tiers[1].pending,
3041
3119
  pendingT2: tiers[2].pending,
3042
- pendingT3: tiers[3].pending
3120
+ pendingT3: tiers[3].pending,
3121
+ maxPending
3043
3122
  },
3044
3123
  contextBreakdown: ctxBreakdown
3045
3124
  };
@@ -3079,6 +3158,7 @@ function cloneState(state) {
3079
3158
  nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
3080
3159
  stats: { ...state.stats },
3081
3160
  absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
3161
+ terminalStreak: state.terminalStreak,
3082
3162
  nextBlockId: state.nextBlockId,
3083
3163
  nextRunId: state.nextRunId
3084
3164
  };
@@ -3765,7 +3845,7 @@ ${block.summary}
3765
3845
  const lines = [`Active compression blocks (${activeBlocks2.length}):`];
3766
3846
  for (const block of activeBlocks2) {
3767
3847
  const range = `${block.effectiveMessageIds.length} messages`;
3768
- const preview = block.summary.slice(0, 200);
3848
+ const preview = clampPrefix(block.summary, 200);
3769
3849
  lines.push(`
3770
3850
  ${block.blockId} | ${range} | "${block.topic ?? "(none)"}"`);
3771
3851
  lines.push(` ${preview}${block.summary.length > 200 ? "..." : ""}`);
@@ -4047,7 +4127,7 @@ function deriveTopicFromSummary(summary) {
4047
4127
  const source = heading !== null ? heading[1] : summary.split("\n").find((l) => l.trim().length > 0) ?? "";
4048
4128
  const text = source.trim().replace(/^[\u2022\-*]\s+/, "");
4049
4129
  if (text.length === 0) return void 0;
4050
- return text.length > 60 ? text.slice(0, 60).trimEnd() : text;
4130
+ return text.length > 60 ? clampPrefix(text, 60).trimEnd() : text;
4051
4131
  }
4052
4132
  function splitLineEntries(content) {
4053
4133
  const parts = content.split(/\n(?=\s*(?:["']\s*,\s*)?["']?\s*[mb]\d{1,7}\s*(?:[-\u2013\u2014\u2026~]|\.\.\.|to)\s*[mb]\d{1,7}\b)/i).map((p) => p.trim().replace(/^(?:["']\s*,?\s*)+/, "").replace(/["']\s*$/, "")).filter((p) => p.length > 0);
@@ -4631,7 +4711,7 @@ function messageDocs(msgs) {
4631
4711
  kind: "message",
4632
4712
  ref: m.ref,
4633
4713
  text: m.text,
4634
- title: `${m.role}: ${m.text.slice(0, 60)}`,
4714
+ title: `${m.role}: ${clampPrefix(m.text, 60)}`,
4635
4715
  role: m.role,
4636
4716
  blockId: m.blockId,
4637
4717
  tier: m.tier,
@@ -4696,7 +4776,7 @@ async function searchBlocksAsync(docs, query, options = {}) {
4696
4776
  function makePreview(text, query, len) {
4697
4777
  if (!text) return "";
4698
4778
  const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 1);
4699
- if (terms.length === 0) return text.slice(0, len);
4779
+ if (terms.length === 0) return clampPrefix(text, len);
4700
4780
  const lower = text.toLowerCase();
4701
4781
  let hitIdx = -1;
4702
4782
  for (const term of terms) {
@@ -4706,13 +4786,13 @@ function makePreview(text, query, len) {
4706
4786
  break;
4707
4787
  }
4708
4788
  }
4709
- if (hitIdx < 0) return text.slice(0, len);
4789
+ if (hitIdx < 0) return clampPrefix(text, len);
4710
4790
  const half = Math.max(0, Math.floor(len / 2) - 10);
4711
4791
  const start = Math.max(0, hitIdx - half);
4712
4792
  const end = Math.min(text.length, start + len);
4713
4793
  const prefix = start > 0 ? "\u2026" : "";
4714
4794
  const suffix = end < text.length ? "\u2026" : "";
4715
- return prefix + text.slice(start, end).trim() + suffix;
4795
+ return prefix + clampWindow(text, start, end).trim() + suffix;
4716
4796
  }
4717
4797
  export {
4718
4798
  ABSORB_PROMPT_MARKER,