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

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
@@ -260,14 +260,17 @@ Copyright protects _expression_, not ideas, methods, or algorithms (17 USC §102
260
260
 
261
261
  `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:
262
262
 
263
- ### 8.1 First-user-message pin
263
+ ### 8.1 First-user-message pin (conditional since #489)
264
264
 
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.
265
+ The contract is a **property**, not a specific-message guarantee: the rebuilt wire retains a **leading user message** whenever one can lead — strict providers reject conversations with no user message or that start from an assistant. An _uncovered_ first user survives prune via the ordinary covered check anyway; the pin only bites when the first user is **covered by an active block**, and then only conditionally (`firstUserDropSafe`, `src/prune.ts`):
266
266
 
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.
267
+ - **Drop-safe → dropped.** When the earliest message that would survive _after_ the first user (non-covered, non-rendered-summary) is itself a user, the covered first user drops like any other covered message. This covers both #489 trigger shapes: a divergent resend that shifts an earlier-folded message into the first-user slot, and a direct fold of the opening message. Its coverage now takes effect — the payload stops re-sending while its block stays active.
268
+ - **Degenerate → pinned (residual limitation).** When no user would lead after it (the earliest survivor is an assistant/tool message, or nothing survives), the covered first user stays pinned verbatim next to its rendered summary, exactly as before #489. Dropping it there would hand the provider an assistant-led or user-less conversation; the cost is the pre-existing one (payload re-sends while the block covers it).
269
+
270
+ Pre-#489 the pin was **unconditional**: it ran before the covered check for every covered first user. Combined with `syncBlocks` keeping the block active (its base id is present in the pre-prune view by definition), this voided fold coverage whenever a covered message became the view's first user — the fold reported success but the payload re-sent itself verbatim on every turn (#489, found analyzing billion-context#1869). The fix keeps the property, drops the over-guarantee.
271
+
272
+ - **Why:** deliberate since prune's first implementation (v0.0.2) — protecting the leading-user property is part of the contract, not incidental. What changed in #489 is _which_ message carries the property: the property now moves to the earliest surviving user instead of being welded to the specific first message. Regression-tested by `tests/state-prune.test.ts` ("prune drops covered first user when another user survives", "prune keeps covered first user when no user survives behind"), `tests/orphan-fixes.test.ts` ("prune: first user message pruned when covered (no duplication)" — degenerate assistant-led shape), and `tests/regression-first-user-pin.test.ts` (#489 pipeline A/B, both trigger shapes).
269
273
  - **Consequences for consumers:**
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).
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).
274
+ - Consumers may now observe byte-level disappearance of the first user message after compression whenever another user message survives behind it; post-compress wire assertions must expect the rendered summary alone in that case. Token-size and prefix-cache estimates should drop the pinned-message allowance for those sessions.
275
+ - Degenerate shapes (assistant-led survivors) still carry the pinned message verbatim next to its summary — estimates must keep the allowance there.
276
+ - Boundary resolution is unchanged: `blockVisibleInRange` (`src/boundaries.ts`) treats a block as present in a range via its rendered summary **or** its earliest surviving raw — still required for the degenerate pinned shape, where the summary anchors _before_ the pinned raw.
@@ -1 +1 @@
1
- {"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../src/boundaries.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAKD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAehE;AAED;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,wBAAwB;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;gBAGjC,IAAI,EAAE,SAAS,GAAG,UAAU,EAC5B,QAAQ,EAAE,OAAO,GAAG,KAAK,EACzB,OAAO,EAAE,MAAM;CAQlB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,aAAa,CAiFf;AAsMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,gBAAgB,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,GAAG,IAAI,CAIf;AAOD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAcT;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EAAE,EACb,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,GAAG,IAAI,CASf;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,CAMzE"}
1
+ {"version":3,"file":"boundaries.d.ts","sourceRoot":"","sources":["../src/boundaries.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb;AAKD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAehE;AAED;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,wBAAwB;IACrC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,UAAU,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;gBAGjC,IAAI,EAAE,SAAS,GAAG,UAAU,EAC5B,QAAQ,EAAE,OAAO,GAAG,KAAK,EACzB,OAAO,EAAE,MAAM;CAQlB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,KAAK,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,aAAa,CAiFf;AAsMD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,gBAAgB,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,GAAG,IAAI,CAIf;AAQD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,gBAAgB,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAcT;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EAAE,EACb,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,GAAG,IAAI,CASf;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,CAMzE"}
package/dist/index.js CHANGED
@@ -209,15 +209,6 @@ 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
- }
221
212
  function rebuildMessages(messages, coveredBases, firstUserIndex, anchors) {
222
213
  const safeAnchors = anchors.map((anchor) => ({
223
214
  ...anchor,
@@ -228,16 +219,14 @@ function rebuildMessages(messages, coveredBases, firstUserIndex, anchors) {
228
219
  const anchoredSummaryIds = new Set(
229
220
  anchors.map((anchor) => summaryMessageId(anchor.blockId))
230
221
  );
222
+ const pinFirstUser = firstUserIndex < 0 || !isCovered(messages[firstUserIndex].id, coveredBases) || !firstUserDropSafe(messages, firstUserIndex, coveredBases);
231
223
  for (let index = 0; index < messages.length; index++) {
232
224
  while (pending.length > 0 && pending[0].insertAt === index) {
233
225
  result.push(renderSummary(pending.shift()));
234
226
  }
235
- if (index === firstUserIndex && firstUserIndex >= 0) {
236
- const pinned = messages[index];
237
- if (!isCovered(pinned.id, coveredBases) || !firstUserDropSafe(messages, index, coveredBases)) {
238
- result.push(pinned);
239
- continue;
240
- }
227
+ if (pinFirstUser && index === firstUserIndex) {
228
+ result.push(messages[index]);
229
+ continue;
241
230
  }
242
231
  if (isCovered(messages[index].id, coveredBases)) continue;
243
232
  if (isRenderedSummaryMessage(messages[index]) && anchoredSummaryIds.has(messages[index].id))
@@ -249,6 +238,15 @@ function rebuildMessages(messages, coveredBases, firstUserIndex, anchors) {
249
238
  }
250
239
  return result;
251
240
  }
241
+ function firstUserDropSafe(messages, firstUserIndex, coveredBases) {
242
+ for (let i = firstUserIndex + 1; i < messages.length; i++) {
243
+ const message = messages[i];
244
+ if (isCovered(message.id, coveredBases)) continue;
245
+ if (isRenderedSummaryMessage(message)) continue;
246
+ return message.role === "user";
247
+ }
248
+ return false;
249
+ }
252
250
  function renderSummary(anchor) {
253
251
  const body = anchor.summary.trim();
254
252
  const topicLine = anchor.topic ? `${SUMMARY_HEADER} \u2014 ${anchor.topic}` : SUMMARY_HEADER;