acp-kernel 0.0.82-pr.342.217 → 0.0.82-pr.345.219

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/DESIGN.md CHANGED
@@ -219,19 +219,3 @@ A default `countTokens` ships with the core (word-level + unicode CJK tokenizer,
219
219
  ## 7. Why the algorithm, but not the DCP-derived code, comes here
220
220
 
221
221
  Copyright protects *expression*, not ideas, methods, or algorithms (17 USC §102(b)). The compression *methods* (3-tier, growth cadence, protected filtering) are the author's. This core reimplements them in **fresh expression** — it is not a copy or refactor of DCP-derived files. See [PROVENANCE.md](./PROVENANCE.md) for the per-module origin classification (original-bring / DCP-derived-reimplement / adapter-only).
222
-
223
- ---
224
-
225
- ## 8. Prune Semantics & Wire Invariants
226
-
227
- `prune` rebuilds the visible view every turn: messages covered by active blocks are dropped, and each active block's rendered summary (`[Compressed conversation section]`, id `acp_summary_bN`) is inserted at the block's earliest covered position (pair-safe, per `pairSafeAnchorIndex`). One behavior of the rebuilt view is a load-bearing contract for downstream consumers:
228
-
229
- ### 8.1 First-user-message pin
230
-
231
- The session's **first user message survives prune unconditionally** — even when it is covered by an active block. In `rebuildMessages` (`src/prune.ts`) the pin check runs *before* the covered-by-active-block check, so after compressing a range that includes the first user message, the rebuilt wire contains **both** the rendered summary **and** the first user message verbatim; all other covered messages drop as usual. Only the *first* user message is pinned — later covered user messages are pruned normally.
232
-
233
- - **Why:** strict providers reject conversations with no user message (e.g., Anthropic requires the conversation to start from the user role). Pinning guarantees the rebuilt wire retains at least one user message whenever the input had one. Deliberate since prune's first implementation (v0.0.2) — the ordering of the two checks is part of the contract, not incidental. Regression-tested by `tests/state-prune.test.ts` ("prune preserves first user message even when covered") and `tests/orphan-fixes.test.ts` ("prune: first user message pruned when covered (no duplication)").
234
- - **Consequences for consumers:**
235
- - Consumers cannot assert byte-level disappearance of the first user message after compression; post-compress wire assertions must expect the rendered summary **plus** the pinned message verbatim.
236
- - Token-size and prefix-cache stability estimates must account for the pinned message staying verbatim (it is often the task description and can be long).
237
- - Boundary resolution compensates: `blockVisibleInRange` (`src/boundaries.ts`) treats a block as present in a range via its rendered summary **or** its earliest surviving raw — required precisely because the summary anchors *before* the pinned raw.
package/dist/index.js CHANGED
@@ -1,3 +1,13 @@
1
+ import {
2
+ activeBlocks,
3
+ advanceSurvival,
4
+ allocateBlockId,
5
+ allocateRunId,
6
+ blockById,
7
+ coveredMessageIds,
8
+ createInitialState,
9
+ highestActiveTier
10
+ } from "./chunk-5TQA7ZQM.js";
1
11
  import {
2
12
  COMPRESS_PHILOSOPHY,
3
13
  HOW_TO_COMPRESS_RULES,
@@ -17,16 +27,6 @@ import {
17
27
  thinkingTokenValue,
18
28
  viableRanges
19
29
  } from "./chunk-VPRKYTHW.js";
20
- import {
21
- activeBlocks,
22
- advanceSurvival,
23
- allocateBlockId,
24
- allocateRunId,
25
- blockById,
26
- coveredMessageIds,
27
- createInitialState,
28
- highestActiveTier
29
- } from "./chunk-5TQA7ZQM.js";
30
30
 
31
31
  // src/refs.ts
32
32
  var REF_WIDTH = 5;
@@ -2233,6 +2233,27 @@ function computeProtectedRefs(messages, state, config, countTokens = estimateTex
2233
2233
  }
2234
2234
  return result;
2235
2235
  }
2236
+ function computeOpenTurnMemberIds(messages) {
2237
+ const resultCallIds = /* @__PURE__ */ new Set();
2238
+ for (const msg of messages) {
2239
+ if (msg.contentType === "tool-result" && typeof msg.toolCallId === "string") {
2240
+ resultCallIds.add(msg.toolCallId);
2241
+ }
2242
+ }
2243
+ const openCallIds = /* @__PURE__ */ new Set();
2244
+ for (const msg of messages) {
2245
+ if (msg.id && msg.role === "assistant" && msg.contentType === "tool-call" && typeof msg.toolCallId === "string" && !resultCallIds.has(msg.toolCallId)) {
2246
+ openCallIds.add(msg.id);
2247
+ }
2248
+ }
2249
+ if (openCallIds.size === 0) return /* @__PURE__ */ new Set();
2250
+ const excluded = /* @__PURE__ */ new Set();
2251
+ for (const group of computeTurnGroups(messages)) {
2252
+ if (!group.some((id) => openCallIds.has(id))) continue;
2253
+ for (const id of group) excluded.add(id);
2254
+ }
2255
+ return excluded;
2256
+ }
2236
2257
  function buildCompressibleRanges(messages, state, config, protectedZoneRefs, countTokens = estimateTextTokens) {
2237
2258
  let compressibleMsgs = [];
2238
2259
  const protectedMsgs = [];
@@ -2281,6 +2302,20 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
2281
2302
  skipSinceCompressible = false;
2282
2303
  skipSinceProtected = true;
2283
2304
  }
2305
+ const openTurnIds = computeOpenTurnMemberIds(messages);
2306
+ if (openTurnIds.size > 0) {
2307
+ let gapPending = false;
2308
+ const kept = [];
2309
+ for (const info of compressibleMsgs) {
2310
+ if (openTurnIds.has(info.id)) {
2311
+ gapPending = true;
2312
+ continue;
2313
+ }
2314
+ kept.push(gapPending ? { ...info, gapBefore: true } : info);
2315
+ gapPending = false;
2316
+ }
2317
+ compressibleMsgs = kept;
2318
+ }
2284
2319
  const unfoldedIds = computeIntegrityWithdrawals(
2285
2320
  messages,
2286
2321
  new Set(compressibleMsgs.map((info) => info.id))
@@ -2298,6 +2333,13 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
2298
2333
  }
2299
2334
  compressibleMsgs = kept;
2300
2335
  }
2336
+ const gapCount = compressibleMsgs.reduce(
2337
+ (s, info) => s + (info.gapBefore ? 1 : 0),
2338
+ 0
2339
+ );
2340
+ if (gapCount > 0) {
2341
+ compressibleMsgs = refineSegmentsForFoldability(messages, compressibleMsgs);
2342
+ }
2301
2343
  const compressible = [];
2302
2344
  let cur = null;
2303
2345
  for (const info of compressibleMsgs) {
@@ -2367,6 +2409,41 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
2367
2409
  protected: protectedRanges
2368
2410
  };
2369
2411
  }
2412
+ var MAX_REFINEMENT_PASSES = 8;
2413
+ function refineSegmentsForFoldability(messages, infos) {
2414
+ let current = infos;
2415
+ for (let pass = 0; pass < MAX_REFINEMENT_PASSES; pass++) {
2416
+ const segments = [];
2417
+ for (const info of current) {
2418
+ const seg = segments[segments.length - 1];
2419
+ if (seg === void 0 || info.gapBefore) segments.push([info]);
2420
+ else seg.push(info);
2421
+ }
2422
+ const removed = /* @__PURE__ */ new Set();
2423
+ for (const seg of segments) {
2424
+ const withdrawn = computeIntegrityWithdrawals(
2425
+ messages,
2426
+ new Set(seg.map((info) => info.id))
2427
+ ).withdrawn;
2428
+ for (const info of seg) {
2429
+ if (withdrawn.has(info.id)) removed.add(info.id);
2430
+ }
2431
+ }
2432
+ if (removed.size === 0) break;
2433
+ let gapPending = false;
2434
+ const kept = [];
2435
+ for (const info of current) {
2436
+ if (removed.has(info.id)) {
2437
+ gapPending = true;
2438
+ continue;
2439
+ }
2440
+ kept.push(gapPending ? { ...info, gapBefore: true } : info);
2441
+ gapPending = false;
2442
+ }
2443
+ current = kept;
2444
+ }
2445
+ return current;
2446
+ }
2370
2447
  function mergeBatch(batch) {
2371
2448
  const first = batch[0];
2372
2449
  const last = batch[batch.length - 1];