# proposed compaction pseudocode

```ts
type SessionState = {
  compactSummary: CompactSummary | null;
  activeMessages: Message[];
  compactContext: CompactContext | null;
  lastReportedPromptTokens: number | null;
};

function runAgentCycle(session: SessionState) {
  while (true) {
    const submissionContext = buildSubmissionContext(session);

    if (!decideNeedCompaction(submissionContext)) {
      const response = submit(submissionContext);

      if (response.ok) {
        persistReportedTruth(session, submissionContext, response);
      }

      return response;
    }

    const compactionResult = compactIfNeeded(session, submissionContext);

    if (!compactionResult.ok) {
      return invalidContextState(compactionResult.reason);
    }

    applyCompactionResult(session, compactionResult);
  }
}

function buildSubmissionContext(session: SessionState): SubmissionContext {
  const serviceContext = getServiceContext();
  const latestExternalRequest = findLatestExternalRequest(
    session.activeMessages,
  );

  return {
    compactSummary: session.compactSummary,
    activeMessages: session.activeMessages,
    compactContext: session.compactContext,
    serviceContext,
    latestExternalRequest,
    reportedPromptTruth: session.lastReportedPromptTokens,
  };
}

// The provider-reported prompt token count already represents the full prompt
// that was actually submitted on the last successful turn.
//
// So this compaction decision is not based on a synthetic whole-request
// estimate. The only thing we need to project is the known next-turn growth
// that happened after that reported checkpoint, such as:
// - newly appended tool results
// - newly generated summary text after a compaction step
// - reserved space for the next model output
//
// In other words:
// current projected prompt load
//   = last reported full-prompt tokens
//   + post-checkpoint growth we already know about
//
// One caveat: the prompt also has dynamic service-context sections. If those
// sections grew since the last successful submission, the old reported total can
// still drift low relative to the next real request. So a split chosen from
// reported checkpoints is a strong starting point, not an infallible guarantee.
//
// If that projected load crosses the current prompt limit for the next
// submission, compaction is required.
//
// Important: this limit is not a fixed compile-time constant. It can change
// when the user changes model selection or context-size settings, so every
// cycle must compare against the current runtime-configured limit.
function decideNeedCompaction(context: SubmissionContext): boolean {
  return exceedsProjectedPromptLimit(context);
}

function compactIfNeeded(
  session: SessionState,
  context: SubmissionContext,
): CompactionResult {
  const compactableRange = getCompactableLiveRange(
    context.activeMessages,
    context.compactContext,
  );

  if (compactableRange.isEmpty) {
    return { ok: false, reason: 'No live range remains for compaction' };
  }

  const splitCandidates = buildCompactionSplitCandidates(compactableRange);

  if (splitCandidates.length === 0) {
    return { ok: false, reason: 'No usable prefix split exists' };
  }

  let emptyResponseCount = 0;

  for (const split of takeFirst(splitCandidates, 3)) {
    const nextSummary = runCompaction({
      previousSummary: context.compactSummary,
      prefixMessages: split.prefix,
      latestExternalRequest: context.latestExternalRequest,
      serviceContext: context.serviceContext,
    });

    if (!nextSummary.ok) {
      return { ok: false, reason: nextSummary.reason };
    }

    if (isEmptyCompactionSummary(nextSummary.summary)) {
      emptyResponseCount += 1;

      if (emptyResponseCount >= 3) {
        return {
          ok: false,
          reason: 'Compaction returned empty summary 3 times',
        };
      }

      continue;
    }

    return {
      ok: true,
      nextSummary: nextSummary.summary,
      retainedTail: invalidateTailCheckpoints(split.tail),
      compactContext: {
        fromId: firstMessageId(split.prefix),
        toId: lastMessageId(split.prefix),
        sourceMessageCount: split.prefix.length,
      },
    };
  }

  return { ok: false, reason: 'No compaction split succeeded within 3 tries' };
}

// Dynamic service-context growth can make the latest reported checkpoint split
// still too optimistic. So we start from the newest usable checkpoint-based
// split, then progressively back off toward older/safer prefix boundaries.
//
// The caller only tries the first few candidates (currently 3).
function buildCompactionSplitCandidates(
  compactableRange: Message[],
): { prefix: Message[]; tail: Message[] }[] {
  const checkpointSplits = findCheckpointBackoffSplits(compactableRange);

  if (checkpointSplits.length > 0) {
    return checkpointSplits;
  }

  const fallbackSplit = findFallbackFifoPrefix(compactableRange);
  return fallbackSplit == null ? [] : [fallbackSplit];
}

// After prefix compaction, any promptTokens that still remain on the retained
// tail are stale full-prompt measurements from a pre-compaction epoch.
//
// They can stay in history if desired, but they must not remain usable as
// compaction checkpoints for the next epoch. This applies even when the tail
// contains multiple messages.
function invalidateTailCheckpoints(retainedTail: Message[]): Message[] {
  return retainedTail.map((message) => ({
    ...message,
    promptTokens: null,
  }));
}

function submit(context: SubmissionContext): SubmitResult {
  return submitRequest({
    serviceContext: context.serviceContext,
    compactSummary: context.compactSummary,
    activeMessages: context.activeMessages,
  });
}

function persistReportedTruth(
  session: SessionState,
  context: SubmissionContext,
  response: SubmitResult,
) {
  const checkpointTargetId = selectLastSubmittedRealInputMessageId(
    context.activeMessages,
  );

  if (checkpointTargetId != null && response.promptTokensReported != null) {
    persistPromptTokenCheckpoint({
      sessionId: currentSessionId(),
      messageId: checkpointTargetId,
      promptTokens: response.promptTokensReported,
    });
  }

  session.lastReportedPromptTokens = response.promptTokensReported ?? null;
}

function applyCompactionResult(
  session: SessionState,
  result: SuccessfulCompactionResult,
) {
  session.compactSummary = result.nextSummary;
  session.activeMessages = result.retainedTail;
  session.compactContext = result.compactContext;
  session.lastReportedPromptTokens = null;
}
```

## required high-level contracts

1. `compactIfNeeded()` must absorb a **causal prefix** into the summary.
2. `retainedTail` must preserve **original order**.
3. Tool ownership must survive compaction; never retain orphan `tool` messages without their owning assistant tool-call message.
4. Reported provider prompt tokens are the source of truth after successful submit.
5. Checkpoint stamping must target the last submitted **real** input message, not internal synthetic scaffolding.
6. The allowed prompt limit is runtime-configured and user-adjustable, so checkpoint reuse must always be evaluated against the current limit, not a historical constant.
7. After compaction, any `promptTokens` remaining on the retained tail must be invalidated for checkpoint reuse because they describe a pre-compaction full prompt, not the new epoch.
8. Dynamic service context can make prior reported prompt totals drift, so checkpoint-based split selection must support bounded backoff retry instead of assuming the newest checkpoint always fits.
9. Compaction should try at most 3 split candidates per cycle, and should fail closed if it receives an empty summary 3 times.
10. After compaction, `lastReportedPromptTokens` must be explicitly invalidated to `null`; only a later successful submit may establish new prompt-token truth.
