billion-context 0.1.25 → 0.1.26

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
@@ -17900,8 +17900,2279 @@ var require_lib = __commonJS({
17900
17900
  }
17901
17901
  });
17902
17902
 
17903
+ // node_modules/acp-kernel/dist/index.js
17904
+ import { createRequire } from "module";
17905
+ var REF_WIDTH = 5;
17906
+ var MIN_INDEX = 1;
17907
+ var MAX_INDEX = 99999;
17908
+ var REF_PATTERN = /^m0*(\d{1,5})$/;
17909
+ var BLOCKED_REF = "BLOCKED";
17910
+ function indexToRef(index) {
17911
+ if (!Number.isInteger(index) || index < MIN_INDEX || index > MAX_INDEX) {
17912
+ throw new RangeError(
17913
+ `ref index out of bounds: ${index} (allowed ${MIN_INDEX}-${MAX_INDEX})`
17914
+ );
17915
+ }
17916
+ return `m${String(index).padStart(REF_WIDTH, "0")}`;
17917
+ }
17918
+ function refToIndex(ref) {
17919
+ const match = REF_PATTERN.exec(ref.trim().toLowerCase());
17920
+ if (!match) return null;
17921
+ const index = Number(match[1]);
17922
+ if (index < MIN_INDEX || index > MAX_INDEX) return null;
17923
+ return index;
17924
+ }
17925
+ function refForRaw(map, rawId) {
17926
+ return map.byRaw[rawId] ?? null;
17927
+ }
17928
+ function assignRefs(messages, options) {
17929
+ const map = {
17930
+ byRaw: { ...options.existing.byRaw },
17931
+ byRef: { ...options.existing.byRef }
17932
+ };
17933
+ let cursor = Number.isInteger(options.nextIndex) && options.nextIndex >= MIN_INDEX ? options.nextIndex : MIN_INDEX;
17934
+ let newlyAssigned = 0;
17935
+ for (const message of messages) {
17936
+ if (!message.id || options.shouldSkip?.(message)) continue;
17937
+ if (map.byRaw[message.id]) continue;
17938
+ if (options.isProtected?.(message)) {
17939
+ map.byRaw[message.id] = BLOCKED_REF;
17940
+ continue;
17941
+ }
17942
+ const ref = allocateFreeRef(map, cursor);
17943
+ cursor = ref.index + 1;
17944
+ map.byRaw[message.id] = ref.text;
17945
+ map.byRef[ref.text] = message.id;
17946
+ newlyAssigned++;
17947
+ }
17948
+ return { map, nextIndex: cursor, newlyAssigned };
17949
+ }
17950
+ function allocateFreeRef(map, start) {
17951
+ let candidate = Math.max(start, MIN_INDEX);
17952
+ while (candidate <= MAX_INDEX) {
17953
+ const text = indexToRef(candidate);
17954
+ if (!map.byRef[text]) {
17955
+ return { text, index: candidate };
17956
+ }
17957
+ candidate++;
17958
+ }
17959
+ throw new Error(
17960
+ `ref capacity exhausted: cannot allocate beyond ${indexToRef(MAX_INDEX)}`
17961
+ );
17962
+ }
17963
+ function highestUsedIndex(map) {
17964
+ let highest = 0;
17965
+ for (const ref of Object.values(map.byRaw)) {
17966
+ const index = ref === BLOCKED_REF ? null : refToIndex(ref);
17967
+ if (index !== null && index > highest) highest = index;
17968
+ }
17969
+ return highest;
17970
+ }
17971
+ function createInitialState() {
17972
+ return {
17973
+ blocks: [],
17974
+ messageRefs: { byRaw: {}, byRef: {} },
17975
+ nudge: {
17976
+ lastPerMessageNudgeTokens: 0,
17977
+ lastNudgeShownTokens: 0,
17978
+ baselineTokens: 0,
17979
+ anchors: {},
17980
+ lastShownByTier: {}
17981
+ },
17982
+ stats: { tokensCompressed: 0, compressionCount: 0 },
17983
+ nextBlockId: 1,
17984
+ nextRunId: 1
17985
+ };
17986
+ }
17987
+ function allocateBlockId(state) {
17988
+ const id = state.nextBlockId;
17989
+ state.nextBlockId = Math.max(1, id) + 1;
17990
+ return `b${id}`;
17991
+ }
17992
+ function allocateRunId(state) {
17993
+ const id = state.nextRunId;
17994
+ state.nextRunId = Math.max(1, id) + 1;
17995
+ return `r${id}`;
17996
+ }
17997
+ function blockById(state, blockId) {
17998
+ return state.blocks.find((block) => block.blockId === blockId);
17999
+ }
18000
+ function activeBlocks(state) {
18001
+ return state.blocks.filter((block) => block.active);
18002
+ }
18003
+ function coveredMessageIds(state) {
18004
+ const covered = /* @__PURE__ */ new Set();
18005
+ for (const block of state.blocks) {
18006
+ if (!block.active) continue;
18007
+ for (const id of block.effectiveMessageIds) covered.add(id);
18008
+ }
18009
+ return covered;
18010
+ }
18011
+ function advanceSurvival(state, promotionThreshold) {
18012
+ for (const block of state.blocks) {
18013
+ if (!block.active) continue;
18014
+ block.survivedCount += 1;
18015
+ if (block.survivedCount >= promotionThreshold) {
18016
+ block.generation = "old";
18017
+ }
18018
+ }
18019
+ }
18020
+ var SUMMARY_HEADER = "[Compressed conversation section]";
18021
+ function prune(messages, state, options = {}) {
18022
+ const covered = coveredMessageIds(state);
18023
+ if (covered.size === 0) return [...messages];
18024
+ const inject = options.injectSummaries ?? true;
18025
+ const firstUserIndex = messages.findIndex(
18026
+ (message) => message.role === "user"
18027
+ );
18028
+ const indexById = /* @__PURE__ */ new Map();
18029
+ messages.forEach((message, index) => indexById.set(message.id, index));
18030
+ const anchors = inject ? collectSummaryAnchors(state, indexById) : [];
18031
+ return stripOrphanedToolResults(
18032
+ stripOrphanedToolCalls(
18033
+ rebuildMessages(messages, covered, firstUserIndex, anchors)
18034
+ )
18035
+ );
18036
+ }
18037
+ function collectSummaryAnchors(state, indexById) {
18038
+ const anchors = [];
18039
+ for (const block of activeBlocks(state)) {
18040
+ let earliest = null;
18041
+ for (const id of block.effectiveMessageIds) {
18042
+ const index = indexById.get(id);
18043
+ if (index !== void 0 && (earliest === null || index < earliest)) {
18044
+ earliest = index;
18045
+ }
18046
+ }
18047
+ anchors.push({
18048
+ blockId: block.blockId,
18049
+ summary: block.summary,
18050
+ topic: block.topic,
18051
+ insertAt: earliest ?? 0
18052
+ });
18053
+ }
18054
+ anchors.sort((left, right) => left.insertAt - right.insertAt);
18055
+ return anchors;
18056
+ }
18057
+ function rebuildMessages(messages, covered, firstUserIndex, anchors) {
18058
+ const result = [];
18059
+ const pending = [...anchors];
18060
+ for (let index = 0; index < messages.length; index++) {
18061
+ while (pending.length > 0 && pending[0].insertAt === index) {
18062
+ result.push(renderSummary(pending.shift()));
18063
+ }
18064
+ if (index === firstUserIndex && firstUserIndex >= 0) {
18065
+ result.push(messages[index]);
18066
+ continue;
18067
+ }
18068
+ if (covered.has(messages[index].id)) continue;
18069
+ result.push(messages[index]);
18070
+ }
18071
+ while (pending.length > 0) {
18072
+ result.push(renderSummary(pending.shift()));
18073
+ }
18074
+ return result;
18075
+ }
18076
+ function renderSummary(anchor) {
18077
+ const body = anchor.summary.trim();
18078
+ const topicLine = anchor.topic ? `${SUMMARY_HEADER} \u2014 ${anchor.topic}` : SUMMARY_HEADER;
18079
+ const text = body.length === 0 ? topicLine : `${topicLine}
18080
+ ${body}`;
18081
+ return {
18082
+ id: `acp_summary_${anchor.blockId}`,
18083
+ role: "system",
18084
+ contentType: "text",
18085
+ text
18086
+ };
18087
+ }
18088
+ function stripOrphanedToolResults(messages) {
18089
+ const knownCallIds = /* @__PURE__ */ new Set();
18090
+ for (const m2 of messages) {
18091
+ if (m2.contentType === "tool-call" && m2.toolCallId) {
18092
+ knownCallIds.add(m2.toolCallId);
18093
+ }
18094
+ }
18095
+ return messages.filter(
18096
+ (m2) => m2.contentType !== "tool-result" || !m2.toolCallId || knownCallIds.has(m2.toolCallId)
18097
+ );
18098
+ }
18099
+ function stripOrphanedToolCalls(messages) {
18100
+ const knownResultIds = /* @__PURE__ */ new Set();
18101
+ for (const m2 of messages) {
18102
+ if (m2.contentType === "tool-result" && m2.toolCallId) {
18103
+ knownResultIds.add(m2.toolCallId);
18104
+ }
18105
+ }
18106
+ return messages.filter(
18107
+ (m2) => m2.contentType !== "tool-call" || !m2.toolCallId || m2.toolName === "compress" || knownResultIds.has(m2.toolCallId)
18108
+ );
18109
+ }
18110
+ function syncBlocks(messages, state) {
18111
+ const presentIds = new Set(messages.map((message) => message.id));
18112
+ const deactivated = [];
18113
+ const result = {
18114
+ blocks: state.blocks.map((block) => ({
18115
+ ...block,
18116
+ directMessageIds: [...block.directMessageIds],
18117
+ effectiveMessageIds: [...block.effectiveMessageIds],
18118
+ directBlockIds: [...block.directBlockIds]
18119
+ })),
18120
+ messageRefs: {
18121
+ byRaw: { ...state.messageRefs.byRaw },
18122
+ byRef: { ...state.messageRefs.byRef }
18123
+ },
18124
+ nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
18125
+ stats: { ...state.stats },
18126
+ nextBlockId: state.nextBlockId,
18127
+ nextRunId: state.nextRunId
18128
+ };
18129
+ const consumedBlockIds = /* @__PURE__ */ new Set();
18130
+ for (const block of result.blocks) {
18131
+ for (const consumedId of block.directBlockIds) {
18132
+ consumedBlockIds.add(consumedId);
18133
+ }
18134
+ }
18135
+ for (const block of result.blocks) {
18136
+ if (consumedBlockIds.has(block.blockId)) {
18137
+ block.active = false;
18138
+ continue;
18139
+ }
18140
+ block.active = true;
18141
+ const stillPresent = block.effectiveMessageIds.some(
18142
+ (id) => presentIds.has(id)
18143
+ );
18144
+ if (!stillPresent) {
18145
+ block.active = false;
18146
+ deactivated.push(block.blockId);
18147
+ }
18148
+ }
18149
+ return { state: result, deactivated };
18150
+ }
18151
+ var require2 = createRequire(import.meta.url);
18152
+ function defaultCountTokens(text) {
18153
+ if (!text) return 0;
18154
+ const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
18155
+ const cjkCount = cjk?.length ?? 0;
18156
+ return cjkCount + Math.ceil((text.length - cjkCount) / 4);
18157
+ }
18158
+ function estimateTokensFast(text) {
18159
+ if (!text) return 0;
18160
+ return Math.ceil(text.length / 4);
18161
+ }
18162
+ function defaultConfig(modelContextLimit, overrides = {}) {
18163
+ const base = {
18164
+ tiers: { enabled: true, tier2Trigger: 5, tier3Trigger: 10 },
18165
+ nudge: {
18166
+ maxContextLimitPct: 0.55,
18167
+ minContextLimitPct: 0.45,
18168
+ frequency: 5,
18169
+ iterationThreshold: 15,
18170
+ force: "soft",
18171
+ growthRatio: 0.05,
18172
+ growthFloor: Math.max(2e4, Math.round(modelContextLimit * 0.05)),
18173
+ growthCap: 5e4,
18174
+ minGrowthFloor: 2e4,
18175
+ minGrowthRatio: 0.45,
18176
+ emergencyThresholdPct: 0.8
18177
+ },
18178
+ promotionThreshold: 5,
18179
+ truncate: { threshold: 1 },
18180
+ compress: {
18181
+ minCompressRange: 5e3,
18182
+ maxSummaryLength: 2e4,
18183
+ minSummaryLength: 50
18184
+ },
18185
+ protectedTools: [],
18186
+ preserveRecentMessages: 5,
18187
+ preserveRecentTokens: 5e3,
18188
+ modelContextLimit
18189
+ };
18190
+ return {
18191
+ ...base,
18192
+ ...overrides,
18193
+ tiers: { ...base.tiers, ...overrides.tiers },
18194
+ nudge: { ...base.nudge, ...overrides.nudge },
18195
+ truncate: { ...base.truncate, ...overrides.truncate },
18196
+ compress: { ...base.compress, ...overrides.compress }
18197
+ };
18198
+ }
18199
+ function validateConfig(config) {
18200
+ const errors = [];
18201
+ if (!Number.isFinite(config.modelContextLimit) || config.modelContextLimit <= 0) {
18202
+ errors.push("modelContextLimit must be a positive number");
18203
+ }
18204
+ if (config.nudge.minContextLimitPct > config.nudge.maxContextLimitPct) {
18205
+ errors.push(
18206
+ "nudge.minContextLimitPct must not exceed nudge.maxContextLimitPct"
18207
+ );
18208
+ }
18209
+ if (config.promotionThreshold < 1) {
18210
+ errors.push("promotionThreshold must be >= 1");
18211
+ }
18212
+ if (config.truncate.threshold <= 0 || config.truncate.threshold > 1) {
18213
+ errors.push("truncate.threshold must be in (0, 1]");
18214
+ }
18215
+ for (const tier of [config.tiers.tier2Trigger, config.tiers.tier3Trigger]) {
18216
+ if (tier < 1) errors.push("tier triggers must be >= 1");
18217
+ }
18218
+ if (config.tiers.tier3Trigger <= config.tiers.tier2Trigger) {
18219
+ errors.push("tiers.tier3Trigger must be greater than tiers.tier2Trigger");
18220
+ }
18221
+ return errors;
18222
+ }
18223
+ var MESSAGE_REF_PATTERN = /^m0*(\d{1,5})$/;
18224
+ var BLOCK_REF_PATTERN = /^b(\d{1,9})$/;
18225
+ function parseBoundary(ref) {
18226
+ const normalized = ref.trim().toLowerCase();
18227
+ const messageMatch = MESSAGE_REF_PATTERN.exec(normalized);
18228
+ if (messageMatch) {
18229
+ const numericId = Number(messageMatch[1]);
18230
+ if (numericId >= 1 && numericId <= 99999) {
18231
+ return { kind: "message", numericId, raw: normalized };
18232
+ }
18233
+ }
18234
+ const blockMatch = BLOCK_REF_PATTERN.exec(normalized);
18235
+ if (blockMatch) {
18236
+ const numericId = Number(blockMatch[1]);
18237
+ if (numericId >= 1) return { kind: "block", numericId, raw: normalized };
18238
+ }
18239
+ return null;
18240
+ }
18241
+ function resolveBoundaries(input) {
18242
+ const start = parseBoundary(input.startRef);
18243
+ const end = parseBoundary(input.endRef);
18244
+ if (!start || !end) {
18245
+ throw new Error(
18246
+ `Invalid boundary ref(s): startId="${input.startRef}", endId="${input.endRef}". Use mNNNNN or bN.`
18247
+ );
18248
+ }
18249
+ const indexByRawId = /* @__PURE__ */ new Map();
18250
+ input.messages.forEach(
18251
+ (message, index) => indexByRawId.set(message.id, index)
18252
+ );
18253
+ let startIndex = resolveAnchorIndex(start, input.state, indexByRawId);
18254
+ let endIndex = resolveAnchorIndex(end, input.state, indexByRawId);
18255
+ if (startIndex === null || endIndex === null) {
18256
+ throw new Error(
18257
+ `Boundary not found in visible context (likely consumed by an existing block). startId="${input.startRef}", endId="${input.endRef}".`
18258
+ );
18259
+ }
18260
+ if (startIndex > endIndex) {
18261
+ [startIndex, endIndex] = [endIndex, startIndex];
18262
+ }
18263
+ const messageIds = [];
18264
+ for (let index = startIndex; index <= endIndex; index++) {
18265
+ const message = input.messages[index];
18266
+ if (message) messageIds.push(message.id);
18267
+ }
18268
+ const boundaryKind = start.kind === "block" || end.kind === "block" ? "block" : "message";
18269
+ const nestedBlockIds = [];
18270
+ const nestedSeen = /* @__PURE__ */ new Set();
18271
+ for (const block of activeBlocks(input.state)) {
18272
+ const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
18273
+ if (anchor !== null && anchor >= startIndex && anchor <= endIndex) {
18274
+ if (!nestedSeen.has(block.blockId)) {
18275
+ nestedSeen.add(block.blockId);
18276
+ nestedBlockIds.push(block.blockId);
18277
+ }
18278
+ }
18279
+ }
18280
+ const protectedGaps = [];
18281
+ return {
18282
+ startIndex,
18283
+ endIndex,
18284
+ messageIds,
18285
+ nestedBlockIds,
18286
+ boundaryKind,
18287
+ protectedGaps
18288
+ };
18289
+ }
18290
+ function resolveAnchorIndex(boundary, state, indexByRawId) {
18291
+ if (boundary.kind === "message") {
18292
+ const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
18293
+ if (!rawId) return null;
18294
+ const index = indexByRawId.get(rawId);
18295
+ return index === void 0 ? null : index;
18296
+ }
18297
+ const block = blockById(state, `b${boundary.numericId}`);
18298
+ if (!block || !block.active) return null;
18299
+ return earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
18300
+ }
18301
+ function formatPaddedRef(index) {
18302
+ return `m${String(index).padStart(5, "0")}`;
18303
+ }
18304
+ function earliestIndexOfIds(ids, indexByRawId) {
18305
+ let earliest = null;
18306
+ for (const id of ids) {
18307
+ const index = indexByRawId.get(id);
18308
+ if (index !== void 0 && (earliest === null || index < earliest)) {
18309
+ earliest = index;
18310
+ }
18311
+ }
18312
+ return earliest;
18313
+ }
18314
+ var TRUNCATION_MARKER = "[truncated for context space]";
18315
+ var DEFAULTS = {
18316
+ minOutputTokens: 1e3,
18317
+ keepPrefixChars: 2e3,
18318
+ keepSuffixChars: 2e3,
18319
+ protectRecentMessages: 3
18320
+ };
18321
+ function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, options = {}) {
18322
+ const opts = { ...DEFAULTS, ...options };
18323
+ if (config.modelContextLimit <= 0) return { messages, truncatedCount: 0, savedTokens: 0 };
18324
+ const threshold = config.truncate.threshold * config.modelContextLimit;
18325
+ if (tokenCount < threshold) return { messages, truncatedCount: 0, savedTokens: 0 };
18326
+ const protectedIndex = messages.length - opts.protectRecentMessages;
18327
+ const candidates = [];
18328
+ for (let index = 0; index < messages.length; index++) {
18329
+ if (index >= protectedIndex) break;
18330
+ const message = messages[index];
18331
+ if (message.contentType !== "tool-result") continue;
18332
+ const text = message.text ?? "";
18333
+ if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
18334
+ const tokens = countTokens(text);
18335
+ if (tokens < opts.minOutputTokens) continue;
18336
+ candidates.push({ index, tokens });
18337
+ }
18338
+ if (candidates.length === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
18339
+ candidates.sort((left, right) => right.tokens - left.tokens);
18340
+ const targetTokens = threshold * 0.9;
18341
+ let savedTokens = 0;
18342
+ const edits = /* @__PURE__ */ new Map();
18343
+ let truncatedCount = 0;
18344
+ for (const candidate of candidates) {
18345
+ if (tokenCount - savedTokens <= targetTokens) break;
18346
+ const original = messages[candidate.index].text ?? "";
18347
+ if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) continue;
18348
+ const prefix = original.slice(0, opts.keepPrefixChars);
18349
+ const suffix = original.slice(-opts.keepSuffixChars);
18350
+ const replacement = prefix + `
18351
+
18352
+ ...${TRUNCATION_MARKER} \u2014 original ~${candidate.tokens} tokens]...
18353
+
18354
+ ` + suffix;
18355
+ edits.set(candidate.index, replacement);
18356
+ savedTokens += candidate.tokens - countTokens(replacement);
18357
+ truncatedCount++;
18358
+ }
18359
+ if (truncatedCount === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
18360
+ const updated = messages.map(
18361
+ (message, index) => edits.has(index) ? { ...message, text: edits.get(index) } : message
18362
+ );
18363
+ return { messages: updated, truncatedCount, savedTokens };
18364
+ }
18365
+ var KEEP_LAST_ORPHANED = 0;
18366
+ function hideConsumedCompressCalls(state, messages) {
18367
+ const activeCallIds = /* @__PURE__ */ new Set();
18368
+ const allBlockCallIds = /* @__PURE__ */ new Set();
18369
+ for (const block of state.blocks) {
18370
+ if (block.compressCallId) {
18371
+ allBlockCallIds.add(block.compressCallId);
18372
+ if (block.active) activeCallIds.add(block.compressCallId);
18373
+ }
18374
+ }
18375
+ const lastOrphanedCallIds = [];
18376
+ for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
18377
+ const message = messages[i];
18378
+ if (message.toolName !== "compress" || message.contentType !== "tool-call") continue;
18379
+ const callId = message.toolCallId;
18380
+ if (callId && !allBlockCallIds.has(callId)) {
18381
+ lastOrphanedCallIds.push(callId);
18382
+ }
18383
+ }
18384
+ const keepCallIds = /* @__PURE__ */ new Set([...activeCallIds, ...lastOrphanedCallIds]);
18385
+ const hiddenCallIds = /* @__PURE__ */ new Set();
18386
+ for (const message of messages) {
18387
+ if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
18388
+ if (message.toolCallId) hiddenCallIds.add(message.toolCallId);
18389
+ }
18390
+ }
18391
+ let hidden = 0;
18392
+ const result = [];
18393
+ for (const message of messages) {
18394
+ if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
18395
+ hidden++;
18396
+ continue;
18397
+ }
18398
+ if (message.contentType === "tool-result" && message.toolCallId && hiddenCallIds.has(message.toolCallId)) {
18399
+ hidden++;
18400
+ continue;
18401
+ }
18402
+ result.push(message);
18403
+ }
18404
+ return { messages: result, hidden };
18405
+ }
18406
+ var registry = /* @__PURE__ */ new Map();
18407
+ function listMessageFilters() {
18408
+ return [...registry.values()];
18409
+ }
18410
+ function applyMessageFilters(messages, config) {
18411
+ if (!config?.enabled) {
18412
+ return { messages, partsFiltered: 0, partsDropped: 0, partsModified: 0 };
18413
+ }
18414
+ const active = listMessageFilters().filter(
18415
+ (filter) => config.filters?.[filter.name]?.enabled !== false
18416
+ );
18417
+ if (active.length === 0) {
18418
+ return { messages, partsFiltered: 0, partsDropped: 0, partsModified: 0 };
18419
+ }
18420
+ let working = messages.map((message) => ({ ...message }));
18421
+ const tally = { partsFiltered: 0, partsDropped: 0, partsModified: 0 };
18422
+ const total = working.length;
18423
+ const immediate = active.filter((filter) => !filter.keepLastOnly);
18424
+ for (let index = 0; index < working.length; index++) {
18425
+ const message = working[index];
18426
+ const text = message.text ?? "";
18427
+ if (text.length === 0) continue;
18428
+ let current = text;
18429
+ const baseCtx = {
18430
+ text: current,
18431
+ role: message.role,
18432
+ messageIndex: index,
18433
+ totalMessages: total,
18434
+ toolName: message.toolName
18435
+ };
18436
+ for (const filter of immediate) {
18437
+ let decision;
18438
+ try {
18439
+ decision = filter.filter(baseCtx);
18440
+ } catch {
18441
+ continue;
18442
+ }
18443
+ if (decision.action === "keep") continue;
18444
+ tally.partsFiltered++;
18445
+ if (decision.action === "drop") {
18446
+ current = "";
18447
+ tally.partsDropped++;
18448
+ } else if (decision.action === "modify" && decision.text !== void 0) {
18449
+ current = decision.text;
18450
+ tally.partsModified++;
18451
+ }
18452
+ baseCtx.text = current;
18453
+ }
18454
+ if (current !== text) working[index] = { ...message, text: current };
18455
+ }
18456
+ const keepLast = active.filter((filter) => filter.keepLastOnly);
18457
+ for (const filter of keepLast) {
18458
+ let foundLast = false;
18459
+ for (let index = working.length - 1; index >= 0; index--) {
18460
+ const message = working[index];
18461
+ const text = message.text ?? "";
18462
+ if (text.length === 0) continue;
18463
+ const ctx = {
18464
+ text,
18465
+ role: message.role,
18466
+ messageIndex: index,
18467
+ totalMessages: total,
18468
+ toolName: message.toolName
18469
+ };
18470
+ let decision;
18471
+ try {
18472
+ decision = filter.filter(ctx);
18473
+ } catch {
18474
+ continue;
18475
+ }
18476
+ if (decision.action !== "drop" && decision.action !== "modify") continue;
18477
+ if (foundLast) {
18478
+ tally.partsFiltered++;
18479
+ tally.partsDropped++;
18480
+ working[index] = { ...message, text: "" };
18481
+ } else {
18482
+ foundLast = true;
18483
+ if (decision.action === "modify" && decision.text !== void 0) {
18484
+ tally.partsFiltered++;
18485
+ tally.partsModified++;
18486
+ working[index] = { ...message, text: decision.text };
18487
+ }
18488
+ }
18489
+ }
18490
+ }
18491
+ return { messages: working, ...tally };
18492
+ }
18493
+ function formatTokens(tokens) {
18494
+ if (tokens < 1e3) return String(tokens);
18495
+ if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
18496
+ return Math.round(tokens / 1e3) + "K";
18497
+ }
18498
+ function classifyType(message) {
18499
+ if (message.contentType === "tool-call" || message.contentType === "tool-result") {
18500
+ return message.toolName || "tool";
18501
+ }
18502
+ return message.contentType;
18503
+ }
18504
+ function escapeRegex(s3) {
18505
+ return s3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18506
+ }
18507
+ var LT = "<";
18508
+ var GT = ">";
18509
+ var TAG_OPEN = LT + "acp ";
18510
+ var TAG_CLOSE = LT + "/acp" + GT;
18511
+ function acpTag(ref, tokens, type) {
18512
+ return TAG_OPEN + 'tokens="' + formatTokens(tokens) + '" type="' + type + '"' + GT + ref + TAG_CLOSE;
18513
+ }
18514
+ function renderMessage(message, map, countTokens, strategy) {
18515
+ const ref = refForRaw(map, message.id);
18516
+ if (!ref || ref === BLOCKED_REF) return message;
18517
+ if (strategy === "none") return message;
18518
+ if (strategy === "text-only" && message.contentType !== "text") {
18519
+ return message;
18520
+ }
18521
+ const ownTagRe = new RegExp(
18522
+ "^" + escapeRegex(TAG_OPEN) + "[^>]*" + GT + escapeRegex(ref) + escapeRegex(TAG_CLOSE) + "\\n?"
18523
+ );
18524
+ const cleanText = (message.text || "").replace(ownTagRe, "");
18525
+ const tokens = countTokens(cleanText);
18526
+ const type = classifyType(message);
18527
+ const prefix = acpTag(ref, tokens, type) + "\n";
18528
+ if (!cleanText) return { ...message, text: prefix };
18529
+ return { ...message, text: prefix + cleanText };
18530
+ }
18531
+ function renderVisibleRefs(messages, state, countTokens = (text) => Math.ceil(text.length / 4), strategy = "all") {
18532
+ const map = state.messageRefs;
18533
+ return messages.map(
18534
+ (message) => renderMessage(message, map, countTokens, strategy)
18535
+ );
18536
+ }
18537
+ function createRenderRefsNode(strategy) {
18538
+ return {
18539
+ name: "render-refs",
18540
+ run(io2, ctx) {
18541
+ return {
18542
+ ...io2,
18543
+ messages: renderVisibleRefs(io2.messages, io2.state, ctx.countTokens, strategy)
18544
+ };
18545
+ }
18546
+ };
18547
+ }
18548
+ var renderRefsNode = createRenderRefsNode("all");
18549
+ var ALWAYS_PROTECTED_TOOLS = ["compress"];
18550
+ var NEVER_PRESERVE_RECENT_TOOLS = [
18551
+ "decompress",
18552
+ "search_context",
18553
+ "read",
18554
+ "bash"
18555
+ ];
18556
+ function isNeverPreserveRecent(msg2) {
18557
+ if (msg2.contentType !== "tool-call" && msg2.contentType !== "tool-result") {
18558
+ return false;
18559
+ }
18560
+ if (!msg2.toolName) return false;
18561
+ return NEVER_PRESERVE_RECENT_TOOLS.includes(msg2.toolName);
18562
+ }
18563
+ function matchToolPattern(toolName, pattern) {
18564
+ if (pattern.endsWith("*")) {
18565
+ return toolName.startsWith(pattern.slice(0, -1));
18566
+ }
18567
+ return toolName === pattern;
18568
+ }
18569
+ function isMessageProtected(msg2, config) {
18570
+ if (msg2.contentType !== "tool-call" && msg2.contentType !== "tool-result" || !msg2.toolName) {
18571
+ return false;
18572
+ }
18573
+ if (ALWAYS_PROTECTED_TOOLS.includes(msg2.toolName)) {
18574
+ return true;
18575
+ }
18576
+ for (const pattern of config.protectedTools) {
18577
+ if (matchToolPattern(msg2.toolName, pattern)) return true;
18578
+ }
18579
+ if (config.isToolProtected?.(msg2.toolName, msg2.text)) return true;
18580
+ return false;
18581
+ }
18582
+ function collectProtectedToolCallIds(messages, config) {
18583
+ const ids = /* @__PURE__ */ new Set();
18584
+ for (const m2 of messages) {
18585
+ if (m2.contentType === "tool-call" && m2.toolCallId && isMessageProtected(m2, config)) {
18586
+ ids.add(m2.toolCallId);
18587
+ }
18588
+ }
18589
+ return ids;
18590
+ }
18591
+ function isMessageProtectedWithPairing(msg2, config, protectedCallIds) {
18592
+ if (isMessageProtected(msg2, config)) return true;
18593
+ if (msg2.contentType === "tool-result" && msg2.toolCallId && protectedCallIds.has(msg2.toolCallId)) {
18594
+ return true;
18595
+ }
18596
+ return false;
18597
+ }
18598
+ function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan = 20) {
18599
+ const callIdsInRange = /* @__PURE__ */ new Set();
18600
+ for (let i = startIndex; i <= endIndex; i++) {
18601
+ const msg2 = messages[i];
18602
+ if (!msg2 || !msg2.toolCallId) continue;
18603
+ if (msg2.toolName === "compress") continue;
18604
+ callIdsInRange.add(msg2.toolCallId);
18605
+ }
18606
+ if (callIdsInRange.size === 0) {
18607
+ return { startIndex, endIndex };
18608
+ }
18609
+ let newEndIndex = endIndex;
18610
+ for (let i = endIndex + 1; i < messages.length && i <= endIndex + maxScan; i++) {
18611
+ const msg2 = messages[i];
18612
+ if (!msg2) break;
18613
+ if (msg2.toolCallId && callIdsInRange.has(msg2.toolCallId)) {
18614
+ newEndIndex = i;
18615
+ } else if (newEndIndex > endIndex) {
18616
+ break;
18617
+ }
18618
+ }
18619
+ let newStartIndex = startIndex;
18620
+ for (let i = startIndex - 1; i >= 0 && i >= startIndex - maxScan; i--) {
18621
+ const msg2 = messages[i];
18622
+ if (!msg2) break;
18623
+ if (msg2.toolCallId && callIdsInRange.has(msg2.toolCallId)) {
18624
+ newStartIndex = i;
18625
+ } else if (newStartIndex < startIndex) {
18626
+ break;
18627
+ }
18628
+ }
18629
+ return { startIndex: newStartIndex, endIndex: newEndIndex };
18630
+ }
18631
+ function refNum(ref) {
18632
+ const n = parseInt(ref.slice(1), 10);
18633
+ return Number.isNaN(n) ? -1 : n;
18634
+ }
18635
+ function estimateMessageTokens(message) {
18636
+ return Math.ceil((message.text ?? "").length / 4);
18637
+ }
18638
+ function isToolMessage(message) {
18639
+ return message.contentType === "tool-call" || message.contentType === "tool-result";
18640
+ }
18641
+ function isSyntheticOrPruned(message, state) {
18642
+ if (message.text?.startsWith("[Compressed conversation section]")) return true;
18643
+ for (const block of state.blocks) {
18644
+ if (block.active && block.effectiveMessageIds.includes(message.id)) return true;
18645
+ }
18646
+ return false;
18647
+ }
18648
+ function computeProtectedRefs(messages, state, config) {
18649
+ const preserveN = config.preserveRecentMessages;
18650
+ const preserveTokens = config.preserveRecentTokens;
18651
+ const result = /* @__PURE__ */ new Set();
18652
+ const visible = [];
18653
+ for (const msg2 of messages) {
18654
+ if (isSyntheticOrPruned(msg2, state)) continue;
18655
+ if (isNeverPreserveRecent(msg2)) continue;
18656
+ const ref = state.messageRefs.byRaw[msg2.id];
18657
+ if (!ref || ref === "BLOCKED") continue;
18658
+ visible.push({ ref, tokens: estimateMessageTokens(msg2) });
18659
+ }
18660
+ if (preserveN > 0) {
18661
+ for (const m2 of visible.slice(-preserveN)) {
18662
+ result.add(m2.ref);
18663
+ }
18664
+ }
18665
+ if (preserveTokens > 0) {
18666
+ let tokenAccum = 0;
18667
+ for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
18668
+ result.add(visible[i].ref);
18669
+ tokenAccum += visible[i].tokens;
18670
+ }
18671
+ }
18672
+ if (preserveN > 0) {
18673
+ for (let i = messages.length - 1; i >= 0; i--) {
18674
+ const msg2 = messages[i];
18675
+ if (msg2.role !== "user" || isSyntheticOrPruned(msg2, state)) continue;
18676
+ const ref = state.messageRefs.byRaw[msg2.id];
18677
+ if (ref && ref !== "BLOCKED") result.add(ref);
18678
+ break;
18679
+ }
18680
+ }
18681
+ return result;
18682
+ }
18683
+ function buildCompressibleRanges(messages, state, config, protectedZoneRefs) {
18684
+ const compressibleMsgs = [];
18685
+ const protectedMsgs = [];
18686
+ const protectedCallIds = collectProtectedToolCallIds(messages, config);
18687
+ for (const msg2 of messages) {
18688
+ if (isSyntheticOrPruned(msg2, state)) continue;
18689
+ const ref = state.messageRefs.byRaw[msg2.id];
18690
+ if (!ref || ref === "BLOCKED") continue;
18691
+ const rn2 = refNum(ref);
18692
+ if (isMessageProtectedWithPairing(msg2, config, protectedCallIds)) {
18693
+ protectedMsgs.push({
18694
+ ref,
18695
+ refNum: rn2,
18696
+ tokens: estimateMessageTokens(msg2),
18697
+ tools: msg2.toolName ? [msg2.toolName] : []
18698
+ });
18699
+ continue;
18700
+ }
18701
+ if (protectedZoneRefs?.has(ref)) {
18702
+ continue;
18703
+ }
18704
+ compressibleMsgs.push({
18705
+ ref,
18706
+ refNum: rn2,
18707
+ tokens: estimateMessageTokens(msg2),
18708
+ isTool: isToolMessage(msg2),
18709
+ isUser: msg2.role === "user"
18710
+ });
18711
+ }
18712
+ const compressible = [];
18713
+ let cur = null;
18714
+ let prevRefNum = -2;
18715
+ for (const info of compressibleMsgs) {
18716
+ const hasGap = info.refNum > prevRefNum + 1;
18717
+ if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
18718
+ compressible.push(cur);
18719
+ cur = null;
18720
+ }
18721
+ prevRefNum = info.refNum;
18722
+ if (!cur) {
18723
+ cur = {
18724
+ startRef: info.ref,
18725
+ endRef: info.ref,
18726
+ count: 1,
18727
+ tokens: info.tokens,
18728
+ toolPct: info.isTool ? 100 : 0,
18729
+ textPct: info.isTool ? 0 : 100
18730
+ };
18731
+ } else {
18732
+ cur.endRef = info.ref;
18733
+ cur.count++;
18734
+ cur.tokens += info.tokens;
18735
+ if (info.isTool) {
18736
+ cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
18737
+ } else {
18738
+ cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
18739
+ }
18740
+ cur.textPct = 100 - cur.toolPct;
18741
+ }
18742
+ }
18743
+ if (cur) compressible.push(cur);
18744
+ const protectedRanges = [];
18745
+ let pcur = null;
18746
+ let pPrevRefNum = -2;
18747
+ for (const info of protectedMsgs) {
18748
+ const hasGap = info.refNum > pPrevRefNum + 1;
18749
+ if (pcur && hasGap) {
18750
+ protectedRanges.push(pcur);
18751
+ pcur = null;
18752
+ }
18753
+ pPrevRefNum = info.refNum;
18754
+ if (!pcur) {
18755
+ pcur = {
18756
+ startRef: info.ref,
18757
+ endRef: info.ref,
18758
+ count: 1,
18759
+ tokens: info.tokens,
18760
+ tools: [...info.tools]
18761
+ };
18762
+ } else {
18763
+ pcur.endRef = info.ref;
18764
+ pcur.count++;
18765
+ pcur.tokens += info.tokens;
18766
+ for (const t of info.tools) {
18767
+ if (!pcur.tools.includes(t)) pcur.tools.push(t);
18768
+ }
18769
+ }
18770
+ }
18771
+ if (pcur) protectedRanges.push(pcur);
18772
+ return {
18773
+ compressible: compressible.filter((g2) => g2.tokens > 0),
18774
+ protected: protectedRanges
18775
+ };
18776
+ }
18777
+ function runPipeline(nodes, initial, ctx) {
18778
+ let io2 = initial;
18779
+ for (const node of nodes) {
18780
+ if (node.enabled && !node.enabled(io2, ctx)) continue;
18781
+ io2 = node.run(io2, ctx);
18782
+ }
18783
+ return io2;
18784
+ }
18785
+ function createCore(ports = {}) {
18786
+ const countTokens = ports.countTokens ?? defaultCountTokens;
18787
+ function applyCompression(input) {
18788
+ const state = cloneState(input.state);
18789
+ const runId = allocateRunId(state);
18790
+ let blocksCreated = 0;
18791
+ let tokensCompressed = 0;
18792
+ const errors = [];
18793
+ const warnings = [];
18794
+ const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config);
18795
+ const preExistingCoverage = collectCoverage(state);
18796
+ const rangeIndexSets = [];
18797
+ for (const spec of input.ranges) {
18798
+ let resolved;
18799
+ try {
18800
+ resolved = resolveBoundaries({
18801
+ startRef: spec.startRef,
18802
+ endRef: spec.endRef,
18803
+ messages: input.messages,
18804
+ state
18805
+ });
18806
+ } catch {
18807
+ continue;
18808
+ }
18809
+ const indices = resolved.messageIds.map(
18810
+ (id) => input.messages.findIndex((m2) => m2.id === id)
18811
+ ).filter((i) => i >= 0);
18812
+ rangeIndexSets.push({ spec, indices });
18813
+ }
18814
+ const sortedRanges = [...rangeIndexSets].sort((a, b2) => {
18815
+ const aMin = a.indices.length > 0 ? Math.min(...a.indices) : Infinity;
18816
+ const bMin = b2.indices.length > 0 ? Math.min(...b2.indices) : Infinity;
18817
+ return aMin - bMin;
18818
+ });
18819
+ for (let i = 1; i < sortedRanges.length; i++) {
18820
+ const prev = sortedRanges[i - 1];
18821
+ const curr = sortedRanges[i];
18822
+ const prevMax = prev.indices.length > 0 ? Math.max(...prev.indices) : -1;
18823
+ const currMin = curr.indices.length > 0 ? Math.min(...curr.indices) : -1;
18824
+ if (prevMax >= currMin && prevMax >= 0) {
18825
+ return {
18826
+ state: input.state,
18827
+ result: {
18828
+ blocksCreated: 0,
18829
+ tokensCompressed: 0,
18830
+ errors: [
18831
+ `content: range (${prev.spec.startRef}..${prev.spec.endRef}) overlaps (${curr.spec.startRef}..${curr.spec.endRef}). Overlapping ranges cannot be compressed in the same batch.`
18832
+ ],
18833
+ warnings: []
18834
+ }
18835
+ };
18836
+ }
18837
+ }
18838
+ if (input.config.compress.minCompressRange > 0 && input.ranges.length > 0) {
18839
+ let totalRangeChars = 0;
18840
+ let hasBlockBoundaryRange = false;
18841
+ for (const spec of input.ranges) {
18842
+ let resolved;
18843
+ try {
18844
+ resolved = resolveBoundaries({
18845
+ startRef: spec.startRef,
18846
+ endRef: spec.endRef,
18847
+ messages: input.messages,
18848
+ state
18849
+ });
18850
+ } catch {
18851
+ continue;
18852
+ }
18853
+ if (resolved.boundaryKind === "block") {
18854
+ hasBlockBoundaryRange = true;
18855
+ continue;
18856
+ }
18857
+ for (const id of resolved.messageIds) {
18858
+ const msg2 = input.messages.find((m2) => m2.id === id);
18859
+ totalRangeChars += msg2?.text?.length ?? 0;
18860
+ }
18861
+ }
18862
+ if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
18863
+ return {
18864
+ state: input.state,
18865
+ result: {
18866
+ blocksCreated: 0,
18867
+ tokensCompressed: 0,
18868
+ errors: [
18869
+ `Total compressible content too small (${totalRangeChars} chars across ${input.ranges.length} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`
18870
+ ],
18871
+ warnings: []
18872
+ }
18873
+ };
18874
+ }
18875
+ }
18876
+ for (const spec of input.ranges) {
18877
+ try {
18878
+ const outcome = applySingleRange({
18879
+ spec,
18880
+ messages: input.messages,
18881
+ state,
18882
+ runId,
18883
+ config: input.config,
18884
+ protectedMessageIds,
18885
+ countTokens,
18886
+ preExistingCoverage
18887
+ });
18888
+ blocksCreated++;
18889
+ tokensCompressed += outcome.tokens;
18890
+ warnings.push(...outcome.warnings);
18891
+ } catch (error) {
18892
+ errors.push(error instanceof Error ? error.message : String(error));
18893
+ }
18894
+ }
18895
+ state.stats.compressionCount += blocksCreated;
18896
+ state.stats.tokensCompressed += tokensCompressed;
18897
+ if (blocksCreated > 0) {
18898
+ state.nudge.lastPerMessageNudgeTokens = 0;
18899
+ state.nudge.lastNudgeShownTokens = 0;
18900
+ state.nudge.lastShownByTier = {};
18901
+ }
18902
+ return { state, result: { blocksCreated, tokensCompressed, errors, warnings } };
18903
+ }
18904
+ function processTurn(input) {
18905
+ const configErrors = validateConfig(input.config);
18906
+ if (configErrors.length > 0) {
18907
+ console.warn(`[acp-kernel] Config validation warnings: ${configErrors.join("; ")}. Thresholds may not fire correctly.`);
18908
+ }
18909
+ const ctx = {
18910
+ config: input.config,
18911
+ tokenCount: input.tokenCount,
18912
+ countTokens
18913
+ };
18914
+ const initial = {
18915
+ messages: input.messages,
18916
+ state: input.state,
18917
+ effects: {}
18918
+ };
18919
+ const strategy = input.renderTags ?? "all";
18920
+ const nodes = buildNodes(strategy);
18921
+ const result = runPipeline(nodes, initial, ctx);
18922
+ return {
18923
+ messages: result.messages,
18924
+ state: result.state,
18925
+ nudge: result.effects.nudge
18926
+ };
18927
+ }
18928
+ function decompress(blockId, state) {
18929
+ return blockById(state, blockId);
18930
+ }
18931
+ function search(query, state) {
18932
+ const terms = query.toLowerCase().split(/\s+/).filter((term) => term.length > 0);
18933
+ if (terms.length === 0) return [];
18934
+ const scored = activeBlocks(state).map((block) => ({ block, score: scoreRelevance(block, terms) })).filter((entry) => entry.score > 0.1).sort((left, right) => right.score - left.score);
18935
+ return scored.map((entry) => entry.block);
18936
+ }
18937
+ function status(state, tokenCount, config) {
18938
+ const active = activeBlocks(state);
18939
+ const usage = config.modelContextLimit > 0 ? tokenCount / config.modelContextLimit : 0;
18940
+ return {
18941
+ contextUsage: usage,
18942
+ tokenCount,
18943
+ modelContextLimit: config.modelContextLimit,
18944
+ activeBlocks: active.length,
18945
+ totalBlocks: state.blocks.length,
18946
+ tokensCompressed: state.stats.tokensCompressed,
18947
+ breakdown: { active: active.length, total: state.blocks.length }
18948
+ };
18949
+ }
18950
+ function defaultNodes() {
18951
+ return buildNodes("all");
18952
+ }
18953
+ function buildNodes(strategy) {
18954
+ const base = [
18955
+ assignRefsNode,
18956
+ syncBlocksNode,
18957
+ pruneNode,
18958
+ filterNode,
18959
+ hideCompressCallsNode,
18960
+ recommendNode,
18961
+ nudgeNode,
18962
+ emergencyTruncateNode
18963
+ ];
18964
+ if (strategy === "none") return base;
18965
+ return [...base, createRenderRefsNode(strategy)];
18966
+ }
18967
+ return { processTurn, applyCompression, defaultNodes, decompress, search, status };
18968
+ }
18969
+ var assignRefsNode = {
18970
+ name: "assign-refs",
18971
+ run(io2, ctx) {
18972
+ const hasProtection = ctx.config.protectedTools.length > 0 || !!ctx.config.isToolProtected;
18973
+ const protectedFn = hasProtection ? (m2) => isMessageProtected(m2, ctx.config) : void 0;
18974
+ const refResult = assignRefs(io2.messages, {
18975
+ existing: io2.state.messageRefs,
18976
+ nextIndex: highestUsedIndex(io2.state.messageRefs) + 1,
18977
+ isProtected: protectedFn
18978
+ });
18979
+ return { ...io2, state: { ...io2.state, messageRefs: refResult.map } };
18980
+ }
18981
+ };
18982
+ var syncBlocksNode = {
18983
+ name: "sync-blocks",
18984
+ run(io2, ctx) {
18985
+ const synced = syncBlocks(io2.messages, io2.state);
18986
+ advanceSurvival(synced.state, ctx.config.promotionThreshold);
18987
+ return { ...io2, state: synced.state };
18988
+ }
18989
+ };
18990
+ var pruneNode = {
18991
+ name: "prune",
18992
+ run(io2) {
18993
+ return { ...io2, messages: prune(io2.messages, io2.state) };
18994
+ }
18995
+ };
18996
+ var filterNode = {
18997
+ name: "filter",
18998
+ enabled: (_io, ctx) => !!ctx.config.messageFilters?.enabled && listMessageFilters().length > 0,
18999
+ run(io2, ctx) {
19000
+ const applied = applyMessageFilters(io2.messages, ctx.config.messageFilters);
19001
+ return { ...io2, messages: applied.messages };
19002
+ }
19003
+ };
19004
+ var hideCompressCallsNode = {
19005
+ name: "hide-compress-calls",
19006
+ run(io2) {
19007
+ const hidden = hideConsumedCompressCalls(io2.state, io2.messages);
19008
+ return { ...io2, messages: hidden.messages };
19009
+ }
19010
+ };
19011
+ var recommendNode = {
19012
+ name: "recommend",
19013
+ run(io2, ctx) {
19014
+ const protectedRefs = computeProtectedRefs(
19015
+ io2.messages,
19016
+ io2.state,
19017
+ ctx.config
19018
+ );
19019
+ const contextRanges = buildCompressibleRanges(
19020
+ io2.messages,
19021
+ io2.state,
19022
+ ctx.config,
19023
+ protectedRefs
19024
+ );
19025
+ const nothingToCompress = contextRanges.compressible.length === 0;
19026
+ const recommendation = {
19027
+ contextRanges,
19028
+ recommendedRanges: contextRanges.compressible,
19029
+ nothingToCompress
19030
+ };
19031
+ return { ...io2, effects: { ...io2.effects, recommendation } };
19032
+ }
19033
+ };
19034
+ var nudgeNode = {
19035
+ name: "nudge-inject",
19036
+ run(io2, ctx) {
19037
+ const nudge = decideNudge({
19038
+ tokenCount: ctx.tokenCount,
19039
+ config: ctx.config,
19040
+ state: io2.state,
19041
+ messages: io2.messages,
19042
+ recommendation: io2.effects.recommendation,
19043
+ countTokens: ctx.countTokens
19044
+ });
19045
+ const baseline = io2.state.nudge.lastPerMessageNudgeTokens;
19046
+ const nudgeGrowthTokens = resolveAdaptiveGrowth(
19047
+ ctx.config.modelContextLimit,
19048
+ ctx.config.nudge
19049
+ );
19050
+ let stamped = { ...io2.state.nudge };
19051
+ if (baseline > 0 && ctx.tokenCount < baseline - nudgeGrowthTokens) {
19052
+ stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
19053
+ stamped.lastNudgeShownTokens = 0;
19054
+ }
19055
+ if (stamped.lastPerMessageNudgeTokens === 0) {
19056
+ stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
19057
+ }
19058
+ if (nudge.shouldInject) {
19059
+ stamped.lastNudgeShownTokens = ctx.tokenCount;
19060
+ if (nudge.tier !== null) {
19061
+ stamped.lastShownByTier = { ...stamped.lastShownByTier, [nudge.tier]: ctx.tokenCount };
19062
+ }
19063
+ }
19064
+ return {
19065
+ ...io2,
19066
+ state: { ...io2.state, nudge: stamped },
19067
+ effects: { ...io2.effects, nudge }
19068
+ };
19069
+ }
19070
+ };
19071
+ var emergencyTruncateNode = {
19072
+ name: "emergency-truncate",
19073
+ run(io2, ctx) {
19074
+ const usage = ctx.config.modelContextLimit > 0 ? ctx.tokenCount / ctx.config.modelContextLimit : 0;
19075
+ if (usage < ctx.config.truncate.threshold) return io2;
19076
+ const trunc = truncateLargeToolOutputs(
19077
+ io2.messages,
19078
+ ctx.tokenCount,
19079
+ ctx.config,
19080
+ ctx.countTokens,
19081
+ { protectRecentMessages: ctx.config.preserveRecentMessages }
19082
+ );
19083
+ return {
19084
+ ...io2,
19085
+ messages: trunc.messages,
19086
+ effects: { ...io2.effects, truncatedCount: trunc.truncatedCount }
19087
+ };
19088
+ }
19089
+ };
19090
+ function applySingleRange(input) {
19091
+ const warnings = [];
19092
+ const resolved = resolveBoundaries({
19093
+ startRef: input.spec.startRef,
19094
+ endRef: input.spec.endRef,
19095
+ messages: input.messages,
19096
+ state: input.state
19097
+ });
19098
+ const rangeMessageIds = applyToolPairAdjustment(
19099
+ resolved,
19100
+ input.messages
19101
+ );
19102
+ if (rangeMessageIds.length > resolved.messageIds.length) {
19103
+ const indexByRawId = /* @__PURE__ */ new Map();
19104
+ input.messages.forEach((m2, i) => indexByRawId.set(m2.id, i));
19105
+ const adjustedStart = indexByRawId.get(rangeMessageIds[0]) ?? resolved.startIndex;
19106
+ const adjustedEnd = indexByRawId.get(rangeMessageIds[rangeMessageIds.length - 1]) ?? resolved.endIndex;
19107
+ const nestedSeen = new Set(resolved.nestedBlockIds);
19108
+ for (const block2 of activeBlocks(input.state)) {
19109
+ if (nestedSeen.has(block2.blockId)) continue;
19110
+ const anchor = earliestIndexOfIds(block2.effectiveMessageIds, indexByRawId);
19111
+ if (anchor !== null && anchor >= adjustedStart && anchor <= adjustedEnd) {
19112
+ nestedSeen.add(block2.blockId);
19113
+ resolved.nestedBlockIds.push(block2.blockId);
19114
+ }
19115
+ }
19116
+ }
19117
+ const isBlockBoundary = resolved.boundaryKind === "block";
19118
+ const targetTier = resolveTargetTier(
19119
+ input.state,
19120
+ resolved.nestedBlockIds,
19121
+ isBlockBoundary
19122
+ );
19123
+ const outputTier = isBlockBoundary ? Math.min(3, targetTier + 1) : 1;
19124
+ const consumedBlockIds = resolved.nestedBlockIds.filter((id) => {
19125
+ const block2 = blockById(input.state, id);
19126
+ return block2?.active && block2.tier === targetTier;
19127
+ });
19128
+ const effectiveMessageIds = new Set(rangeMessageIds);
19129
+ for (const consumedId of consumedBlockIds) {
19130
+ const consumed = blockById(input.state, consumedId);
19131
+ if (consumed) {
19132
+ for (const id of consumed.effectiveMessageIds)
19133
+ effectiveMessageIds.add(id);
19134
+ }
19135
+ }
19136
+ const directMessageIds = [...effectiveMessageIds].filter(
19137
+ (id) => !input.preExistingCoverage.has(id)
19138
+ );
19139
+ let filteredIds = filterProtectedToolMessages(
19140
+ directMessageIds,
19141
+ input.messages,
19142
+ input.config
19143
+ );
19144
+ if (filteredIds.length < directMessageIds.length) {
19145
+ const kept = new Set(filteredIds);
19146
+ for (const id of directMessageIds) {
19147
+ if (!kept.has(id)) effectiveMessageIds.delete(id);
19148
+ }
19149
+ }
19150
+ const protectedRefs = input.protectedMessageIds;
19151
+ const hitProtectedRaw = protectedRefs ? filteredIds.filter((id) => {
19152
+ const ref = input.state.messageRefs.byRaw[id];
19153
+ return ref !== void 0 && protectedRefs.has(ref);
19154
+ }) : [];
19155
+ if (hitProtectedRaw.length > 0) {
19156
+ const protectedSet = new Set(hitProtectedRaw);
19157
+ filteredIds = filteredIds.filter((id) => !protectedSet.has(id));
19158
+ for (const id of hitProtectedRaw) effectiveMessageIds.delete(id);
19159
+ const hitRefs = hitProtectedRaw.map((id) => input.state.messageRefs.byRaw[id]).filter((v2) => typeof v2 === "string");
19160
+ if (filteredIds.length === 0 && consumedBlockIds.length === 0) {
19161
+ const recentN = input.config.preserveRecentMessages;
19162
+ throw new Error(
19163
+ `Range is entirely within the protected zone (the last ${recentN} messages and/or the most recent user message): ${hitRefs.join(
19164
+ ", "
19165
+ )}. Adjust startId/endId to older messages.`
19166
+ );
19167
+ }
19168
+ warnings.push(
19169
+ `Excluded ${hitProtectedRaw.length} protected message(s) ${hitRefs.join(
19170
+ ", "
19171
+ )} from compression range (recent/last-user zone).`
19172
+ );
19173
+ }
19174
+ validateCompressionRange(input, filteredIds, consumedBlockIds.length);
19175
+ let compressedTokens = 0;
19176
+ for (const id of filteredIds) {
19177
+ const message = input.messages.find((entry) => entry.id === id);
19178
+ compressedTokens += input.countTokens(message?.text ?? "");
19179
+ }
19180
+ for (const consumedId of consumedBlockIds) {
19181
+ const consumed = blockById(input.state, consumedId);
19182
+ if (consumed) {
19183
+ compressedTokens += input.countTokens(consumed.summary);
19184
+ }
19185
+ }
19186
+ const blockId = allocateBlockId(input.state);
19187
+ const block = {
19188
+ blockId,
19189
+ runId: input.runId,
19190
+ tier: outputTier,
19191
+ topic: input.spec.topic,
19192
+ summary: input.spec.summary,
19193
+ directMessageIds: filteredIds,
19194
+ effectiveMessageIds: [...effectiveMessageIds],
19195
+ directBlockIds: [...consumedBlockIds],
19196
+ compressedTokens,
19197
+ createdAt: Date.now(),
19198
+ survivedCount: 0,
19199
+ generation: "young",
19200
+ active: true,
19201
+ compressCallId: input.spec.compressCallId
19202
+ };
19203
+ input.state.blocks.push(block);
19204
+ for (const consumedId of consumedBlockIds) {
19205
+ const consumed = blockById(input.state, consumedId);
19206
+ if (consumed) consumed.active = false;
19207
+ }
19208
+ return { tokens: compressedTokens, warnings };
19209
+ }
19210
+ function applyToolPairAdjustment(resolved, messages) {
19211
+ if (resolved.boundaryKind === "block") {
19212
+ return resolved.messageIds;
19213
+ }
19214
+ const adjusted = adjustBoundariesForToolPairs(
19215
+ resolved.startIndex,
19216
+ resolved.endIndex,
19217
+ messages
19218
+ );
19219
+ if (adjusted.startIndex === resolved.startIndex && adjusted.endIndex === resolved.endIndex) {
19220
+ return resolved.messageIds;
19221
+ }
19222
+ const ids = [];
19223
+ for (let i = adjusted.startIndex; i <= adjusted.endIndex; i++) {
19224
+ const msg2 = messages[i];
19225
+ if (msg2) ids.push(msg2.id);
19226
+ }
19227
+ return ids;
19228
+ }
19229
+ function validateCompressionRange(input, directMessageIds, consumedBlockCount) {
19230
+ const cfg = input.config.compress;
19231
+ const summary = input.spec.summary?.trim() ?? "";
19232
+ if (summary.length === 0) {
19233
+ throw new Error(
19234
+ "Summary is empty \u2014 provide a meaningful summary of the compressed range."
19235
+ );
19236
+ }
19237
+ if (cfg.minSummaryLength > 0 && summary.length < cfg.minSummaryLength) {
19238
+ throw new Error(
19239
+ `Summary too short (${summary.length} chars, min ${cfg.minSummaryLength}). The summary must capture the compressed range's key information.`
19240
+ );
19241
+ }
19242
+ const effectiveMax = input.spec.summaryMaxChars ?? cfg.maxSummaryLength;
19243
+ if (effectiveMax > 0 && summary.length > effectiveMax) {
19244
+ throw new Error(
19245
+ `Summary too long (${summary.length} chars, max ${effectiveMax}). Strip noise \u2014 keep critical paths, decisions, errors, and code references. Or pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit.`
19246
+ );
19247
+ }
19248
+ if (directMessageIds.length === 0 && consumedBlockCount === 0) {
19249
+ throw new Error(
19250
+ "Range contains no compressible messages \u2014 all are already covered by active blocks or protected."
19251
+ );
19252
+ }
19253
+ }
19254
+ function filterProtectedToolMessages(directMessageIds, messages, config) {
19255
+ const protectedCallIds = /* @__PURE__ */ new Set();
19256
+ const removedIds = /* @__PURE__ */ new Set();
19257
+ for (const msg2 of messages) {
19258
+ if (isMessageProtected(msg2, config) && msg2.toolCallId) {
19259
+ protectedCallIds.add(msg2.toolCallId);
19260
+ }
19261
+ }
19262
+ for (const id of directMessageIds) {
19263
+ const msg2 = messages.find((m2) => m2.id === id);
19264
+ if (!msg2) continue;
19265
+ if (isMessageProtected(msg2, config)) {
19266
+ removedIds.add(id);
19267
+ if (msg2.toolCallId) protectedCallIds.add(msg2.toolCallId);
19268
+ }
19269
+ }
19270
+ for (const id of directMessageIds) {
19271
+ if (removedIds.has(id)) continue;
19272
+ const msg2 = messages.find((m2) => m2.id === id);
19273
+ if (!msg2) continue;
19274
+ if (msg2.contentType === "tool-result" && msg2.toolCallId && protectedCallIds.has(msg2.toolCallId)) {
19275
+ removedIds.add(id);
19276
+ }
19277
+ }
19278
+ return directMessageIds.filter((id) => !removedIds.has(id));
19279
+ }
19280
+ function resolveTargetTier(state, nestedBlockIds, isBlockBoundary) {
19281
+ if (!isBlockBoundary) return 1;
19282
+ if (nestedBlockIds.length === 0) return 1;
19283
+ let minTier = 3;
19284
+ for (const id of nestedBlockIds) {
19285
+ const block = blockById(state, id);
19286
+ if (block && block.tier < minTier) minTier = block.tier;
19287
+ }
19288
+ return minTier;
19289
+ }
19290
+ function collectCoverage(state) {
19291
+ const coverage = /* @__PURE__ */ new Set();
19292
+ for (const block of activeBlocks(state)) {
19293
+ for (const id of block.effectiveMessageIds) coverage.add(id);
19294
+ }
19295
+ return coverage;
19296
+ }
19297
+ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
19298
+ if (!modelContextLimit || modelContextLimit <= 0) return nudge.growthFloor;
19299
+ return Math.min(
19300
+ nudge.growthCap,
19301
+ Math.max(
19302
+ nudge.growthFloor,
19303
+ Math.round(modelContextLimit * nudge.growthRatio)
19304
+ )
19305
+ );
19306
+ }
19307
+ function pendingByTier(state, recommendation, countTokens) {
19308
+ const out = {};
19309
+ const compressible = recommendation?.contextRanges.compressible ?? [];
19310
+ out[1] = { pending: compressible.reduce((s3, r) => s3 + r.tokens, 0), targetBlocks: [] };
19311
+ const active = activeBlocks(state);
19312
+ const t1 = active.filter((b2) => b2.tier === 1);
19313
+ const t2 = active.filter((b2) => b2.tier === 2);
19314
+ out[2] = { pending: t1.reduce((s3, b2) => s3 + countTokens(b2.summary), 0), targetBlocks: t1 };
19315
+ out[3] = { pending: t2.reduce((s3, b2) => s3 + countTokens(b2.summary), 0), targetBlocks: t2 };
19316
+ return out;
19317
+ }
19318
+ function decideNudge(input) {
19319
+ const { config, state, tokenCount, recommendation, countTokens } = input;
19320
+ const limit = config.modelContextLimit;
19321
+ const usage = limit > 0 ? tokenCount / limit : 0;
19322
+ const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
19323
+ const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
19324
+ const baseline = state.nudge.lastPerMessageNudgeTokens;
19325
+ const hadPendingNudge = state.nudge.lastNudgeShownTokens > 0;
19326
+ const hasPendingNudge = hadPendingNudge;
19327
+ const effectiveThreshold = hasPendingNudge ? Math.floor(nudgeGrowthTokens / 2) : nudgeGrowthTokens;
19328
+ const growthReference = state.nudge.lastNudgeShownTokens > 0 ? state.nudge.lastNudgeShownTokens : baseline > 0 ? baseline : tokenCount;
19329
+ const growthFloor = Math.max(
19330
+ config.nudge.minGrowthFloor,
19331
+ config.nudge.minGrowthRatio * nudgeGrowthTokens
19332
+ );
19333
+ const growthSinceReference = tokenCount - growthReference;
19334
+ const rec = recommendation;
19335
+ const tiers = pendingByTier(state, rec, countTokens);
19336
+ let injectedTier = null;
19337
+ let injectedReason = "";
19338
+ const growthReady = growthSinceReference >= growthFloor;
19339
+ if (!emergencyOverride && growthReady) {
19340
+ for (const tier of [1, 2, 3]) {
19341
+ if (!config.tiers.enabled && tier > 1) break;
19342
+ const info = tiers[tier];
19343
+ if (!info || info.pending < nudgeGrowthTokens) continue;
19344
+ const lastShown = state.nudge.lastShownByTier[tier] ?? 0;
19345
+ const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
19346
+ if (!cadenceMet) continue;
19347
+ injectedTier = tier;
19348
+ injectedReason = tier === 1 ? `T1 compressible ${info.pending} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%` : `T${tier} distill ready: ${info.targetBlocks.length} tier-${tier - 1} blocks (${info.pending} tokens) >= ${nudgeGrowthTokens}, usage ${Math.round(usage * 100)}%`;
19349
+ break;
19350
+ }
19351
+ }
19352
+ const shouldInject = emergencyOverride || injectedTier !== null;
19353
+ let reason;
19354
+ if (emergencyOverride) {
19355
+ reason = `EMERGENCY: usage ${Math.round(usage * 100)}% >= ${Math.round(config.nudge.emergencyThresholdPct * 100)}%`;
19356
+ } else if (injectedTier !== null) {
19357
+ reason = injectedReason;
19358
+ } else {
19359
+ const tiersList = [1, 2, 3];
19360
+ const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
19361
+ const ready = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens).map((t) => `T${t} ${tiers[t].pending}`);
19362
+ const readyHint = ready.length > 0 ? `, ready: ${ready.join(", ")}` : "";
19363
+ const blocked = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor).map((t) => `T${t} (cadence)`);
19364
+ const blockedHint = blocked.length > 0 ? `, blocked: ${blocked.join(", ")}` : "";
19365
+ const maxPending = Math.max(0, ...Object.values(tiers).map((t) => t.pending));
19366
+ const pendingShort = maxPending < nudgeGrowthTokens;
19367
+ const growthShort = growthSinceReference < growthFloor;
19368
+ const parts = [];
19369
+ if (pendingShort) parts.push(`max compressible ${maxPending} < threshold ${nudgeGrowthTokens}`);
19370
+ if (growthShort) parts.push(`growth ${growthSinceReference} < floor ${growthFloor}`);
19371
+ if (parts.length === 0) parts.push(`max compressible ${maxPending}, growth ${growthSinceReference}`);
19372
+ reason = `${parts.join("; ")}${readyHint}${blockedHint}`;
19373
+ }
19374
+ const ctxBreakdown = computeContextBreakdown(input.messages, tokenCount, growthSinceReference, countTokens);
19375
+ return {
19376
+ shouldInject,
19377
+ reason,
19378
+ compressibleRanges: rec?.recommendedRanges ?? [],
19379
+ protectedRanges: rec?.contextRanges.protected ?? [],
19380
+ tierTargetBlocks: injectedTier ? tiers[injectedTier].targetBlocks : [],
19381
+ contextUsage: usage,
19382
+ tier: injectedTier,
19383
+ breakdown: {
19384
+ usage,
19385
+ growth: growthSinceReference,
19386
+ growthReference,
19387
+ effectiveThreshold,
19388
+ nudgeGrowthTokens,
19389
+ growthFloor,
19390
+ hasPendingNudge: hasPendingNudge ? 1 : 0,
19391
+ emergencyOverride: emergencyOverride ? 1 : 0,
19392
+ pendingT1: tiers[1].pending,
19393
+ pendingT2: tiers[2].pending,
19394
+ pendingT3: tiers[3].pending
19395
+ },
19396
+ contextBreakdown: ctxBreakdown
19397
+ };
19398
+ }
19399
+ function computeContextBreakdown(messages, total, growth, countTokens) {
19400
+ const count = countTokens ?? ((t) => Math.ceil(t.length / 4));
19401
+ let system = 0, tool = 0, summaries = 0, code = 0, text = 0;
19402
+ for (const msg2 of messages) {
19403
+ const tokens = count(msg2.text ?? "");
19404
+ if (msg2.text?.startsWith("[Compressed conversation section]")) {
19405
+ summaries += tokens;
19406
+ } else if (msg2.contentType === "tool-call" || msg2.contentType === "tool-result") {
19407
+ tool += tokens;
19408
+ } else if (msg2.role === "system") {
19409
+ system += tokens;
19410
+ } else if (msg2.text?.includes("```")) {
19411
+ code += tokens;
19412
+ } else {
19413
+ text += tokens;
19414
+ }
19415
+ }
19416
+ return { system, tool, summaries, code, text, total, growth };
19417
+ }
19418
+ function cloneState(state) {
19419
+ return {
19420
+ blocks: state.blocks.map((block) => ({
19421
+ ...block,
19422
+ directMessageIds: [...block.directMessageIds],
19423
+ effectiveMessageIds: [...block.effectiveMessageIds],
19424
+ directBlockIds: [...block.directBlockIds]
19425
+ })),
19426
+ messageRefs: {
19427
+ byRaw: { ...state.messageRefs.byRaw },
19428
+ byRef: { ...state.messageRefs.byRef }
19429
+ },
19430
+ nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
19431
+ stats: { ...state.stats },
19432
+ nextBlockId: state.nextBlockId,
19433
+ nextRunId: state.nextRunId
19434
+ };
19435
+ }
19436
+ function scoreRelevance(block, terms) {
19437
+ const topic = (block.topic ?? "").toLowerCase();
19438
+ const summary = block.summary.toLowerCase();
19439
+ let score = 0;
19440
+ for (const term of terms) {
19441
+ const topicHits = countOccurrences(topic, term);
19442
+ if (topicHits > 0) score += Math.min(topicHits * 0.15, 0.45);
19443
+ const summaryHits = countOccurrences(summary, term);
19444
+ if (summaryHits > 0) score += Math.min(summaryHits * 0.04, 0.2);
19445
+ }
19446
+ return Math.min(score, 1);
19447
+ }
19448
+ function countOccurrences(haystack, needle) {
19449
+ if (!haystack || !needle) return 0;
19450
+ let count = 0;
19451
+ let position = 0;
19452
+ while ((position = haystack.indexOf(needle, position)) !== -1) {
19453
+ count++;
19454
+ position += needle.length;
19455
+ }
19456
+ return count;
19457
+ }
19458
+ var COMPRESS_PHILOSOPHY = `Compression Philosophy:
19459
+ - All compression serves the primary task, but be frugal.
19460
+ - Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
19461
+ - Compress by need, not by percentage.
19462
+ - Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
19463
+ var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
19464
+
19465
+ When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.
19466
+
19467
+ KEEP VERBATIM \u2014 never paraphrase or abbreviate these:
19468
+ - Full file paths with line numbers, directory prefix on every mention (\`lib/hooks.ts:347\`, \`src/index.ts:12-18\`, \`gatenet_v3/model.py:45\`). Never abbreviate to a bare filename (\`hooks.ts\`, \`model.py\`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.
19469
+ - Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. \`kv_keys += define_gate * a_key[i](emb)\` is more useful than "see model_kvnet.py").
19470
+ - Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).
19471
+ - Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not "X is worse" alone (write "1.76\xD7 PPL gap because KV store is static", not "KVNet underperforms").
19472
+ - Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing; without it the decision looks arbitrary).
19473
+ - Constraints discovered ("must support Node 22", "no new dependencies", "AGENTS.md forbids \`as any\`").
19474
+ - Exact values: versions, config keys, thresholds, magic numbers.
19475
+ - User intent \u2014 quote short user messages verbatim. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Mark them clearly as past quotes (e.g., "User said: ..."), not as current directives. Losing these changes the task itself.
19476
+ - The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., "initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause"). Losing the goal or its evolution makes all subsequent work appear unmotivated.
19477
+ - Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.
19478
+ - Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.
19479
+ - Message refs of key anchors (\`m00420\`, \`m00510\u2013m00520\`) \u2014 they let you or a later reader jump back via decompress to the exact original.
19480
+
19481
+ DROP \u2014 extract the signal, discard the vessel:
19482
+ - Verbose logs (build/test/\`npm\` output) once you have captured the error line or the result.
19483
+ - Duplicate file reads once the needed content is recorded.
19484
+ - Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).
19485
+ - Dead-end exploration \u2014 but PRESERVE the lesson in one line: "tried X, failed because Y".
19486
+ - Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).
19487
+ - Repeated status checks (\`git status\`, \`ls\`) once state is known.
19488
+
19489
+ For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
19490
+
19491
+ PRIORITY \u2014 when the summary must be compact, preserve in this order:
19492
+ 1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
19493
+ 2. Decisions and rationale.
19494
+ 3. Exact technical artifacts: paths, signatures, errors, values.
19495
+ 4. Conclusions and key findings.
19496
+ 5. Lessons learned: what failed and why.
19497
+
19498
+ Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
19499
+ var TIER2_DISTILL_RULES = `TIER 2 COMPRESSION \u2014 DISTILLATION
19500
+
19501
+ You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
19502
+
19503
+ KEEP \u2014 these are the only things that survive distillation:
19504
+ - Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
19505
+ - Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
19506
+ - Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
19507
+ - Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
19508
+ - Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
19509
+ - Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
19510
+ - Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
19511
+ - Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
19512
+
19513
+ DROP \u2014 these were useful during the work but are no longer needed:
19514
+ - Exact line numbers, diffs, verbose function signatures, full code listings.
19515
+ - Build/deploy process details, test execution steps.
19516
+ - Review process details (who reviewed, what rounds, test counts).
19517
+ - Verbose logs, command output, intermediate debugging steps.
19518
+
19519
+ FORMAT:
19520
+ - Start each distilled block with a source header line:
19521
+ \`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
19522
+ Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
19523
+ - 3-5 bullet points per source block, each a self-contained fact.
19524
+ - Dense, scannable \u2014 no narrative prose.
19525
+ - Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
19526
+ - Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
19527
+
19528
+ SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]."`;
19529
+ var TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
19530
+
19531
+ You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
19532
+
19533
+ PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
19534
+ 1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
19535
+ 2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
19536
+ 3. Key decisions with architectural impact ("chose X over Y because Z").
19537
+ 4. Critical constraints ("must support Node 22").
19538
+ Drop everything else. Tier 3 is a lookup index, not a knowledge base.
19539
+
19540
+ FORMAT:
19541
+ - Start with a source header line:
19542
+ \`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
19543
+ - Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
19544
+ - No explanations, no rationale, no process \u2014 just the fact.
19545
+ - Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
19546
+ - Merge related facts from different source blocks if they concern the same topic.
19547
+
19548
+ EXAMPLES:
19549
+ - "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
19550
+ - "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
19551
+ - "Bug 1214 fixed \u2014 compress consumed all user messages"
19552
+ - "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
19553
+ - "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
19554
+
19555
+ DROP:
19556
+ - Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
19557
+ - Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
19558
+ - Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
19559
+ - Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
19560
+
19561
+ SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
19562
+ var EFFICIENCY_NOTE = `This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
19563
+
19564
+ ${COMPRESS_PHILOSOPHY}`;
19565
+ var EMERGENCY_HEADER = `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
19566
+
19567
+ ${COMPRESS_PHILOSOPHY}`;
19568
+ function formatK(n) {
19569
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
19570
+ return `${n}`;
19571
+ }
19572
+ function formatBreakdown(bd) {
19573
+ if (!bd) return "";
19574
+ const parts = [];
19575
+ if (bd.system > 0) parts.push(`${formatK(bd.system)} system`);
19576
+ if (bd.tool > 0) parts.push(`${formatK(bd.tool)} tool`);
19577
+ if (bd.summaries > 0) parts.push(`${formatK(bd.summaries)} summaries`);
19578
+ if (bd.code > 0) parts.push(`${formatK(bd.code)} code`);
19579
+ if (bd.text > 0) parts.push(`${formatK(bd.text)} text`);
19580
+ const growth = bd.growth > 0 ? `
19581
+ +${formatK(bd.growth)} since last nudge` : "";
19582
+ return `Context breakdown: ${parts.join(" | ")}${growth}`;
19583
+ }
19584
+ function formatTierTargetBlocks(blocks) {
19585
+ if (blocks.length === 0) {
19586
+ return "Target blocks: (none \u2014 no tier blocks found)";
19587
+ }
19588
+ const lines = blocks.map((b2) => {
19589
+ const summaryTokens = Math.ceil((b2.summary ?? "").length / 4);
19590
+ const topic = b2.topic ? ` "${b2.topic}"` : "";
19591
+ return ` ${b2.blockId} ${b2.effectiveMessageIds.length} msgs ${formatK(b2.compressedTokens)}\u2192${formatK(summaryTokens)}${topic}`;
19592
+ });
19593
+ return `Target ${blocks[0].tier === 1 ? "tier-1" : "tier-2"} blocks to distill (${blocks.length}):
19594
+ ${lines.join("\n")}`;
19595
+ }
19596
+ function formatRanges(compressible, protectedRanges) {
19597
+ if (compressible.length === 0 && protectedRanges.length === 0) {
19598
+ return "[No specific ranges detected \u2014 compress any consumed content.]";
19599
+ }
19600
+ const refNum2 = (ref) => {
19601
+ const m2 = ref.match(/\d+/);
19602
+ return m2 ? parseInt(m2[0], 10) : 0;
19603
+ };
19604
+ const entries = [];
19605
+ for (const r of compressible) {
19606
+ entries.push({
19607
+ startRef: r.startRef,
19608
+ endRef: r.endRef,
19609
+ startNum: refNum2(r.startRef),
19610
+ endNum: refNum2(r.endRef),
19611
+ count: r.count,
19612
+ tokens: r.tokens,
19613
+ toolPct: r.toolPct,
19614
+ textPct: r.textPct,
19615
+ compressibleTokens: r.tokens,
19616
+ compressibleCount: r.count,
19617
+ protectedTokens: 0,
19618
+ protectedCount: 0,
19619
+ protectedTools: [],
19620
+ dangerous: r.dangerous ?? false
19621
+ });
19622
+ }
19623
+ for (const r of protectedRanges) {
19624
+ entries.push({
19625
+ startRef: r.startRef,
19626
+ endRef: r.endRef,
19627
+ startNum: refNum2(r.startRef),
19628
+ endNum: refNum2(r.endRef),
19629
+ count: r.count,
19630
+ tokens: r.tokens,
19631
+ toolPct: 0,
19632
+ textPct: 0,
19633
+ compressibleTokens: 0,
19634
+ compressibleCount: 0,
19635
+ protectedTokens: r.tokens,
19636
+ protectedCount: r.count,
19637
+ protectedTools: [...r.tools],
19638
+ dangerous: false
19639
+ });
19640
+ }
19641
+ entries.sort((a, b2) => a.startNum - b2.startNum);
19642
+ const merged = [];
19643
+ for (const e of entries) {
19644
+ const last = merged[merged.length - 1];
19645
+ if (last && e.startNum <= last.endNum + 1) {
19646
+ last.endRef = e.endRef;
19647
+ last.endNum = Math.max(last.endNum, e.endNum);
19648
+ last.count += e.count;
19649
+ last.tokens += e.tokens;
19650
+ last.compressibleTokens += e.compressibleTokens;
19651
+ last.compressibleCount += e.compressibleCount;
19652
+ last.protectedTokens += e.protectedTokens;
19653
+ last.protectedCount += e.protectedCount;
19654
+ if (e.dangerous) last.dangerous = true;
19655
+ for (const t of e.protectedTools) {
19656
+ if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
19657
+ }
19658
+ } else {
19659
+ merged.push({ ...e });
19660
+ }
19661
+ }
19662
+ const lines = merged.map((e) => {
19663
+ const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
19664
+ if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
19665
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
19666
+ }
19667
+ if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
19668
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
19669
+ }
19670
+ return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
19671
+ });
19672
+ return `Compressible ranges (${merged.length}, oldest first):
19673
+ ${lines.join("\n")}`;
19674
+ }
19675
+ function renderNudgeText(decision) {
19676
+ const breakdownStr = formatBreakdown(decision.contextBreakdown);
19677
+ const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
19678
+ if (decision.tier !== null && decision.tier >= 2) {
19679
+ const isT2 = decision.tier === 2;
19680
+ const targets = decision.tierTargetBlocks ?? [];
19681
+ const blockList = formatTierTargetBlocks(targets);
19682
+ const startId = targets[0]?.blockId ?? "b1";
19683
+ const endId = targets[targets.length - 1]?.blockId ?? "b5";
19684
+ return {
19685
+ voice: "gentle",
19686
+ text: [
19687
+ EFFICIENCY_NOTE,
19688
+ "",
19689
+ breakdownStr,
19690
+ "",
19691
+ `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`,
19692
+ isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries.`,
19693
+ blockList,
19694
+ `Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
19695
+ "",
19696
+ HOW_TO_COMPRESS_RULES,
19697
+ "",
19698
+ isT2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES
19699
+ ].join("\n")
19700
+ };
19701
+ }
19702
+ const isEmergency = !!decision.breakdown?.emergencyOverride;
19703
+ if (isEmergency) {
19704
+ return {
19705
+ voice: "emergency",
19706
+ text: [
19707
+ EMERGENCY_HEADER,
19708
+ "",
19709
+ breakdownStr,
19710
+ "",
19711
+ HOW_TO_COMPRESS_RULES,
19712
+ "",
19713
+ `{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
19714
+ "Only use IDs from visible messages above. Compress older work first.",
19715
+ "",
19716
+ rangesStr
19717
+ ].join("\n")
19718
+ };
19719
+ }
19720
+ return {
19721
+ voice: "gentle",
19722
+ text: [
19723
+ EFFICIENCY_NOTE,
19724
+ "",
19725
+ breakdownStr,
19726
+ "",
19727
+ HOW_TO_COMPRESS_RULES,
19728
+ "",
19729
+ rangesStr,
19730
+ "",
19731
+ `\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`
19732
+ ].join("\n")
19733
+ };
19734
+ }
19735
+ function deactivateBlock(state, blockIds, options = {}) {
19736
+ const targets = new Set(blockIds);
19737
+ const updated = state.blocks.map((block) => {
19738
+ if (!targets.has(block.blockId) || !block.active) return block;
19739
+ return {
19740
+ ...block,
19741
+ active: false,
19742
+ durationMs: block.durationMs,
19743
+ createdAt: block.createdAt
19744
+ };
19745
+ });
19746
+ let final = updated;
19747
+ if (options.deep) {
19748
+ const visited = /* @__PURE__ */ new Set();
19749
+ const queue = [];
19750
+ for (const id of blockIds) {
19751
+ const block = updated.find((b2) => b2.blockId === id);
19752
+ if (block) queue.push(...block.directBlockIds);
19753
+ }
19754
+ while (queue.length > 0) {
19755
+ const id = queue.shift();
19756
+ if (visited.has(id)) continue;
19757
+ visited.add(id);
19758
+ final = final.map((block) => {
19759
+ if (block.blockId !== id) return block;
19760
+ queue.push(...block.directBlockIds);
19761
+ return block.active ? { ...block, active: false } : block;
19762
+ });
19763
+ }
19764
+ }
19765
+ return { ...state, blocks: final };
19766
+ }
19767
+ function collectBlockContent(state, block, messages, options = {}) {
19768
+ const full = options.full ?? false;
19769
+ const targetIds = new Set(block.effectiveMessageIds);
19770
+ if (full) {
19771
+ const msgs = messages.filter((m2) => targetIds.has(m2.id));
19772
+ if (msgs.length === 0) return { text: "", count: 0 };
19773
+ return { text: msgs.map(formatMessage).join("\n\n"), count: msgs.length };
19774
+ }
19775
+ const nestedChildren = [];
19776
+ const nestedCovered = /* @__PURE__ */ new Set();
19777
+ for (const childId of block.directBlockIds) {
19778
+ const child = state.blocks.find((b2) => b2.blockId === childId);
19779
+ if (!child?.active) continue;
19780
+ nestedChildren.push(child);
19781
+ for (const id of child.effectiveMessageIds) nestedCovered.add(id);
19782
+ }
19783
+ const parts = [];
19784
+ for (const child of nestedChildren) {
19785
+ const label = child.topic ? `${child.blockId}: ${child.topic}` : child.blockId;
19786
+ parts.push(`${SUMMARY_HEADER} \u2014 ${label}
19787
+ ${child.summary}`);
19788
+ }
19789
+ let directCount = 0;
19790
+ for (const m2 of messages) {
19791
+ if (targetIds.has(m2.id) && !nestedCovered.has(m2.id)) {
19792
+ parts.push(formatMessage(m2));
19793
+ directCount++;
19794
+ }
19795
+ }
19796
+ const count = directCount + nestedChildren.length;
19797
+ if (count === 0) return { text: "", count: 0 };
19798
+ return { text: parts.join("\n\n"), count };
19799
+ }
19800
+ function formatMessage(message) {
19801
+ const text = message.text ?? "";
19802
+ if (message.toolName && message.contentType !== "text") {
19803
+ return `[${message.role} \u2022 ${message.toolName}]
19804
+ ${text}`;
19805
+ }
19806
+ return `[${message.role}]
19807
+ ${text}`;
19808
+ }
19809
+ function formatTokens2(n) {
19810
+ if (!Number.isFinite(n) || n <= 0) return "0";
19811
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
19812
+ }
19813
+ function pct(n, total) {
19814
+ if (n <= 0 || total <= 0) return 0;
19815
+ return Math.max(1, Math.round(n / total * 100));
19816
+ }
19817
+ function numericPart2(blockId) {
19818
+ const match = /^b(\d+)$/.exec(blockId);
19819
+ return match && match[1] !== void 0 ? Number(match[1]) : 0;
19820
+ }
19821
+ function summaryTokensOf(block, countTokens) {
19822
+ return countTokens(block.summary);
19823
+ }
19824
+ function effectiveCompressedTokens(block, _state, _countTokens) {
19825
+ return block.compressedTokens;
19826
+ }
19827
+ function tierLabel(block) {
19828
+ return `T${block.tier}`;
19829
+ }
19830
+ function tierBreakdown(blocks, countTokens) {
19831
+ const tierTokens = {};
19832
+ for (const block of blocks) {
19833
+ tierTokens[block.tier] = (tierTokens[block.tier] ?? 0) + summaryTokensOf(block, countTokens);
19834
+ }
19835
+ const tiers = Object.keys(tierTokens).map(Number);
19836
+ if (tiers.length <= 1) return null;
19837
+ const parts = [];
19838
+ for (const tier of [1, 2, 3]) {
19839
+ if (tierTokens[tier]) parts.push(`T${tier}: ${formatTokens2(tierTokens[tier])}`);
19840
+ }
19841
+ return parts.join(" | ");
19842
+ }
19843
+ function collectVisible(messages, state, countTokens) {
19844
+ const coveredIds = /* @__PURE__ */ new Set();
19845
+ for (const block of state.blocks) {
19846
+ if (!block.active) continue;
19847
+ for (const id of block.effectiveMessageIds) coveredIds.add(id);
19848
+ }
19849
+ let summaryTokens = 0;
19850
+ for (const block of state.blocks) {
19851
+ if (block.active) summaryTokens += summaryTokensOf(block, countTokens);
19852
+ }
19853
+ const visible = [];
19854
+ messages.forEach((message, index) => {
19855
+ if (coveredIds.has(message.id)) return;
19856
+ const ref = refForRaw(state.messageRefs, message.id);
19857
+ if (!ref) return;
19858
+ const tokens = countTokens(message.text ?? "");
19859
+ const tool = message.toolName ?? "text";
19860
+ if (tokens > 0) visible.push({ ref, tokens, tool, index });
19861
+ });
19862
+ return { visible, summaryTokens };
19863
+ }
19864
+ function buildStatusReport(state, messages, countTokens, options = {}) {
19865
+ const scope = options.scope;
19866
+ const view = options.view ?? "ranges";
19867
+ const toolFilter = options.tool;
19868
+ const sort = options.sort ?? "size";
19869
+ const limit = options.limit ?? 30;
19870
+ const activeBlocks2 = state.blocks.filter((b2) => b2.active).sort((a, b2) => numericPart2(a.blockId) - numericPart2(b2.blockId));
19871
+ if (scope === "compressed") {
19872
+ return renderCompressedDrilldown(activeBlocks2, state, sort, limit, countTokens);
19873
+ }
19874
+ const { visible, summaryTokens } = collectVisible(messages, state, countTokens);
19875
+ if (scope === "uncompressed") {
19876
+ if (view === "messages") {
19877
+ return renderMessageDrilldown(visible, toolFilter, sort, limit);
19878
+ }
19879
+ return renderUncompressedRanges(visible);
19880
+ }
19881
+ return renderOverview(visible, summaryTokens, activeBlocks2, state, countTokens, limit);
19882
+ }
19883
+ function renderOverview(visible, summaryTokens, blocks, state, countTokens, limit) {
19884
+ const lines = [];
19885
+ const toolTypeMap = /* @__PURE__ */ new Map();
19886
+ for (const message of visible) {
19887
+ toolTypeMap.set(message.tool, (toolTypeMap.get(message.tool) ?? 0) + message.tokens);
19888
+ }
19889
+ const topTool = [...toolTypeMap.entries()].sort((a, b2) => b2[1] - a[1])[0]?.[0];
19890
+ const totalTool = visible.filter((m2) => m2.tool !== "text").reduce((sum, m2) => sum + m2.tokens, 0);
19891
+ const totalText = visible.filter((m2) => m2.tool === "text").reduce((sum, m2) => sum + m2.tokens, 0);
19892
+ const total = summaryTokens + totalTool + totalText;
19893
+ lines.push("CONTEXT BREAKDOWN");
19894
+ lines.push(
19895
+ ` ${formatTokens2(totalTool)} tool (${pct(totalTool, total)}%) | ${formatTokens2(totalText)} text (${pct(totalText, total)}%) | ${formatTokens2(summaryTokens)} summaries (${pct(summaryTokens, total)}%)`
19896
+ );
19897
+ const topTypes = [...toolTypeMap.entries()].sort((a, b2) => b2[1] - a[1]).slice(0, 3);
19898
+ if (topTypes.length > 0) {
19899
+ lines.push(` Top tools: ${topTypes.map(([t, n]) => `${t} (${pct(n, total)}%)`).join(", ")}`);
19900
+ }
19901
+ lines.push("");
19902
+ if (blocks.length === 0) {
19903
+ lines.push("COMPRESSED BLOCKS");
19904
+ lines.push(" No compressed blocks.");
19905
+ } else {
19906
+ const totalSummary = blocks.reduce((s3, b2) => s3 + summaryTokensOf(b2, countTokens), 0);
19907
+ const totalEffective = blocks.reduce(
19908
+ (s3, b2) => s3 + effectiveCompressedTokens(b2, state, countTokens),
19909
+ 0
19910
+ );
19911
+ lines.push(
19912
+ `COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens2(totalSummary)} summary, ${formatTokens2(totalEffective)} original)`
19913
+ );
19914
+ const breakdown = tierBreakdown(blocks, countTokens);
19915
+ if (breakdown) lines.push(` Tier usage: ${breakdown}`);
19916
+ lines.push("");
19917
+ const sorted = [...blocks].sort(
19918
+ (a, b2) => effectiveCompressedTokens(b2, state, countTokens) - effectiveCompressedTokens(a, state, countTokens) || b2.createdAt - a.createdAt
19919
+ );
19920
+ for (const block of sorted.slice(0, limit)) {
19921
+ const topic = block.topic ?? "(no topic)";
19922
+ const eff = effectiveCompressedTokens(block, state, countTokens);
19923
+ lines.push(
19924
+ ` ${block.blockId} (${tierLabel(block)}) ${formatTokens2(eff)}\u2192${formatTokens2(summaryTokensOf(block, countTokens))} ${block.effectiveMessageIds.length} msgs "${topic}"`
19925
+ );
19926
+ }
19927
+ }
19928
+ lines.push("");
19929
+ lines.push(
19930
+ `Tip: buildStatusReport({scope:"uncompressed", view:"messages", tool:"${topTool ?? "bash"}"}) for per-message listing`
19931
+ );
19932
+ return lines.join("\n");
19933
+ }
19934
+ function renderUncompressedRanges(visible) {
19935
+ const lines = [];
19936
+ const totalTokens = visible.reduce((s3, m2) => s3 + m2.tokens, 0);
19937
+ lines.push(`UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${visible.length} visible messages`);
19938
+ lines.push("");
19939
+ if (visible.length === 0) {
19940
+ lines.push(" (no uncompressed messages)");
19941
+ return lines.join("\n");
19942
+ }
19943
+ const refNum2 = (ref) => {
19944
+ const m2 = ref.match(/\d+/);
19945
+ return m2 ? parseInt(m2[0], 10) : 0;
19946
+ };
19947
+ const merged = [];
19948
+ for (const m2 of visible) {
19949
+ const num = refNum2(m2.ref);
19950
+ const last = merged[merged.length - 1];
19951
+ if (last && num === last.startNum + last.count) {
19952
+ last.endRef = m2.ref;
19953
+ last.count += 1;
19954
+ last.tokens += m2.tokens;
19955
+ } else {
19956
+ merged.push({ startRef: m2.ref, endRef: m2.ref, startNum: num, count: 1, tokens: m2.tokens, tool: m2.tool });
19957
+ }
19958
+ }
19959
+ for (const r of merged.slice(0, 30)) {
19960
+ const range = r.count === 1 ? r.startRef : `${r.startRef}\u2013${r.endRef}`;
19961
+ lines.push(` ${range} (${r.count} msgs, ${formatTokens2(r.tokens)}${r.count > 1 ? ` (${Math.round(r.tokens / r.count)}/msg)` : ""}) ${r.tool}`);
19962
+ }
19963
+ if (merged.length > 30) {
19964
+ lines.push(` ... and ${merged.length - 30} more ranges`);
19965
+ }
19966
+ return lines.join("\n");
19967
+ }
19968
+ function renderMessageDrilldown(visible, toolFilter, sort, limit) {
19969
+ let filtered = visible;
19970
+ if (toolFilter) filtered = filtered.filter((m2) => m2.tool === toolFilter);
19971
+ if (sort === "time") filtered.sort((a, b2) => a.index - b2.index);
19972
+ else if (sort === "tool") filtered.sort((a, b2) => a.tool.localeCompare(b2.tool) || b2.tokens - a.tokens);
19973
+ else filtered.sort((a, b2) => b2.tokens - a.tokens);
19974
+ const totalTokens = filtered.reduce((s3, m2) => s3 + m2.tokens, 0);
19975
+ const allTokens = visible.reduce((s3, m2) => s3 + m2.tokens, 0);
19976
+ const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens2(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${filtered.length} msgs`;
19977
+ const lines = [header, `Sorted by ${sort}`, ""];
19978
+ const shown = filtered.slice(0, limit);
19979
+ for (const message of shown) {
19980
+ lines.push(` ${message.ref} (${formatTokens2(message.tokens)}) ${message.tool}`);
19981
+ }
19982
+ if (filtered.length > shown.length) {
19983
+ lines.push("");
19984
+ lines.push(`${shown.length} of ${filtered.length} shown.`);
19985
+ }
19986
+ return lines.join("\n");
19987
+ }
19988
+ function renderCompressedDrilldown(blocks, state, sort, limit, countTokens) {
19989
+ let sorted = [...blocks];
19990
+ if (sort === "time") sorted.sort((a, b2) => a.createdAt - b2.createdAt);
19991
+ else if (sort === "age") sorted.sort((a, b2) => b2.survivedCount - a.survivedCount);
19992
+ else
19993
+ sorted.sort(
19994
+ (a, b2) => effectiveCompressedTokens(b2, state, countTokens) - effectiveCompressedTokens(a, state, countTokens) || b2.createdAt - a.createdAt
19995
+ );
19996
+ const totalSummary = sorted.reduce((s3, b2) => s3 + summaryTokensOf(b2, countTokens), 0);
19997
+ const totalEffective = sorted.reduce(
19998
+ (s3, b2) => s3 + effectiveCompressedTokens(b2, state, countTokens),
19999
+ 0
20000
+ );
20001
+ const lines = [
20002
+ `COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens2(totalEffective)} original \u2192 ${formatTokens2(totalSummary)} summary`
20003
+ ];
20004
+ const breakdown = tierBreakdown(sorted, countTokens);
20005
+ if (breakdown) lines.push(`Tier usage: ${breakdown}`);
20006
+ lines.push("");
20007
+ const shown = sorted.slice(0, limit);
20008
+ for (const block of shown) {
20009
+ const nested = block.directBlockIds.length > 0 ? ` nested=[${block.directBlockIds.join(",")}]` : "";
20010
+ const topic = block.topic ?? "(no topic)";
20011
+ const eff = effectiveCompressedTokens(block, state, countTokens);
20012
+ lines.push(
20013
+ ` ${block.blockId} (${tierLabel(block)}) ${formatTokens2(eff)}\u2192${formatTokens2(summaryTokensOf(block, countTokens))} ${block.effectiveMessageIds.length} msgs age=${block.survivedCount} ${block.generation}${nested}`
20014
+ );
20015
+ lines.push(` "${topic}"`);
20016
+ }
20017
+ if (sorted.length > shown.length) {
20018
+ lines.push("");
20019
+ lines.push(`${shown.length} of ${sorted.length} shown.`);
20020
+ }
20021
+ return lines.join("\n");
20022
+ }
20023
+ var substringAlgorithm = {
20024
+ name: "substring",
20025
+ description: "Exact substring counting (original baseline). Predictable, no normalization.",
20026
+ score(docs, query) {
20027
+ const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 0);
20028
+ if (terms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
20029
+ return docs.map((d) => {
20030
+ const haystack = d.text.toLowerCase();
20031
+ let score = 0;
20032
+ for (const term of terms) score += countOccurrences2(haystack, term);
20033
+ return { ref: d.ref, score };
20034
+ });
20035
+ }
20036
+ };
20037
+ function countOccurrences2(haystack, needle) {
20038
+ if (!needle) return 0;
20039
+ return haystack.split(needle).length - 1;
20040
+ }
20041
+ function stem(word) {
20042
+ let w2 = word;
20043
+ if (w2.length <= 3) return w2;
20044
+ if (w2.endsWith("ies")) w2 = w2.slice(0, -3) + "y";
20045
+ else if (w2.endsWith("ses") || w2.endsWith("xes") || w2.endsWith("zes")) w2 = w2.slice(0, -2);
20046
+ else if (w2.endsWith("ches") || w2.endsWith("shes")) w2 = w2.slice(0, -2);
20047
+ else if (w2.endsWith("s") && !w2.endsWith("ss")) w2 = w2.slice(0, -1);
20048
+ if (w2.endsWith("ing") && w2.length > 5) w2 = w2.slice(0, -3);
20049
+ if (w2.endsWith("ed") && w2.length > 4) w2 = w2.slice(0, -2);
20050
+ if (w2.endsWith("ation") && w2.length > 6) w2 = w2.slice(0, -3);
20051
+ else if (w2.endsWith("tion") && w2.length > 5) w2 = w2.slice(0, -4) + "t";
20052
+ else if (w2.endsWith("ion") && w2.length > 4) w2 = w2.slice(0, -3);
20053
+ if (w2.endsWith("ment") && w2.length > 6) w2 = w2.slice(0, -4);
20054
+ if (w2.endsWith("ness") && w2.length > 6) w2 = w2.slice(0, -4);
20055
+ if (w2.endsWith("ly") && w2.length > 4) w2 = w2.slice(0, -2);
20056
+ return w2;
20057
+ }
20058
+ var CJK = /[\u3400-\u9fff\uf900-\ufaff\u3040-\u30ff\uac00-\ud7af]/;
20059
+ var CJK_RUN = new RegExp(`${CJK.source}+`, "g");
20060
+ var LATIN_WORD = /[a-z][a-z0-9_]*[a-z0-9]|[a-z0-9]/g;
20061
+ function tokenize(text, opts = {}) {
20062
+ const lower = text.toLowerCase();
20063
+ const tokens = [];
20064
+ const latin = lower.match(LATIN_WORD) ?? [];
20065
+ for (let w2 of latin) {
20066
+ if (w2.length >= 2) {
20067
+ if (opts.stem) w2 = stem(w2);
20068
+ tokens.push(w2);
20069
+ }
20070
+ }
20071
+ const cjkRuns = lower.match(CJK_RUN) ?? [];
20072
+ for (const run of cjkRuns) {
20073
+ if (run.length === 1) {
20074
+ tokens.push(run);
20075
+ } else {
20076
+ for (let i = 0; i < run.length - 1; i++) tokens.push(run.slice(i, i + 2));
20077
+ for (const ch of run) tokens.push(ch);
20078
+ }
20079
+ }
20080
+ return tokens;
20081
+ }
20082
+ function charBigrams(text) {
20083
+ const grams = [];
20084
+ for (let i = 0; i < text.length - 1; i++) {
20085
+ const pair = text.slice(i, i + 2);
20086
+ if (pair.trim().length === pair.length) grams.push(pair);
20087
+ }
20088
+ return grams;
20089
+ }
20090
+ function tfMap(text, stem2) {
20091
+ const m2 = /* @__PURE__ */ new Map();
20092
+ for (const t of tokenize(text, { stem: stem2 })) m2.set(t, (m2.get(t) ?? 0) + 1);
20093
+ return m2;
20094
+ }
20095
+ var bm25Algorithm = {
20096
+ name: "bm25",
20097
+ description: "BM25 with stemming + CJK bigram tokenization. IR-standard relevance ranking.",
20098
+ score(docs, query) {
20099
+ const N2 = docs.length;
20100
+ const k1 = 1.2;
20101
+ const b2 = 0.75;
20102
+ const parsed = docs.map((d) => {
20103
+ const text = d.text;
20104
+ const tf = tfMap(text, true);
20105
+ let len = 0;
20106
+ for (const v2 of tf.values()) len += v2;
20107
+ return { id: d.ref, tf, len };
20108
+ });
20109
+ const avgdl = parsed.reduce((s3, d) => s3 + d.len, 0) / (N2 || 1);
20110
+ const qTerms = tokenize(query, { stem: true });
20111
+ if (qTerms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
20112
+ const idf = /* @__PURE__ */ new Map();
20113
+ for (const t of new Set(qTerms)) {
20114
+ let df = 0;
20115
+ for (const d of parsed) if (d.tf.has(t)) df++;
20116
+ idf.set(t, Math.log(1 + (N2 - df + 0.5) / (df + 0.5)));
20117
+ }
20118
+ return parsed.map((d) => {
20119
+ let score = 0;
20120
+ for (const t of qTerms) {
20121
+ const f2 = d.tf.get(t) ?? 0;
20122
+ if (f2 === 0) continue;
20123
+ const idfT = idf.get(t) ?? 0;
20124
+ score += idfT * (f2 * (k1 + 1)) / (f2 + k1 * (1 - b2 + b2 * d.len / (avgdl || 1)));
20125
+ }
20126
+ return { ref: d.id, score };
20127
+ });
20128
+ }
20129
+ };
20130
+ var fuzzyAlgorithm = {
20131
+ name: "fuzzy",
20132
+ description: "Character bigram overlap. Typo-tolerant, script-agnostic, high recall.",
20133
+ score(docs, query) {
20134
+ const qTokens = query.toLowerCase().split(/[\s,]+/).filter((t) => t.length >= 4);
20135
+ if (qTokens.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
20136
+ const qGrams = /* @__PURE__ */ new Set();
20137
+ for (const t of qTokens) for (const g2 of charBigrams(t)) qGrams.add(g2);
20138
+ if (qGrams.size === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
20139
+ return docs.map((d) => {
20140
+ const haystack = d.text.toLowerCase();
20141
+ const docGrams = new Set(charBigrams(haystack));
20142
+ let hits = 0;
20143
+ for (const g2 of qGrams) if (docGrams.has(g2)) hits++;
20144
+ return { ref: d.ref, score: hits / qGrams.size };
20145
+ });
20146
+ }
20147
+ };
20148
+ var W_BM25 = 0.7;
20149
+ var W_FUZZY = 0.3;
20150
+ var hybridAlgorithm = {
20151
+ name: "hybrid",
20152
+ description: "Weighted BM25(stem) + fuzzy n-gram. Default \u2014 best precision + recall.",
20153
+ score(docs, query) {
20154
+ const bm = bm25Algorithm.score(docs, query);
20155
+ const fz = fuzzyAlgorithm.score(docs, query);
20156
+ const maxBm = Math.max(...bm.map((r) => r.score), 1e-9);
20157
+ const maxFz = Math.max(...fz.map((r) => r.score), 1e-9);
20158
+ const bmMap = new Map(bm.map((r) => [r.ref, r.score / maxBm]));
20159
+ const fzMap = new Map(fz.map((r) => [r.ref, r.score / maxFz]));
20160
+ return docs.map((d) => ({
20161
+ ref: d.ref,
20162
+ score: W_BM25 * (bmMap.get(d.ref) ?? 0) + W_FUZZY * (fzMap.get(d.ref) ?? 0)
20163
+ }));
20164
+ }
20165
+ };
20166
+ var registry2 = /* @__PURE__ */ new Map();
20167
+ function registerSearchAlgorithm(algo) {
20168
+ registry2.set(algo.name, algo);
20169
+ }
20170
+ registerSearchAlgorithm(substringAlgorithm);
20171
+ registerSearchAlgorithm(bm25Algorithm);
20172
+ registerSearchAlgorithm(fuzzyAlgorithm);
20173
+ registerSearchAlgorithm(hybridAlgorithm);
20174
+
17903
20175
  // src/config.ts
17904
- import { defaultConfig } from "acp-kernel";
17905
20176
  import { readFileSync, existsSync, mkdirSync as mkdirSync2, writeFileSync } from "fs";
17906
20177
  import { dirname } from "path";
17907
20178
 
@@ -18191,7 +20462,6 @@ function parseRouteEntry(v2) {
18191
20462
  import http from "http";
18192
20463
  import fs3 from "fs";
18193
20464
  import { tmpdir as tmpdir2 } from "os";
18194
- import { createCore, renderNudgeText, deactivateBlock as deactivateBlock4 } from "acp-kernel";
18195
20465
 
18196
20466
  // src/registry.ts
18197
20467
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -18409,19 +20679,32 @@ function anthropicToCore(body) {
18409
20679
  role: "tool",
18410
20680
  contentType: "tool-result",
18411
20681
  toolCallId: b2.tool_use_id,
18412
- text
20682
+ text,
20683
+ ...b2.is_error === true ? { toolIsError: true } : {}
18413
20684
  });
18414
20685
  if (b2.cache_control) cacheControls.set(id, b2.cache_control);
18415
20686
  break;
18416
20687
  }
18417
20688
  case "thinking": {
18418
20689
  const base = deriveMessageId("assistant", "reasoning", b2.thinking);
18419
- msgs.push({ id: clusters.next(base), role: "assistant", contentType: "reasoning", text: b2.thinking });
20690
+ msgs.push({
20691
+ id: clusters.next(base),
20692
+ role: "assistant",
20693
+ contentType: "reasoning",
20694
+ text: b2.thinking,
20695
+ ...b2.signature ? { thinkingSignature: b2.signature } : {}
20696
+ });
18420
20697
  break;
18421
20698
  }
18422
20699
  case "image": {
18423
20700
  const base = deriveMessageId(m2.role, "text", "[image]");
18424
- msgs.push({ id: clusters.next(base), role: m2.role, contentType: "text", text: "[image]" });
20701
+ msgs.push({
20702
+ id: clusters.next(base),
20703
+ role: m2.role,
20704
+ contentType: "text",
20705
+ text: "[image]",
20706
+ rawAnthropicBlock: b2
20707
+ });
18425
20708
  break;
18426
20709
  }
18427
20710
  }
@@ -18449,9 +20732,14 @@ function coreToAnthropic(messages, cacheControls) {
18449
20732
  current = { role: target, blocks: [] };
18450
20733
  }
18451
20734
  switch (m2.contentType) {
18452
- case "text":
20735
+ case "text": {
20736
+ if (m2.rawAnthropicBlock) {
20737
+ current.blocks.push(m2.rawAnthropicBlock);
20738
+ break;
20739
+ }
18453
20740
  current.blocks.push({ type: "text", text: m2.text ?? "", ...cc(m2.id) });
18454
20741
  break;
20742
+ }
18455
20743
  case "tool-call":
18456
20744
  current.blocks.push({
18457
20745
  type: "tool_use",
@@ -18466,11 +20754,16 @@ function coreToAnthropic(messages, cacheControls) {
18466
20754
  type: "tool_result",
18467
20755
  tool_use_id: m2.toolCallId ?? "",
18468
20756
  content: m2.text ?? "",
20757
+ ...m2.toolIsError ? { is_error: true } : {},
18469
20758
  ...cc(m2.id)
18470
20759
  });
18471
20760
  break;
18472
20761
  case "reasoning":
18473
- current.blocks.push({ type: "thinking", thinking: m2.text ?? "" });
20762
+ current.blocks.push({
20763
+ type: "thinking",
20764
+ thinking: m2.text ?? "",
20765
+ ...m2.thinkingSignature ? { signature: m2.thinkingSignature } : {}
20766
+ });
18474
20767
  break;
18475
20768
  }
18476
20769
  }
@@ -18499,6 +20792,13 @@ function safeParse(s3) {
18499
20792
  }
18500
20793
  }
18501
20794
 
20795
+ // src/bili-message.ts
20796
+ function parseDataUrl(url) {
20797
+ const m2 = /^data:([^;,]+)(?:;base64)?,(.+)$/i.exec(url);
20798
+ if (!m2) return void 0;
20799
+ return { mediaType: m2[1], base64: m2[2] };
20800
+ }
20801
+
18502
20802
  // src/openai.ts
18503
20803
  function openaiToCore(body) {
18504
20804
  const msgs = [];
@@ -18508,12 +20808,20 @@ function openaiToCore(body) {
18508
20808
  case "system":
18509
20809
  case "developer": {
18510
20810
  const base = deriveMessageId(m2.role, "text", stringContent(m2.content));
18511
- msgs.push({ id: clusters.next(base), role: "system", contentType: "text", text: stringContent(m2.content) });
20811
+ msgs.push({ id: clusters.next(base), role: "system", contentType: "text", text: stringContent(m2.content), originalRole: m2.role });
18512
20812
  break;
18513
20813
  }
18514
20814
  case "user": {
18515
- const base = deriveMessageId("user", "text", stringContent(m2.content));
18516
- msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text: stringContent(m2.content) });
20815
+ const text = stringContent(m2.content);
20816
+ const img = firstImagePart(m2.content);
20817
+ const base = deriveMessageId("user", "text", text);
20818
+ msgs.push({
20819
+ id: clusters.next(base),
20820
+ role: "user",
20821
+ contentType: "text",
20822
+ text,
20823
+ ...img ? { rawOpenaiContent: img.part, imageMediaType: img.mediaType, imageBase64: img.base64 } : {}
20824
+ });
18517
20825
  break;
18518
20826
  }
18519
20827
  case "assistant": {
@@ -18588,9 +20896,20 @@ function coreToOpenai(messages) {
18588
20896
  } else {
18589
20897
  flush();
18590
20898
  if (m2.role === "system") {
18591
- out.push({ role: "system", content: m2.text ?? "" });
20899
+ out.push({ role: m2.originalRole === "developer" ? "developer" : "system", content: m2.text ?? "" });
18592
20900
  } else if (m2.role === "user") {
18593
- out.push({ role: "user", content: m2.text ?? "" });
20901
+ if (m2.rawOpenaiContent || m2.imageBase64) {
20902
+ const parts = [];
20903
+ if (m2.text) parts.push({ type: "text", text: m2.text });
20904
+ if (m2.rawOpenaiContent) {
20905
+ parts.push(m2.rawOpenaiContent);
20906
+ } else if (m2.imageBase64 && m2.imageMediaType) {
20907
+ parts.push({ type: "image_url", image_url: { url: `data:${m2.imageMediaType};base64,${m2.imageBase64}` } });
20908
+ }
20909
+ out.push({ role: "user", content: parts });
20910
+ } else {
20911
+ out.push({ role: "user", content: m2.text ?? "" });
20912
+ }
18594
20913
  } else if (m2.role === "tool") {
18595
20914
  out.push({ role: "tool", tool_call_id: m2.toolCallId ?? "", content: m2.text ?? "" });
18596
20915
  }
@@ -18628,6 +20947,20 @@ function stringContent(content) {
18628
20947
  }
18629
20948
  return "";
18630
20949
  }
20950
+ function firstImagePart(content) {
20951
+ if (!Array.isArray(content)) return void 0;
20952
+ for (const p2 of content) {
20953
+ if (p2 && typeof p2 === "object" && p2.type === "image_url") {
20954
+ const iu = p2.image_url;
20955
+ const url = iu?.url;
20956
+ if (typeof url === "string") {
20957
+ const parsed = parseDataUrl(url);
20958
+ if (parsed) return { part: p2, mediaType: parsed.mediaType, base64: parsed.base64 };
20959
+ }
20960
+ }
20961
+ }
20962
+ return void 0;
20963
+ }
18631
20964
 
18632
20965
  // src/responses.ts
18633
20966
  var OPAQUE_ITEM_TYPES = /* @__PURE__ */ new Set([
@@ -18657,6 +20990,20 @@ function messageContent(c) {
18657
20990
  if (Array.isArray(c)) return c.map(partText).join("\n");
18658
20991
  return "";
18659
20992
  }
20993
+ function findInputImage(c) {
20994
+ if (!Array.isArray(c)) return void 0;
20995
+ for (const p2 of c) {
20996
+ if (p2 && typeof p2 === "object" && p2.type === "input_image") {
20997
+ const url = p2.image_url;
20998
+ if (typeof url === "string") {
20999
+ const parsed = parseDataUrl(url);
21000
+ if (parsed) return { mediaType: parsed.mediaType, base64: parsed.base64 };
21001
+ }
21002
+ return {};
21003
+ }
21004
+ }
21005
+ return void 0;
21006
+ }
18660
21007
  function responsesToCore(body) {
18661
21008
  const msgs = [];
18662
21009
  const systemParts = [];
@@ -18686,8 +21033,18 @@ function responsesToCore(body) {
18686
21033
  if (m2.role === "system" || m2.role === "developer") {
18687
21034
  systemParts.push(text);
18688
21035
  } else if (m2.role === "user") {
21036
+ const img = findInputImage(m2.content);
18689
21037
  const base = deriveMessageId("user", "text", text);
18690
- msgs.push({ id: clusters.next(base), role: "user", contentType: "text", text });
21038
+ msgs.push({
21039
+ id: clusters.next(base),
21040
+ role: "user",
21041
+ contentType: "text",
21042
+ text,
21043
+ ...img ? {
21044
+ rawResponsesItem: it2,
21045
+ ...img.mediaType && img.base64 ? { imageMediaType: img.mediaType, imageBase64: img.base64 } : {}
21046
+ } : {}
21047
+ });
18691
21048
  idx++;
18692
21049
  } else if (m2.role === "assistant") {
18693
21050
  if (text) {
@@ -18778,7 +21135,11 @@ function coreToResponses(messages, customToolCallIds = /* @__PURE__ */ new Set()
18778
21135
  if (m2.role === "system") {
18779
21136
  out.push({ type: "message", role: "developer", content: m2.text ?? "" });
18780
21137
  } else if (m2.role === "user") {
18781
- out.push({ type: "message", role: "user", content: m2.text ?? "" });
21138
+ if (m2.rawResponsesItem) {
21139
+ out.push(m2.rawResponsesItem);
21140
+ } else {
21141
+ out.push({ type: "message", role: "user", content: m2.text ?? "" });
21142
+ }
18782
21143
  } else if (m2.role === "assistant") {
18783
21144
  if (m2.contentType === "text") {
18784
21145
  out.push({ type: "message", role: "assistant", content: m2.text ?? "" });
@@ -18840,15 +21201,11 @@ function conversationSignalResponses(body, headerValue2) {
18840
21201
  return hashId(seed);
18841
21202
  }
18842
21203
 
18843
- // src/session.ts
18844
- import { createInitialState as createInitialState2 } from "acp-kernel";
18845
-
18846
21204
  // src/persist.ts
18847
21205
  import { promises as fs } from "fs";
18848
21206
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
18849
21207
  import { createHash as createHash2 } from "crypto";
18850
21208
  import * as path4 from "path";
18851
- import { createInitialState } from "acp-kernel";
18852
21209
  var PERSIST_VERSION = 2;
18853
21210
  function mergeState(parsed) {
18854
21211
  const fresh = createInitialState();
@@ -18886,6 +21243,12 @@ var SessionStore = class {
18886
21243
  /** Monotonic counter for unique temp filenames within a process. */
18887
21244
  tmpSeq = 0;
18888
21245
  log;
21246
+ /** Per-session write serialization chain. Each writeNow/flushSync chains
21247
+ * onto the previous write for the SAME session, so two concurrent writes
21248
+ * to the same session never race on fs.rename (Windows: EPERM/EBUSY when
21249
+ * two renames target the same file). The promise resolves when this
21250
+ * session's write queue is fully drained. */
21251
+ writeChains = /* @__PURE__ */ new Map();
18889
21252
  constructor(opts) {
18890
21253
  this.dir = opts?.dir ?? defaultDir();
18891
21254
  this.debounceMs = opts?.debounceMs ?? defaultDebounce();
@@ -18989,8 +21352,21 @@ var SessionStore = class {
18989
21352
  this.timers.set(session.id, timer2);
18990
21353
  }
18991
21354
  /** Asynchronously persist a session right now (skips the debounce). Throws
18992
- * on write failure so callers can react (e.g. avoid evicting). */
21355
+ * on write failure so callers can react (e.g. avoid evicting). Serialized
21356
+ * per-session via writeChains so concurrent writes don't race on rename. */
18993
21357
  async writeNow(session) {
21358
+ if (!this.enabled) return;
21359
+ const id = session.id;
21360
+ const prev = this.writeChains.get(id) ?? Promise.resolve();
21361
+ const next = prev.catch(() => {
21362
+ }).then(() => this.writeNowInner(session));
21363
+ this.writeChains.set(id, next);
21364
+ next.finally(() => {
21365
+ if (this.writeChains.get(id) === next) this.writeChains.delete(id);
21366
+ });
21367
+ return next;
21368
+ }
21369
+ async writeNowInner(session) {
18994
21370
  if (!this.enabled) return;
18995
21371
  const record = buildRecord(session);
18996
21372
  const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
@@ -19003,7 +21379,7 @@ var SessionStore = class {
19003
21379
  const data = JSON.stringify(record);
19004
21380
  try {
19005
21381
  await fs.writeFile(tmp, data, "utf8");
19006
- await fs.rename(tmp, file);
21382
+ await renameWithRetry(tmp, file);
19007
21383
  } catch (e) {
19008
21384
  try {
19009
21385
  await fs.unlink(tmp).catch(() => {
@@ -19028,6 +21404,7 @@ var SessionStore = class {
19028
21404
  }
19029
21405
  const record = buildRecord(session);
19030
21406
  const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
21407
+ const data = JSON.stringify(record);
19031
21408
  try {
19032
21409
  mkdirSync3(path4.dirname(file), { recursive: true });
19033
21410
  } catch (e) {
@@ -19035,8 +21412,21 @@ var SessionStore = class {
19035
21412
  }
19036
21413
  const tmp = this.tempPath(session.id);
19037
21414
  try {
19038
- writeFileSync2(tmp, JSON.stringify(record), "utf8");
19039
- renameSync2(tmp, file);
21415
+ writeFileSync2(tmp, data, "utf8");
21416
+ let lastErr;
21417
+ for (let attempt = 0; attempt < 3; attempt++) {
21418
+ try {
21419
+ renameSync2(tmp, file);
21420
+ lastErr = void 0;
21421
+ break;
21422
+ } catch (e) {
21423
+ lastErr = e;
21424
+ const code = e.code;
21425
+ if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") break;
21426
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20 * (attempt + 1));
21427
+ }
21428
+ }
21429
+ if (lastErr) throw lastErr;
19040
21430
  return true;
19041
21431
  } catch (e) {
19042
21432
  this.log("error", `[persist] flushSync FAILED for ${session.id}: ${msg(e)} \u2014 session NOT evicted to prevent loss`);
@@ -19131,6 +21521,21 @@ function isValidRecord(parsed) {
19131
21521
  function msg(e) {
19132
21522
  return e instanceof Error ? e.message : String(e);
19133
21523
  }
21524
+ async function renameWithRetry(src, dest) {
21525
+ let lastErr;
21526
+ for (let attempt = 0; attempt < 3; attempt++) {
21527
+ try {
21528
+ await fs.rename(src, dest);
21529
+ return;
21530
+ } catch (e) {
21531
+ lastErr = e;
21532
+ const code = e.code;
21533
+ if (code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw e;
21534
+ await new Promise((r) => setTimeout(r, 20 * (attempt + 1)));
21535
+ }
21536
+ }
21537
+ throw lastErr;
21538
+ }
19134
21539
  function defaultDir() {
19135
21540
  return sessionsDir();
19136
21541
  }
@@ -19201,7 +21606,7 @@ function getSession(id, meta) {
19201
21606
  meta: { protocol: meta?.protocol, upstreamOrigin: meta?.upstreamOrigin, label: meta?.label },
19202
21607
  stats: { requests: 0, tokensSaved: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0, cacheSamples: 0, lastInputTokens: 0, contextTokens: 0 },
19203
21608
  metadata: {},
19204
- state: createInitialState2(),
21609
+ state: createInitialState(),
19205
21610
  createdAt: Date.now(),
19206
21611
  lastSeen: Date.now(),
19207
21612
  blockContents: /* @__PURE__ */ new Map(),
@@ -19263,7 +21668,6 @@ async function flushAllSessions() {
19263
21668
  }
19264
21669
 
19265
21670
  // src/compress-tool.ts
19266
- import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES } from "acp-kernel";
19267
21671
  var COMPRESS_TOOL_NAME = "compress";
19268
21672
  var ACP_TEXT_OPEN = "<acp_compress>";
19269
21673
  var ACP_TEXT_CLOSE = "</acp_compress>";
@@ -19484,14 +21888,7 @@ var PROXY_TOOL_NAMES = /* @__PURE__ */ new Set([
19484
21888
  ACP_STATUS_TOOL_NAME
19485
21889
  ]);
19486
21890
 
19487
- // src/stream.ts
19488
- import { buildStatusReport, collectBlockContent as collectBlockContent2, estimateTokensFast } from "acp-kernel";
19489
-
19490
21891
  // src/decompress-shared.ts
19491
- import {
19492
- collectBlockContent,
19493
- deactivateBlock
19494
- } from "acp-kernel";
19495
21892
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
19496
21893
  import { dirname as dirname3, join as join2 } from "path";
19497
21894
  import { tmpdir } from "os";
@@ -19597,8 +21994,8 @@ function applyRanges(ranges, ctx) {
19597
21994
  ctx.session.state = res.state;
19598
21995
  for (const b2 of res.state.blocks) {
19599
21996
  if (beforeIds.has(b2.blockId)) continue;
19600
- const full = collectBlockContent2(res.state, b2, ctx.messages, { full: true });
19601
- const one = collectBlockContent2(res.state, b2, ctx.messages, { full: false });
21997
+ const full = collectBlockContent(res.state, b2, ctx.messages, { full: true });
21998
+ const one = collectBlockContent(res.state, b2, ctx.messages, { full: false });
19602
21999
  if (full.count > 0 || one.count > 0) {
19603
22000
  cacheBlockContent(ctx.session, b2.blockId, {
19604
22001
  one: { text: one.text, count: one.count },
@@ -20115,10 +22512,6 @@ function reapOrphanBlocks(session, visible, deactivate) {
20115
22512
  }
20116
22513
 
20117
22514
  // src/compress-loop.ts
20118
- import {
20119
- buildStatusReport as buildStatusReport2,
20120
- estimateTokensFast as estimateTokensFast2
20121
- } from "acp-kernel";
20122
22515
  function executeProxyTool(toolName, args, ctx) {
20123
22516
  if (toolName === "compress") {
20124
22517
  return applyRanges(parseCompressInput(args), ctx);
@@ -20143,7 +22536,7 @@ function executeProxyTool(toolName, args, ctx) {
20143
22536
  ${lines.join("\n\n")}`;
20144
22537
  }
20145
22538
  if (toolName === "acp_status") {
20146
- return buildStatusReport2(ctx.session.state, ctx.messages, estimateTokensFast2);
22539
+ return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
20147
22540
  }
20148
22541
  return `[Unknown proxy tool: ${toolName}]`;
20149
22542
  }
@@ -20476,10 +22869,6 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
20476
22869
  }
20477
22870
 
20478
22871
  // src/compress-loop-anthropic.ts
20479
- import {
20480
- buildStatusReport as buildStatusReport3,
20481
- estimateTokensFast as estimateTokensFast3
20482
- } from "acp-kernel";
20483
22872
  function executeProxyTool2(toolName, args, ctx) {
20484
22873
  if (toolName === "compress") {
20485
22874
  return applyRanges(parseCompressInput(args), ctx);
@@ -20504,7 +22893,7 @@ function executeProxyTool2(toolName, args, ctx) {
20504
22893
  ${lines.join("\n\n")}`;
20505
22894
  }
20506
22895
  if (toolName === "acp_status") {
20507
- return buildStatusReport3(ctx.session.state, ctx.messages, estimateTokensFast3);
22896
+ return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
20508
22897
  }
20509
22898
  return `[Unknown proxy tool: ${toolName}]`;
20510
22899
  }
@@ -20779,10 +23168,6 @@ function routeAnthropicEvent(eventStr, isFirstRound, state, cb) {
20779
23168
  }
20780
23169
 
20781
23170
  // src/compress-loop-responses.ts
20782
- import {
20783
- buildStatusReport as buildStatusReport4,
20784
- estimateTokensFast as estimateTokensFast4
20785
- } from "acp-kernel";
20786
23171
  var TEXT_PROTOCOL = process.env.ACP_COMPRESS_PROTOCOL === "text";
20787
23172
  function extractTextTriggers(text) {
20788
23173
  const calls = [];
@@ -20840,7 +23225,7 @@ function executeProxyTool3(toolName, args, ctx) {
20840
23225
  ${lines.join("\n\n")}`;
20841
23226
  }
20842
23227
  if (toolName === "acp_status") {
20843
- return buildStatusReport4(ctx.session.state, ctx.messages, estimateTokensFast4);
23228
+ return buildStatusReport(ctx.session.state, ctx.messages, estimateTokensFast);
20844
23229
  }
20845
23230
  return `[Unknown proxy tool: ${toolName}]`;
20846
23231
  }
@@ -21347,9 +23732,9 @@ data: ${JSON.stringify({ type: "message_stop" })}
21347
23732
  // src/session-id.ts
21348
23733
  function extractKey(headers) {
21349
23734
  const auth = headers["authorization"];
21350
- if (typeof auth === "string" && auth.length > 0) return auth.trim().toLowerCase();
23735
+ if (typeof auth === "string" && auth.length > 0) return auth.trim();
21351
23736
  const apiKey = headers["x-api-key"];
21352
- if (typeof apiKey === "string" && apiKey.length > 0) return `key:${apiKey.trim().toLowerCase()}`;
23737
+ if (typeof apiKey === "string" && apiKey.length > 0) return `key:${apiKey.trim()}`;
21353
23738
  return "(no-key)";
21354
23739
  }
21355
23740
  function clientConversationHeader(headers) {
@@ -21672,7 +24057,17 @@ async function startServer(opts) {
21672
24057
  }
21673
24058
  return server;
21674
24059
  }
24060
+ function isLoopback(addr) {
24061
+ if (!addr) return false;
24062
+ return addr === "::1" || addr === "127.0.0.1" || addr.startsWith("127.") || addr.startsWith("::ffff:127.");
24063
+ }
21675
24064
  async function handle(req, res, opts, core, config, log2) {
24065
+ const isAdminPath = req.url === "/__bili/" || req.url?.startsWith("/__bili/") || req.url === "/__acp/" || req.url?.startsWith("/__acp/");
24066
+ if (isAdminPath && !isLoopback(req.socket.remoteAddress)) {
24067
+ res.writeHead(403, { "content-type": "application/json" });
24068
+ res.end(JSON.stringify({ error: "management endpoints are loopback-only; access denied for " + (req.socket.remoteAddress ?? "unknown") }));
24069
+ return;
24070
+ }
21676
24071
  if (req.method === "GET" && req.url === "/__bili/stats") return sendStats(res);
21677
24072
  if (req.method === "GET" && req.url === "/") {
21678
24073
  const accept = req.headers.accept ?? "";
@@ -21791,14 +24186,14 @@ function diagNudge(turn, sessionId, tokenCount, limit) {
21791
24186
  const n = turn.nudge;
21792
24187
  if (!n) return `[${sessionId}] nudge: unavailable`;
21793
24188
  const b2 = n.breakdown ?? {};
21794
- const pct = limit > 0 ? `${Math.round(tokenCount / limit * 100)}%` : "?";
24189
+ const pct2 = limit > 0 ? `${Math.round(tokenCount / limit * 100)}%` : "?";
21795
24190
  const growth = b2["growth"] ?? 0;
21796
24191
  const floor = b2["growthFloor"] ?? 0;
21797
24192
  const interval = b2["nudgeGrowthTokens"] ?? 0;
21798
24193
  const pendingT1 = b2["pendingT1"] ?? 0;
21799
24194
  const ref = b2["growthReference"] ?? 0;
21800
24195
  const inject = n.shouldInject ? `INJECT T${n.tier ?? "?"}` : "idle";
21801
- return `[${sessionId}] nudge ${inject}: usage=${pct} (${tokenCount}/${limit}), growth=${growth}/${floor} (ref=${ref}, interval=${interval}), pendingT1=${pendingT1}/${interval}, reason="${n.reason.slice(0, 120)}"`;
24196
+ return `[${sessionId}] nudge ${inject}: usage=${pct2} (${tokenCount}/${limit}), growth=${growth}/${floor} (ref=${ref}, interval=${interval}), pendingT1=${pendingT1}/${interval}, reason="${n.reason.slice(0, 120)}"`;
21802
24197
  }
21803
24198
  function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
21804
24199
  const sessionId = session.id;
@@ -21824,7 +24219,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
21824
24219
  log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
21825
24220
  log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
21826
24221
  processedMessages = turn.messages;
21827
- reapOrphanBlocks(session, msgs, deactivateBlock4);
24222
+ reapOrphanBlocks(session, msgs, deactivateBlock);
21828
24223
  rebuiltMessages = coreToAnthropic(processedMessages, cacheControls);
21829
24224
  systemOut = injectSystem(parsed, opts);
21830
24225
  if (opts.compress.injectTool) {
@@ -21872,7 +24267,7 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
21872
24267
  log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
21873
24268
  log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
21874
24269
  processedMessages = turn.messages;
21875
- reapOrphanBlocks(session, msgs, deactivateBlock4);
24270
+ reapOrphanBlocks(session, msgs, deactivateBlock);
21876
24271
  rebuiltMessages = coreToOpenai(processedMessages);
21877
24272
  const sysParts = [];
21878
24273
  if (shouldInject) sysParts.push(buildCompressSystemPrompt());
@@ -21927,7 +24322,7 @@ function prepareResponses(parsed, req, opts, core, config, log2, session) {
21927
24322
  log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
21928
24323
  log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
21929
24324
  processedMessages = turn.messages;
21930
- reapOrphanBlocks(session, msgs, deactivateBlock4);
24325
+ reapOrphanBlocks(session, msgs, deactivateBlock);
21931
24326
  const conversationItems = coreToResponses(processedMessages, customToolCallIds);
21932
24327
  if (preamble.length > 0) {
21933
24328
  log2("info", `[${sessionId}] preserved ${preamble.length} opaque preamble item(s): ${preamble.map((p2) => p2.type).join(",")}`);
@@ -22324,7 +24719,7 @@ function logMsg(opts, level, msg2) {
22324
24719
  }
22325
24720
 
22326
24721
  // src/update.ts
22327
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, access, constants, rm, cp } from "fs/promises";
24722
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, access, constants, rm, cp, unlink } from "fs/promises";
22328
24723
 
22329
24724
  // node_modules/tar/dist/esm/index.min.js
22330
24725
  import Qr from "events";
@@ -25386,6 +27781,15 @@ async function tryAcquireLock() {
25386
27781
  return null;
25387
27782
  }
25388
27783
  log("info", `[update] stealing stale lock (pid=${existing.pid}, age=${Math.round(age / 1e3)}s, alive=${holderAlive})`);
27784
+ try {
27785
+ await unlink(LOCK_FILE);
27786
+ } catch (e) {
27787
+ const code = e.code;
27788
+ if (code !== "ENOENT") {
27789
+ log("warn", `[update] could not remove stale lock: ${e.message}`);
27790
+ return null;
27791
+ }
27792
+ }
25389
27793
  }
25390
27794
  try {
25391
27795
  await writeFile2(LOCK_FILE, JSON.stringify({ pid, ts: now }), { flag: "wx" });