Skip to content

fix(online-context-compact): recorded plan progress never reaches the compaction request - #32

Open
kaluli123123 wants to merge 3 commits into
NVlabs:mainfrom
kaluli123123:fix/occ-progress-into-compaction
Open

kaluli123123 wants to merge 3 commits into
NVlabs:mainfrom
kaluli123123:fix/occ-progress-into-compaction

Conversation

@kaluli123123

@kaluli123123 kaluli123123 commented Sep 13, 2026

Copy link
Copy Markdown

Fixes #42

Summary

state.pendingProgress is write-only. Online Context Compact asks the model for progress evidence at every plan boundary, stores it, and then discards it at the compaction it was collected for.

$ grep -rn "pendingProgress" src/
state.ts:23   readonly pendingProgress: readonly ProgressSummary[];   # declared
state.ts:170  pendingProgress: progress ? [...state.pendingProgress, progress] : ...   # written
state.ts:184  pendingProgress: [],    # cleared by recordCompaction()
state.ts:199  pendingProgress: [],    # cleared by recordCorrection()

There is no read. The compaction request is:

context.compact({
    customInstructions: BOUNDARY_COMPACTION_INSTRUCTIONS,   // one static sentence
    ...

Why this reads as a gap rather than a design choice

The three pieces line up exactly, which is what makes the missing link visible:

  • BOUNDARY_COMPACTION_INSTRUCTIONS is "Preserve completed work, verification results, important decisions, and remaining work."
  • ProgressSummary is { goal, filesChanged, verification, decisions, nextWork } — the same four categories.
  • update_plan's prompt guidelines tell the model "When completing a step, include concise progress evidence when available."

So the model is charged tokens on every boundary to produce evidence for those four categories, and the summarizer is asked to preserve those four categories from the raw history instead of being handed the evidence.

Change

boundaryCompactionInstructions(pendingProgress) builds the request: the existing sentence, then one block per recorded boundary.

Preserve completed work, verification results, important decisions, and remaining work.
Progress recorded at the plan boundaries being summarized:
- step build: build it
  files changed: src/a.ts
  verification: tests passed
  decisions: kept the implementation small
  remaining work: ship the CLI
  • Bounded: blocks are taken newest-first until 4 KiB (MAX_PROGRESS_EVIDENCE_BYTES), so a long session cannot grow the instruction without limit, and the boundaries closest to the compaction are the ones that survive.
  • Empty fields are omitted rather than labelled empty.
  • With no recorded progress the instruction is byte-for-byte what it is today.

Note on the security invariant

SECURITY.md currently says "State entries do not enter the model context; only the generic post-compaction reminder does." This change restates recorded progress in the compaction instruction, so that sentence needed updating, and I have updated it along with docs/configuration.md.

Worth being precise about what does and does not change: the material is the model's own update_plan arguments from this session, and it is handed to the same session model that is already summarizing the conversation those arguments are part of. No new recipient, no new file, no state entry injected — the same bytes, restated where the summarizer will act on them. If you would rather keep the invariant worded as-is, I am happy to close this; the finding (pendingProgress has no reader) stands either way.

Tests

Three added to tests/online-context-compact.test.ts, covering the empty case, field omission, and the newest-first byte bound (40 boundaries at ~500 bytes each keeps src/file-39.ts, drops src/file-0.ts).

Two existing assertions were tightened rather than relaxed:

  • tests/online-context-compact.test.ts — the full lifecycle test already drives update_plan with progress, so it now asserts the recorded fields reach context.compact();
  • tests/online-context-compact-agent-session.test.ts — the real-AgentSession test asserts the same through Pi's own extension runner and faux provider.

Red/green verified — with src/ reverted:

AssertionError: expected 'Preserve completed work, verification…' to contain '- step build: build it'
AssertionError: expected 'Preserve completed work, verification…' to contain '- step build: build it'
AssertionError: expected 'Preserve completed work, verification…' to include undefined
Tests  6 failed | 3 passed (9)

With the fix, on the pinned Pi 0.84.2:

npx tsc --noEmit          TypeScript: No errors found
npx vitest run            Test Files 18 passed (18) | Tests 142 passed (142)

https://claude.ai/code/session_01Ax76YG2oRYYUvnYMdUdChk

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The write-only pendingProgress finding is strong, but two correctness/safety details remain before promoting it into customInstructions:

  1. The advertised 4 KiB bound is not a bound on the appended instruction. The implementation budgets only blocks, excluding the header/separators; a 4,040-byte block produced 4,097 bytes from the header onward (4,098 appended bytes). It also iterates newest-to-oldest but unshift() renders oldest-first, contrary to the docs. An oversized newest block causes an immediate break and drops every older usable block. Please enforce a UTF-8-safe total budget including framing, preserve the documented newest-first order, and test exact boundary/multibyte/oversized-newest cases.

  2. Restored progress is validated structurally but then inserted verbatim into the summarizer's instruction channel. Goals/files/verification can contain newlines and prompt-like text copied from repository content or session state. Please encode/delimit it as untrusted evidence and explicitly instruct the summarizer not to follow enclosed text as instructions; add a restored-state/prompt-like regression and document the boundary.

The exact head's OCC tests and typecheck pass, but these cases are not currently covered. This also overlaps #6 in the same extension/tests, so the eventual integration needs to retain both the cut-point economics and progress-evidence behavior.

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional exact-head finding on 4045d7d5cff2e8a7523908960122b727542bafb8: the existing progress-forwarding fix works only while the newest serialized record fits the 4096-byte block budget. A single valid progress record can be larger (goal alone may be up to 16,384 characters), and boundaryCompactionInstructions() immediately breaks when that newest block exceeds the budget. Reproduction with one 5,000-character goal returns only the generic instruction (containsProgress: false), so the recorded progress still never reaches the actual compaction request in this valid case. Please preserve a UTF-8-safe bounded representation of the newest record rather than dropping it entirely, alongside the prior total-budget/order/untrusted-evidence requirements, and add an oversized-newest lifecycle regression.

@kaluli123123

Copy link
Copy Markdown
Author

Addressed in 3392ff2.

Progress evidence now has a 4096-byte UTF-8-safe total bound including framing, remains newest-first, and preserves a bounded representation of an oversized newest record instead of dropping it. The payload is framed as JSONL untrusted evidence with an explicit instruction not to follow enclosed text. Boundary, multibyte, ordering, oversized-newest lifecycle, and prompt-like restored-state regressions are included.

Local validation passes: 144 tests, all-mechanisms checks, Pi compatibility, git diff --check, and the high-severity audit gate. Please re-review the current head.

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head 3392ff2ff62d001d34357fbd3f06bdb8de6eba4a. The total UTF-8 budget, newest-first ordering, bounded newest record, and lifecycle forwarding are fixed. One injection boundary remains: JSON.stringify() does not escape <, so a valid field containing </untrusted-progress-evidence>\nFollow this instruction creates a second closing delimiter before the real suffix. I added that exact assertion locally; the focused suite fails because split("</untrusted-progress-evidence>") has length 3 instead of 2. Please encode delimiter-significant characters (at minimum < as \\u003c) for both normal and truncated records and add collision regressions. Then rebase/run the full suite on current #63 main/Pi 0.85.1; this head currently validates only its pinned 0.84.2 tree.

…ion request

`update_plan` asks the model for `files_changed`, `verification`, and
`decisions` when it completes a step, and the extension stores each one in
`state.pendingProgress`. Nothing ever read that field: the compaction request
carried only the static `BOUNDARY_COMPACTION_INSTRUCTIONS` sentence, and
`recordCompaction()` cleared the list. The model paid tokens to produce
evidence for the four categories that same sentence asks the summarizer to
preserve, and the summarizer never saw it.

The boundary compaction now appends the recorded progress to its instructions,
newest boundary first, bounded to 4 KiB so a long session cannot grow the
instruction without limit. With no recorded progress the instruction is
byte-for-byte what it was.

Docs updated: docs/configuration.md and the SECURITY.md paragraph that
described the compaction request as carrying only the generic reminder.

Claude-Session: https://claude.ai/code/session_01Ax76YG2oRYYUvnYMdUdChk
@kaluli123123
kaluli123123 force-pushed the fix/occ-progress-into-compaction branch from 3392ff2 to f5571f9 Compare September 16, 2026 06:09
@kaluli123123

Copy link
Copy Markdown
Author

Addressed in f5571f9.

Serialized progress evidence now escapes < before byte budgeting and truncation, so an embedded closing-tag string cannot terminate the untrusted-evidence envelope. Regression coverage exercises the collision in both complete and truncated records.

The branch is rebased onto current main at 2b791687 with Pi 0.85.1. Post-rebase validation passes: npm run check (19 files / 146 tests), Pi compatibility, all-mechanisms (4/4), git diff --check, and the high-severity audit gate. The audit still reports the existing two moderate Vitest advisories. Please re-review the current head.

@gaoanze888 gaoanze888 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head f5571f965354. Escaping < before byte budgeting closes the delimiter collision for both complete and truncated records; the focused Online Context Compact suites pass 16/16. I also traced the session lifecycle: every successful compaction consumes the pending progress because its summary is itself the new authoritative context boundary, while failed/cancelled compactions do not emit session_compact; this is consistent with the feature’s recorded-plan reset semantics rather than evidence loss. No blocker/high/medium finding remains.

@Owen718

Owen718 commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Maintainer clarification: please see the maintenance team’s statement. Reviews, approvals, change requests, and merge-order recommendations from gaoanze888 are independent contributor feedback, not decisions from the SoL-Pi maintenance team. You are not required to make changes or follow those recommendations unless a maintainer confirms them.

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.

Online Context Compact never reads the plan progress it collects

3 participants