Skip to content

feat(compaction)!: remove summary compaction in favor of verbatim - #1313

Merged
lavaman131 merged 6 commits into
mainfrom
feat/1305-verbatim-only-compaction
Jun 9, 2026
Merged

feat(compaction)!: remove summary compaction in favor of verbatim#1313
lavaman131 merged 6 commits into
mainfrom
feat/1305-verbatim-only-compaction

Conversation

@lavaman131

@lavaman131 lavaman131 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Removes the legacy summary compaction path so all compaction — manual `/compact`, auto threshold/overflow, SDK, RPC, and extension-triggered — flows exclusively through verbatim (deletion-only) compaction via `contextCompact()`. Also ships `ctx.ui.custom()` for graph-visible custom TUI human-in-the-loop prompts in workflows.

Closes #1305. Closes #1309. Rebased on top of the P0 fix from #1311 (`fix(compaction): stabilize deletion planner`), which is already in `main`.

Breaking Changes

Removed Replacement
`compact()` (summary overload) `contextCompact()`
`CompactionResult`, `CompactionPreparation` `ContextCompactionResult`, `ContextCompactionPreparation`
`appendCompaction()`, `prepareCompaction()`, `generateSummary()` `contextCompact()`
`findCutPoint()`, `findTurnStartIndex()`, `CutPointResult` n/a (internal)
`CompactionEntry` `ContextCompactionEntry`
`getLatestCompactionEntry()` `getLatestCompactionBoundaryEntry()`
`CompactionSummaryMessageComponent` n/a (removed message type)
`CompactOptions.customInstructions` n/a
RPC `compact.customInstructions` n/a
`compaction.keepRecentTokens` setting n/a
`/compact [instructions]` slash-command arg `/compact` (no args)

Historical `type:"compaction"` JSONL lines on disk are inert — they are never re-injected into active LLM context.

Key Changes

Compaction (breaking)

  • Runtime: Removed summary-compaction code path (`compaction.ts` reduced from ~710 lines to ~21 lines); all triggers now call `contextCompact()` directly via the verbatim `context-compaction` implementation.
  • Extension hooks:
    • `session_before_compact` now exposes `reason` (`"manual" | "threshold" | "overflow"`), `mode`, and a mutation-safe `ContextCompactionPreparation`. Accepts `{ cancel: true }` or `{ deletionRequest: ContextDeletionRequest }` returns (previously accepted a full `CompactionResult` summary). Empty deletion requests are rejected before any persisted side effect.
    • `session_compact` now receives `reason`, `mode`, `result: ContextCompactionResult`, and `contextCompactionEntry` (previously `CompactionEntry`). Observer errors are routed to the non-fatal extension-error channel and can no longer make a successful, already-persisted compaction report failure.
  • Settings: Removed `compaction.keepRecentTokens`.
  • RPC: `compact` RPC now accepts no arguments.
  • Public API: Removed summary-compaction exports from `src/index.ts` and `src/core/index.ts` (see breaking changes table).
  • UI: Removed `CompactionSummaryMessage` component and rendering path.
  • Internals: Extracted documented `isTaskBearingEntry()` predicate shared with the critical-overflow protection logic; restored `never` assertions in `convertToLlm` and `messageText`; deduplicated planner-fallback into a single `runPlanner()` closure.
  • Docs: `docs/compaction.md` rewritten with verbatim-only behavior, a verbatim-vs-summary comparison table, and refreshed Mermaid diagrams. `docs/extensions.md`, `docs/settings.md`, `docs/session-format.md`, `docs/json.md`, `docs/sessions.md` updated accordingly.
  • Examples: `custom-compaction.ts`, `handoff.ts`, `trigger-compact.ts` updated for the new hook shapes.
  • Tests: Removed `compaction-summary-reasoning.test.ts` and summary-specific coverage; added unconditionally-gated tests for deletion-shaped hooks (cancel, empty-request rejection, immutable-transcript validation, critical-overflow protection).

Workflows: `ctx.ui.custom()` HIL prompts

  • New API: `ctx.ui.custom(factory, options?)` suspends a workflow stage at a graph-visible `awaiting_input` prompt node, renders a custom TUI component built with real Atomic `TUI`/`Theme`/`KeybindingsManager` types, and resumes with the value passed to `done(value)`.
  • Replay: Custom prompts participate in live-memory replay through stable hashed identities (`customIdentityHash`). The optional `replayIdentity` string in `WorkflowCustomUiOptions` lets authors signal semantic changes across callsite-stable widget upgrades.
  • Signals: Honors both the prompt-level `AbortSignal` (via `options.signal`) and run-level abort.
  • Headless/unavailable: Rejects with a clear error in headless mode or when the UI adapter does not implement `custom`; does not silently degrade.
  • Scope: Iteration 1 covers inline graph rendering. `overlay: true` and non-TUI `workflow send` answers return `unsupported` errors; full overlay and out-of-band answer support is tracked for a follow-up.
  • New exports: `WorkflowCustomUiFactory`, `WorkflowCustomUiOptions`, `WorkflowCustomUiComponent`, `WorkflowCustomUiTui`, `WorkflowCustomUiTheme`, `WorkflowCustomUiKeybindings`, `WorkflowCustomUiOverlayOptions`, `WorkflowCustomUiOverlayHandle`.

Validation

  • `bun run typecheck` ✅
  • `bun run lint` ✅
  • `bun run test:unit` ✅
  • `bun run test:integration` ✅
  • Full `coding-agent` Vitest suite ✅ (1369 passed, 40 API-gated skips)

Note: A follow-up PR for #1308 (enhanced overflow eviction of reasoning traces) is stacked on this branch.


Review feedback addressed (2026-06-08)

Rebased onto the latest `main` (now past the `0.8.27` release plus workflow fixes #1316/#1317); changelog/version sections reconciled and `main`'s unreleased entries folded into `0.8.28-alpha.1`. The branch is now conflict-free.

Scope — intentional stacking of #1309. This PR deliberately bundles the compaction removal (#1305) and the workflows `ctx.ui.custom()` HIL feature (#1309). The two changes touch disjoint areas (`packages/coding-agent/src/core/compaction/` vs `packages/workflows/`) and are individually revertible by commit.

Addressed items:

  1. `session_compact` observer isolation — post-commit observer emit is now wrapped so a throwing observer is routed to the non-fatal extension-error channel and cannot make a successful, already-persisted compaction report failure.
  2. `critical_overflow` task-bearing guard — extracted a documented `isTaskBearingEntry()` predicate shared with the critical-overflow protection logic, with tests asserting that deleting all task-bearing entries is rejected.
  3. Exhaustiveness guards — restored `never` assertions in `convertToLlm` and `messageText`. The legacy `compactionSummary` role from upstream `pi-agent-core` is now handled explicitly (inert: excluded from LLM context).
  4. Planner-fallback deduplication — extracted a single `runPlanner()` closure, removing triplicated resolve-auth/run-planner blocks.
  5. Credential-less CI coverage — security-relevant deletion-shaped tests (cancel, empty-request rejection, protected-metadata/immutable-transcript) now run unconditionally; only the planner-fallback test is gated behind `ANTHROPIC_API_KEY`.

@mintlify

mintlify Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bastani 🟢 Ready View Preview Jun 8, 2026, 8:38 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@claude claude Bot changed the title feat(compaction)!: remove summary compaction in favor of verbatim (#1305) feat(compaction)!: remove summary compaction in favor of verbatim Jun 8, 2026
@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review: feat(compaction)! — remove summary compaction in favor of verbatim (#1305)

Thanks for this — a large, well-structured removal. The new deletion-shaped hook contract is clean, the security thinking around the extension-facing preparation snapshot is excellent, and test coverage for the new hooks (cancel / deletion request / empty-request rejection / immutable-transcript validation / offline deletion) is genuinely thorough. Comments below, roughly ordered by importance.

🔴 1. Legacy session reload resurrects the full pre-compaction transcript

This is the one I would most like confirmed before merge.

In buildSessionContext() (session-manager.ts) the old code honored a legacy compaction boundary — it emitted the summary, then only messages from firstKeptEntryId onward. The new code drops that branch entirely and iterates the whole filteredPath with for (const entry of filteredPath) appendMessage(entry).

The compaction entry itself is now inert (good — the summary is no longer injected), but the message entries before firstKeptEntryId are still ancestors on the path, so they now get re-appended into active context. For any session previously summary-compacted, resuming it will restore the entire pre-compaction transcript that summarization had removed AND drop the summary that replaced it.

That is not data loss (the JSONL is append-only), but it can cause a large, surprising context jump — potentially an immediate overflow on load — for existing sessions. The changelog says historical type:compaction lines are inert and not injected, which is true for the summary, but does not capture that the messages it replaced come back.

Suggestions: confirm this is intended; consider a migration (migrateSessionEntries) that converts a legacy compaction boundary into an equivalent context_compaction deletion of the pre-firstKeptEntryId entries (or keep honoring firstKeptEntryId as a deletion boundary in buildSessionContext); and add a test that loads a session containing a legacy compaction entry and asserts the resulting active context (currently uncovered).

🟡 2. isLegacyCompactionSummaryMessage obfuscation

chat-message-renderer.ts uses return message.role === "compaction" + "Summary". The string concatenation exists only to defeat TS narrowing now that compactionSummary is gone from the union. It works but reads like a bug. Prefer an explicit, commented cast: (message.role as string) === "compactionSummary".

🟡 3. Lost exhaustiveness checks

Several never-based exhaustive switches were replaced with silent fall-throughs to handle legacy runtime roles: convertToLlm (default return undefined), messageText (trailing return ""), and addMessageToChat (default break). These are needed because legacy compactionSummary messages can still appear at runtime, but they also silently swallow any genuinely new role added later. Consider keeping the exhaustive never check while branching the known-legacy role explicitly first, so future additions still fail at compile time.

🟡 4. Duplicated planner path in _applyContextVerbatimCompaction

In agent-session.ts, the "extension handlers present but no deletionRequest" branch and the "no extension handlers" branch run identical resolvePlannerAuth() + runContextCompact(...) logic (the same call copied verbatim). Worth extracting a small local runPlanner() helper to dedupe.

🟢 5. deepFreeze(structuredClone(preparation)) cost

Cloning + deep-freezing the whole transcript on every compaction is fine in the common case (only when a session_before_compact handler is registered, and compaction already pays an LLM round-trip). Minor notes: it is O(transcript size) for very large sessions, and structuredClone throws if any message field is ever non-cloneable. Low risk, just flagging.

🟢 6. Dropped coverage: extension throws fallback to planner

The old "continue with default compaction if extension throws" test was removed. The behavior is still preserved by runner.emit() per-handler try/catch (it logs via emitError and leaves result undefined, so the planner runs). Since that is a meaningful guarantee, consider re-adding a small test asserting a throwing session_before_compact handler falls through to planner compaction.

🟢 7. Broadened hasTaskBearingContext

context-compaction.ts — the "would leave no user task" guard now accepts custom, branchSummary, and entryType branch_summary in addition to user. Reasonable given compactionSummary is gone, but it means a session whose only surviving task-bearing context is a branch summary now passes. Confirm that is intended.


Overall a solid, careful change, and the direction (verbatim-only, auditable deletion) is the right one. Item 1 is the only thing I would consider blocking pending confirmation/testing; the rest are cleanups.

🤖 Generated with Claude Code

@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Review: feat(compaction)!: remove summary compaction in favor of verbatim

Reviewed the full diff (74 files). Overall this is a clean, well-documented removal — the breaking-changes table, changelog, and rewritten docs/compaction.md are excellent, and the new extension-hook security posture is genuinely well thought out. A few things worth addressing before merge.

🔒 Security / correctness (positives worth calling out)

  • Validating against the internal snapshot, not the extension-facing clone (agent-session.ts): validateContextDeletionRequest(extensionDeletionRequest, preparation.transcript, …) uses the un-cloned preparation, while extensions only ever see deepFreeze(structuredClone(preparation)). This correctly prevents a hostile/buggy extension from weakening protected flags to delete protected entries. The immutable-transcript test covers it. 👍
  • Empty-request rejection before any side effect (backup/append/rebuild) is the right ordering.
  • Lazy auth resolution so local extension deletionRequests work offline is a nice touch and clearly commented.

🐛 Issues / risks

  1. session_compact observer errors fail an already-committed compaction (agent-session.ts:2103). The event is awaited after the backup is written, the context_compaction entry is persisted, and agent.state.messages is rebuilt. A throwing after-compact observer will reject compact() (and surface as an error/abort in the auto-compaction path) even though compaction fully succeeded and is on disk. session_compact is documented as an observation hook — consider wrapping the emit in try/catch (log and continue) so a misbehaving observer can't make a successful compaction report failure.

  2. Broadened hasTaskBearingContext weakens the "leave a user task" guarantee in critical_overflow (context-compaction.ts:977). It now accepts custom / branchSummary / branch_summary in addition to user. In standard mode user messages are protected anyway, but in critical_overflow mode user entries are deletable (isCriticalOverflowProtectedEntryDeletable permits user). The net effect: under critical overflow a plan could delete every actual user message and still pass validation as long as a custom/branch-summary entry survives. That's probably acceptable (branch summaries carry task context), but it's a behavioral change from the old user || protected compactionSummary rule — worth a deliberate confirmation and ideally a test asserting the intended critical-overflow behavior.

  3. Lost compile-time exhaustiveness checks. convertToLlm (messages.ts) dropped its const _exhaustiveCheck: never = m in the default branch (now return undefined), and messageText (context-compaction.ts) added a trailing return "". Both were exhaustive switches that would have caught a forgotten message role at compile time; now a future role silently falls through to undefined/"". Removing the compactionSummary arm is correct — but consider keeping a never assertion on the remaining union so the next role addition still fails the build instead of silently degrading.

🧹 Maintainability

  1. Triplicated planner-fallback block in _applyContextVerbatimCompaction. The resolvePlannerAuth()if (!auth) return undefinedrunContextCompact(preparation, this.model, auth.apiKey, …) sequence is copy-pasted three times (deletionRequest-absent branch, no-handlers branch). Extracting a single runPlanner() closure would remove ~25 duplicated lines and one future drift risk.

🧪 Test coverage

  1. The new offline-capable tests are gated behind describe.skipIf(!API_KEY) (compaction-extensions.test.ts). The cancel / empty-request-rejection / protected-metadata / immutable-transcript tests all supply a deletionRequest or cancel and never reach the planner, so they don't need credentials — yet the whole describe is skipped without ANTHROPIC_API_KEY. That means the core new validation logic (the security-relevant part of this PR) is not exercised in credential-less CI. Recommend splitting: keep only the "planner fallback continues without a deletion request" test under skipIf(!API_KEY), and move the deletion-shaped tests into an unconditional describe using populateCompactableSession().

❓ Scope

  1. Unrelated workflows feature bundled in (feat(workflows): ctx.ui.custom HIL prompts (#1309) — ~950 lines across packages/workflows/** and test/**). The PR title/description are compaction-only and don't mention #1309. If this is intentional stacking it's fine, but mixing two independent features in one breaking-change PR complicates review and rollback. Worth a note in the description, or splitting if not already merged elsewhere.

Minor

  • compaction.ts is now purely token-metrics helpers (shouldCompact, estimateTokens, etc.); the file comment was updated but the filename "compaction.ts" is now a bit misleading vs. context-compaction.ts. Non-blocking.

Nice work overall — the verbatim-only direction and the hook-isolation design are solid. Items (1) and (5) are the ones I'd most want addressed before merge.

Route compaction documentation, APIs, tests, and runtime paths toward verbatim-only context compaction for #1305.

BREAKING CHANGE: Legacy summary compaction APIs and hook shapes are removed in favor of verbatim context compaction.

Assistant-model: GPT-5.5
Addresses review comments on the verbatim-only compaction change:

- agent-session: isolate session_compact observer errors from the committed
  compaction. The hook fires after backup/persist/rebuild, so a throwing
  observer is now routed to the non-fatal extension-error channel instead of
  rejecting a successful, already-persisted compaction.
- agent-session: extract a single runPlanner() closure, removing the
  triplicated resolve-auth/run-planner fallback blocks.
- messages/context-compaction: restore compile-time exhaustiveness (never)
  guards in convertToLlm and messageText. This surfaced that the upstream
  pi-agent-core AgentMessage union still includes the legacy compactionSummary
  role, now handled explicitly (inert: excluded from LLM context) instead of
  silently falling through.
- context-compaction: extract a documented isTaskBearingEntry() predicate shared
  by the critical-overflow protection and the task-bearing guard; a surviving
  branch summary intentionally satisfies the guard even when every user message
  is evicted under critical overflow.
- tests: add critical-overflow task-bearing assertions; run the credential-less
  deletion-shaped compaction tests unconditionally and gate only the planner
  fallback behind ANTHROPIC_API_KEY.

Assistant-model: Claude Opus 4.8
@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review — PR #1313 (verbatim-only compaction + ctx.ui.custom)

Reviewed the runtime, extension hooks, session-manager, and the workflows custom-UI path against CLAUDE.md conventions. This is a large, well-structured removal — the dead summary path is excised cleanly and the new exhaustiveness guards are a real improvement. Findings are ordered by impact; nothing here is a blocker.

Strengths

  • Compile-time exhaustiveness guards in convertToLlm / messageText (const _exhaustiveCheck: never = m) are the right call — adding a new AgentMessage role will now fail the build instead of silently dropping messages from context. The explicit, documented compactionSummary -> inert handling is clear.
  • session_compact observer error isolation (agent-session.ts): wrapping the post-commit emit in try/catch -> emitError so a throwing observer cannot turn an already-persisted compaction into a reported failure is correct and well-commented.
  • Lazy resolvePlannerAuth so extension-supplied deletionRequests work offline without configured credentials is a nice design, and the manual-throws / auto-returns-undefined split is documented at each call site.
  • Validation against the internal snapshot, not the extension-facing clone — the deepFreeze(structuredClone(...)) defensive copy plus re-validation against preparation.transcript is the right security posture, and it is directly tested (validates extension deletion requests against internal protected metadata).
  • Test coverage for the new hook shapes is genuinely good: cancel, empty-request rejection without persistence, protected-metadata enforcement, and custom-UI replay identity (caller/factory/callsite, label-neutral replay).

Suggestions

(1) Perf — build extensionPreparation only when a handler exists. In _applyContextVerbatimCompaction (agent-session.ts:2022), deepFreeze(structuredClone(preparation)) runs on every compaction, but it is only consumed inside the if (this._extensionRunner.hasHandlers("session_before_compact")) branch at line 2047. For the common no-extension case this deep-clones and freezes the entire transcript for nothing — and compaction fires precisely when the transcript is largest. Move the clone inside the hasHandlers guard.

(2) Robustness — structuredClone can throw DataCloneError. Related to (1): if any entry in preparation ever carries a non-cloneable value (a function, a class instance), structuredClone throws and converts a successful compaction into a hard failure. Transcript entries look like plain data today, so this is latent rather than active — but since it is now on the hot path, a guarding try/catch (or a comment asserting the invariant) would harden it.

(3) Please confirm intent — broadened task-bearing validation. validateContextDeletionRequest now treats custom / branchSummary / branch_summary as task-bearing via the new isTaskBearingEntry, where it previously required a literal user message (or protected compaction summary). The consequence — documented in the predicate comment — is that critical_overflow may now evict every literal user message as long as a custom or branch entry survives. Worth a second look that an extension-injected custom entry is always an acceptable stand-in for the user intent; otherwise a session could end up with no genuine user task in active context. The intent is clearly deliberate, just flagging the behavioral surface.

(4) Minor — type-only dep added to the dependency-light authoring contract. authoring-contract.ts now type-imports KeybindingsManager/Theme from @bastani/atomic and TUI types from @earendil-works/pi-tui. These erase at runtime, but the standalone package-typing surface now needs those packages resolvable for types — the module doc comment was updated to match, and workflow-package-typing.test.ts covers it, so this is just a note to keep that consumer in mind.

(5) Nit — duplicated no-op fallback. The two runPlanner() fallback blocks in _applyContextVerbatimCompaction (no-handlers branch vs. handler-present-but-no-deletionRequest branch) are byte-identical. Readability is fine as-is; collapsing is optional.

(6) Nit — hil-answer-notifications now scans all runs x stages per store notification (footprint-based) instead of diffing previous vs. current snapshots, relying entirely on emitOnce dedup. Correct and simpler, just O(runs x stages) per notify() — fine at current scale.

Conventions
Bun-only workflow respected, no dist/build step added to packages/workflows, raw-TS .js import convention preserved, changelog entries land under [Unreleased]. No new any/unknown leaks in the touched code.

Overall: solid, well-tested change. (1) and (2) are the two I would act on before merge; the rest are confirm/optional.

Generated with Claude Code

… dedupe fallback

Addresses the second-round review on PR #1313 (_applyContextVerbatimCompaction):

- Build the deep-frozen extension snapshot only when a session_before_compact
  handler exists, instead of on every compaction. Compaction fires when the
  transcript is largest, so the common no-extension path no longer deep-clones
  and freezes the whole transcript for nothing.
- Guard structuredClone with try/catch: a non-cloneable entry would otherwise
  raise a raw DataCloneError and turn a viable compaction into a hard failure.
  It now surfaces a clear error. Transcript entries are plain data, so this is a
  latent-invariant guard on the (now hot) path.
- Collapse the two byte-identical runPlanner() fallback blocks into a single
  post-hook `if (!validated)` branch.

Reviewer items 3/4/6 were confirm/notes only: the broadened task-bearing
predicate is the previously-confirmed intended behavior, so no code change.

Assistant-model: Claude Opus 4.8
@lavaman131
lavaman131 merged commit 82fc88d into main Jun 9, 2026
10 checks passed
@lavaman131
lavaman131 deleted the feat/1305-verbatim-only-compaction branch June 9, 2026 04:44
@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Claude Code Review — PR #1313 (verbatim-only compaction + ctx.ui.custom<T>())

Reviewed against base ce871e05. Overall this is a clean, carefully-executed change. The summary-compaction removal is thorough: keepRecentTokens, the runtime path, RPC arg, UI component, and public exports are all removed with no dangling references; the planner-fallback dedup, deep-frozen extension preparation, and unconditional security-relevant tests are real improvements. The workflows ctx.ui.custom<T>() feature is defensively written — abort listeners are disposed, stale-resume is guarded by request-id, and headless/adapter-missing paths reject cleanly without stranding the graph node. Findings below; none are blocking.

Medium

1. Legacy summary-compacted sessions re-inject their full pre-boundary history on resume.
packages/coding-agent/src/core/session-manager.ts (buildSessionContext, ~L495–540). The old code treated a legacy type:"compaction" entry as a cut point: emit the summary, then keep only entries from firstKeptEntryId forward, dropping all raw pre-boundary history. The new code iterates the entire filteredPath and merely treats the legacy entry as inert — but buildContextDeletionFilteredPath does not drop pre-boundary entries, since a legacy compaction entry is not a context_deletion. Net effect: resuming a session that was previously summary-compacted now loads the entire raw history the summary had replaced, discarding the curated summary and risking an immediate context-window overflow on the first turn. It self-heals (the next turn triggers verbatim auto-compaction), but the one-time bloat is a real behavior change for existing on-disk sessions. The "inert metadata" framing in the PR description covers the boundary record, but the boundary previously also bounded context — that bounding is silently lost. No test loads a legacy compaction session; worth adding one.

2. branchEntries passed to session_before_compact is the live, mutable branch array, bypassing the deep-freeze isolation.
agent-session.ts (~L2068) passes branchEntries: pathEntries, where pathEntries = this.sessionManager.getBranch() returns the live entry objects. The PR deliberately deepFreeze(structuredClone(preparation)) so extensions cannot mutate protection metadata — but a handler can still freely mutate branchEntries[i].message / .protected, corrupting the in-memory tree used by the subsequent buildSessionContext() rebuild and by what is persisted. This partially undermines the isolation guarantee the freeze was added for. Consider freezing (or cloning) branchEntries too, or documenting it as read-only. Untested.

Low

3. session_compact observer isolation is real but likely redundant — and untested. agent-session.ts (~L2140) wraps the post-commit emit in try/catch routing throws to emitError, which is correct and matches the PR's headline claim. But ExtensionRunner.emit() already swallows non-"before" handler throws internally, so the outer guard can essentially never fire. Either way the guarantee holds — but since this is a stated goal of the review-feedback commit, add a test that registers a throwing session_compact handler and asserts compaction still reports success.

4. "Session compacted N times" status is now dead for all new compactions. interactive-mode.ts:3938-3940 filters e.type === "compaction" (legacy only), never context_compaction. Since context_compaction is now the sole path, this status message will never appear again. Cosmetic, but trivially fixable.

5. deepFreeze has no cycle guard. agent-session.ts:118-126 recurses via Object.values unbounded; structuredClone preserves cycles, so a cyclic entry would hang. Latent only (transcript entries are acyclic plain data today), but cheap to harden with a visited-set.

6. (workflows) Anonymous-factory replay identity can collide. runs/foreground/executor.ts (~L380–390) — when a factory has no replayIdentity and an empty .name, identity falls back to Function.prototype.toString source text. Two distinct closures built from the same source at the same callsite hash identically, so a continuation replay could hand back a sibling prompt's stale answer. Mitigated by the documented replayIdentity option and the callsite hash, so it's a sharp edge rather than a bug — but consider warning when falling back to source-text identity.

Test coverage

Strong additions for cancel / empty-request rejection / protected-metadata enforcement / offline-extension deletion / auth fallback (compaction), and brokered-node / replay-identity / signal-cancel / adapter-delegation (workflows). Gaps worth closing: (a) loading a legacy type:"compaction" session [#1]; (b) throwing session_compact observer [#3]; (c) branchEntries live-mutation [#2]; (d) overlay: true rejection and run-level (vs options.signal) abort of a pending custom prompt.


Automated review. Findings #1, #2, and #4 were verified against the source; the rest are reasoned from the diff. Treat as advisory.

lavaman131 added a commit that referenced this pull request Jun 29, 2026
)

* feat(compaction)!: remove summary compaction

Route compaction documentation, APIs, tests, and runtime paths toward verbatim-only context compaction for #1305.

BREAKING CHANGE: Legacy summary compaction APIs and hook shapes are removed in favor of verbatim context compaction.

Assistant-model: GPT-5.5

* chore(release): prepare 0.8.28-alpha.1

* feat(workflows): ctx.ui.custom HIL prompts (#1309) — workflows-only, drift removed

* docs(changelog): organize 0.8.27-alpha.1 (#1311) and 0.8.28-alpha.1 (1305+1309) sections

* refactor(compaction): address PR #1313 review feedback

Addresses review comments on the verbatim-only compaction change:

- agent-session: isolate session_compact observer errors from the committed
  compaction. The hook fires after backup/persist/rebuild, so a throwing
  observer is now routed to the non-fatal extension-error channel instead of
  rejecting a successful, already-persisted compaction.
- agent-session: extract a single runPlanner() closure, removing the
  triplicated resolve-auth/run-planner fallback blocks.
- messages/context-compaction: restore compile-time exhaustiveness (never)
  guards in convertToLlm and messageText. This surfaced that the upstream
  pi-agent-core AgentMessage union still includes the legacy compactionSummary
  role, now handled explicitly (inert: excluded from LLM context) instead of
  silently falling through.
- context-compaction: extract a documented isTaskBearingEntry() predicate shared
  by the critical-overflow protection and the task-bearing guard; a surviving
  branch summary intentionally satisfies the guard even when every user message
  is evicted under critical overflow.
- tests: add critical-overflow task-bearing assertions; run the credential-less
  deletion-shaped compaction tests unconditionally and gate only the planner
  fallback behind ANTHROPIC_API_KEY.

Assistant-model: Claude Opus 4.8

* refactor(compaction): lazy extension snapshot, guard structuredClone, dedupe fallback

Addresses the second-round review on PR #1313 (_applyContextVerbatimCompaction):

- Build the deep-frozen extension snapshot only when a session_before_compact
  handler exists, instead of on every compaction. Compaction fires when the
  transcript is largest, so the common no-extension path no longer deep-clones
  and freezes the whole transcript for nothing.
- Guard structuredClone with try/catch: a non-cloneable entry would otherwise
  raise a raw DataCloneError and turn a viable compaction into a hard failure.
  It now surfaces a clear error. Transcript entries are plain data, so this is a
  latent-invariant guard on the (now hot) path.
- Collapse the two byte-identical runPlanner() fallback blocks into a single
  post-hook `if (!validated)` branch.

Reviewer items 3/4/6 were confirm/notes only: the broadened task-bearing
predicate is the previously-confirmed intended behavior, so no code change.

Assistant-model: Claude Opus 4.8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support custom TUI widget prompts (ctx.ui.custom) in workflow human-in-the-loop stages Remove summary compaction in favor of verbatim compaction

1 participant