Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 88 additions & 19 deletions src/boundaries.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { activeBlocks, blockById } from "./state.js";
import { isRenderedSummaryMessage, summaryMessageId } from "./prune.js";
import type {
CompressionBlock,
CompressionState,
CoreMessage,
ResolvedBoundary,
Expand Down Expand Up @@ -86,15 +88,25 @@ export function resolveBoundaries(
);
}

const indexByRawId = new Map<string, number>();
const indexByMessageId = new Map<string, number>();
input.messages.forEach((message, index) =>
indexByRawId.set(message.id, index),
indexByMessageId.set(message.id, index),
);

let snappedBoundaries: string[] = [];
const startAnchor = resolveAnchorIndex(start, input.state, indexByRawId, "start");
const startAnchor = resolveAnchorIndex(
start,
input.state,
indexByMessageId,
"start",
);
if (startAnchor.snapped) snappedBoundaries.push(startAnchor.snapped);
const endAnchor = resolveAnchorIndex(end, input.state, indexByRawId, "end");
const endAnchor = resolveAnchorIndex(
end,
input.state,
indexByMessageId,
"end",
);
if (endAnchor.snapped) snappedBoundaries.push(endAnchor.snapped);
let startIndex = startAnchor.index;
let endIndex = endAnchor.index;
Expand All @@ -106,7 +118,11 @@ export function resolveBoundaries(
const messageIds: string[] = [];
for (let index = startIndex; index <= endIndex; index++) {
const message = input.messages[index];
if (message) messageIds.push(message.id);
// Synthetic summary messages are transient view representations, not
// compressible content: exclude them so they never leak into a new
// block's effectiveMessageIds/directMessageIds.
if (message && !isRenderedSummaryMessage(message))
messageIds.push(message.id);
}

const boundaryKind: BoundaryKind =
Expand All @@ -115,7 +131,7 @@ export function resolveBoundaries(
const nestedBlockIds: string[] = [];
const nestedSeen = new Set<string>();
for (const block of activeBlocks(input.state)) {
const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
const anchor = visibleBlockAnchor(block, indexByMessageId);
if (anchor !== null && anchor >= startIndex && anchor <= endIndex) {
if (!nestedSeen.has(block.blockId)) {
nestedSeen.add(block.blockId);
Expand Down Expand Up @@ -145,7 +161,7 @@ interface AnchorResolution {
function resolveAnchorIndex(
boundary: ParsedBoundary,
state: CompressionState,
indexByRawId: Map<string, number>,
indexByMessageId: Map<string, number>,
endpoint: "start" | "end",
): AnchorResolution {
const label = endpoint === "start" ? "startId" : "endId";
Expand All @@ -160,15 +176,15 @@ function resolveAnchorIndex(
`${label}="${boundary.raw}" does not exist in this session (typo or wrong session) — run acp_status for current refs.`,
);
}
const index = indexByRawId.get(rawId);
const index = indexByMessageId.get(rawId);
if (index !== undefined) {
return { index, snapped: null };
}
const owner = activeOwnerAnchor(state, [rawId], indexByRawId);
const owner = activeOwnerAnchor(state, [rawId], indexByMessageId);
if (owner !== null) {
return {
index: owner,
snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block — anchored to that block's summary instead.`,
snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block — anchored to the active block covering it instead.`,
};
}
throw new BoundaryNotFoundError(
Expand All @@ -187,12 +203,16 @@ function resolveAnchorIndex(
);
}
if (block.active) {
const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
const anchor = visibleBlockAnchor(block, indexByMessageId);
if (anchor !== null) {
return { index: anchor, snapped: null };
}
}
const owner = activeOwnerAnchor(state, block.effectiveMessageIds, indexByRawId);
const owner = activeOwnerAnchor(
state,
block.effectiveMessageIds,
indexByMessageId,
);
if (owner !== null) {
return {
index: owner,
Expand All @@ -209,46 +229,95 @@ function resolveAnchorIndex(
throw new BoundaryNotFoundError(
"consumed",
endpoint,
`${label}="b${boundary.numericId}" not found in visible context (block messages consumed by a higher-tier block).`,
`${label}="b${boundary.numericId}" is an active block but none of its content (raw messages or rendered summary) is visible in the current context — run acp_status to verify.`,
);
}

/**
* Snap a consumed anchor to the active block that now owns its content.
* Throwing instead dead-ends compress calls that follow nudge instructions
* with older (already-distilled) refs — the livelock in dog/billion-context-pi#32.
*
* Only TRUE ANCESTORS qualify: an active block that INHERITED the content by
* consuming another block. If the active block DIRECTLY compressed the
* message itself, callers must get the "already compressed — retry with the
* block's bN ref" guidance instead of a silent snap (which would turn a
* message-range retry into a same-tier duplicate block).
*/
function activeOwnerAnchor(
state: CompressionState,
ownedIds: string[],
indexByRawId: Map<string, number>,
indexByMessageId: Map<string, number>,
): number | null {
if (ownedIds.length === 0) return null;
const owned = new Set(ownedIds);
let best: number | null = null;
for (const block of state.blocks) {
if (!block.active) continue;
const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
const inherited = inheritedContentIds(state, block);
let ownsInherited = false;
for (const id of owned) {
if (inherited.has(id)) {
ownsInherited = true;
break;
}
}
if (!ownsInherited) continue;
const anchor = visibleBlockAnchor(block, indexByMessageId);
if (anchor === null) continue;
const ownsContent = block.effectiveMessageIds.some((id) => owned.has(id));
if (ownsContent && (best === null || anchor < best)) {
if (best === null || anchor < best) {
best = anchor;
}
}
return best;
}

/**
* Content a block INHERITED by consuming other blocks: the union of its
* children's effective coverage. Derived from `directBlockIds` rather than
* inferred as effective−direct so imported or rebuilt state shapes cannot
* flip the consumed-vs-snap decision.
*/
function inheritedContentIds(
state: CompressionState,
block: CompressionBlock,
): Set<string> {
const ids = new Set<string>();
for (const childId of block.directBlockIds) {
const child = blockById(state, childId);
if (!child) continue;
for (const id of child.effectiveMessageIds) ids.add(id);
}
return ids;
}

function formatPaddedRef(index: number): string {
return `m${String(index).padStart(5, "0")}`;
}

/**
* Visible anchor index for a block: its rendered summary message if present
* (post-prune views replace raw messages with `acp_summary_bN`), else the
* earliest visible raw message it covers. Without the summary fallback an
* active block whose raws were pruned becomes unresolvable and cannot be
* promoted (dog/billion-context-pi#195).
*/
export function visibleBlockAnchor(
block: CompressionBlock,
indexByMessageId: Map<string, number>,
): number | null {
const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
if (summaryIndex !== undefined) return summaryIndex;
return earliestIndexOfIds(block.effectiveMessageIds, indexByMessageId);
}

export function earliestIndexOfIds(
ids: string[],
indexByRawId: Map<string, number>,
indexByMessageId: Map<string, number>,
): number | null {
let earliest: number | null = null;
for (const id of ids) {
const index = indexByRawId.get(id);
const index = indexByMessageId.get(id);
if (index !== undefined && (earliest === null || index < earliest)) {
earliest = index;
}
Expand Down
Loading
Loading