acp-kernel 0.0.99-pr.487.343 → 0.0.99-pr.490.347

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
@@ -262,10 +262,12 @@ Copyright protects _expression_, not ideas, methods, or algorithms (17 USC §102
262
262
 
263
263
  ### 8.1 First-user-message pin
264
264
 
265
- 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.
265
+ The session's **first user message survives prune when it is covered by an active block — unless dropping it cannot lose the guarantee the pin exists for.** In `rebuildMessages` (`src/prune.ts`) the pin check runs _before_ the covered-by-active-block check, but a covered pin yields when another user message still survives behind it (`firstUserDropSafe`: the earliest non-covered, non-rendered-summary message after the pin must itself be a plain user text core — the only shape the downstream orphan-stripping passes cannot remove). In that case the fold applies fully: the covered first user drops like any other covered message, and the rebuilt wire opens with the surviving user (with the rendered summary anchored in front of it, re-voiced or stripped downstream per mode). Only the _first_ user message is pinned — later covered user messages are pruned normally.
266
266
 
267
- - **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)").
267
+ - **Why:** strict providers reject conversations with no user message (e.g., Anthropic requires the conversation to start from the user role). The pin 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. The original unconditional form was an over-guarantee: it pinned the _specific_ first message rather than the property "a user leads". Once a covered message became the view's first user — the client trimmed earlier history before resending, or the fold targeted the opening message directly — the pin defeated coverage forever: the block stayed active while its payload re-sent itself verbatim every turn, reporting success (billion-context#1869). The conditional yield restores coverage for exactly those shapes while preserving the guarantee everywhere else.
268
+ - **Residual limitation (deliberate):** if no user message survives behind the covered pin (e.g. `[U1-covered, A1, U2-covered…]` where only assistant/tool cores remain), the pin holds and the fold of that range is a no-op on the wire — shipping a non-user-leading frame would reject strict providers, which outweighs the compression gain. Consumers should treat "folded the opening message and nothing user-shaped remains" as a known degenerate case.
268
269
  - **Consequences for consumers:**
269
- - 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.
270
- - 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).
270
+ - Post-compress wire assertions about the first user message depend on what survives behind it: with a surviving user, expect byte-level disappearance plus the rendered summary; in the degenerate case above, expect the pinned message verbatim alongside the summary.
271
+ - Token-size and prefix-cache stability estimates must account for the pinned message staying verbatim in the degenerate case (it is often the task description and can be long).
271
272
  - 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.
273
+ - **Regression-tested by** `tests/state-prune.test.ts` (both the drop and the degenerate-keep shapes), `tests/orphan-fixes.test.ts` ("prune: first user message pruned when covered (no duplication)"), and `tests/issue1476-reid.test.ts` (pinned-echo id/ref stability across post-fold passes).
package/dist/index.js CHANGED
@@ -209,6 +209,15 @@ function pairSafeAnchorIndex(messages, index) {
209
209
  }
210
210
  return safe;
211
211
  }
212
+ function firstUserDropSafe(messages, firstUserIndex, coveredBases) {
213
+ for (let i = firstUserIndex + 1; i < messages.length; i++) {
214
+ const message = messages[i];
215
+ if (isCovered(message.id, coveredBases)) continue;
216
+ if (isRenderedSummaryMessage(message)) continue;
217
+ return message.role === "user" && message.contentType === "text";
218
+ }
219
+ return false;
220
+ }
212
221
  function rebuildMessages(messages, coveredBases, firstUserIndex, anchors) {
213
222
  const safeAnchors = anchors.map((anchor) => ({
214
223
  ...anchor,
@@ -224,8 +233,11 @@ function rebuildMessages(messages, coveredBases, firstUserIndex, anchors) {
224
233
  result.push(renderSummary(pending.shift()));
225
234
  }
226
235
  if (index === firstUserIndex && firstUserIndex >= 0) {
227
- result.push(messages[index]);
228
- continue;
236
+ const pinned = messages[index];
237
+ if (!isCovered(pinned.id, coveredBases) || !firstUserDropSafe(messages, index, coveredBases)) {
238
+ result.push(pinned);
239
+ continue;
240
+ }
229
241
  }
230
242
  if (isCovered(messages[index].id, coveredBases)) continue;
231
243
  if (isRenderedSummaryMessage(messages[index]) && anchoredSummaryIds.has(messages[index].id))