@@ -119,20 +119,61 @@ function readCompactionEntries(h: PiTestHarness): Array<Record<string, unknown>>
119119 . filter ( ( entry ) => entry . type === "compaction" ) ;
120120}
121121
122+ /**
123+ * Count published Pi compartments for a session — the stable post-publish
124+ * checkpoint. Pi compartments are written by historian's atomic transaction
125+ * (`pi-historian-runner.ts:569-582`), so the row count going from 0 → ≥1 is
126+ * a reliable indicator that the publish transaction committed.
127+ */
128+ function readCompartmentCount ( h : PiTestHarness , sessionId : string ) : number {
129+ const db = new Database ( h . contextDbPath ( ) , { readonly : true } ) ;
130+ try {
131+ const row = db
132+ . prepare (
133+ "SELECT COUNT(*) AS c FROM compartments WHERE session_id = ? AND harness = 'pi'" ,
134+ )
135+ . get ( sessionId ) as { c : number } | undefined ;
136+ return row ?. c ?? 0 ;
137+ } finally {
138+ db . close ( ) ;
139+ }
140+ }
141+
122142describe ( "pi compaction marker" , ( ) => {
123- // FIXME(v0.21.6): Test scenario stopped triggering historian publication
124- // after the Phase 2 deferred-marker rewrite. The original test was
125- // designed for eager `appendCompaction()` and worked because the
126- // post-trigger turn alone produced both publish + apply. With the
127- // deferred queue we need publish AND a separate drain pass — but in
128- // this scenario historian is not publishing at all under the new
129- // execute-gating, so no pending blob is ever written. Needs harness-
130- // level investigation (mock matcher? historian-spawn args? Pi 0.74
131- // RPC stdin behavior change?). The drain logic itself is covered by
132- // packages/pi-plugin/src/compaction-marker-manager-pi.test.ts and the
133- // production-side helpers under storage-meta-persisted. Skipping in
134- // v0.21.5 to unblock the release pipeline.
135- it . skip ( "defers native compaction entry through pending blob and drains on next materializing pass" , async ( ) => {
143+ // FIXME(post-v0.21.5): Despite the Oracle-guided rewrite below (which
144+ // waits for the durable compartment row + uses ~90% pressure instead
145+ // of the original ~180% over-spike that routed through emergency
146+ // recovery), this test still fails in our e2e environment. Two
147+ // separate attempts in v0.21.5 release prep:
148+ // 1. Waiting for pending_pi_compaction_marker_state (the transient
149+ // internal queue) → 120s timeout at the wait, because the blob
150+ // is cleared by the next drain pass before the polling loop sees
151+ // it (Oracle: bg_e4d4c044).
152+ // 2. Waiting for compartment row → ALSO 120s timeout but at a
153+ // different waitFor, meaning even the durable post-publish
154+ // checkpoint isn't reaching this scenario. Possible causes:
155+ // Pi 0.74 RPC-mode subagent behavior, mock-provider historian
156+ // matching, or pressure math interacting with the test's
157+ // warmup sequence.
158+ //
159+ // The production drain logic is well-covered by unit tests in
160+ // packages/pi-plugin/src/compaction-marker-manager-pi.test.ts plus
161+ // the integration tests under storage-meta-persisted, and the
162+ // architecture is verified live in user dogfooding.
163+ //
164+ // This skipped test should be revisited as a focused investigation
165+ // (live RPC subagent traces, mock-provider request log, e2e harness
166+ // hooks for historian-publish completion) rather than another
167+ // assertion adjustment.
168+ it . skip ( "defers native compaction entry and drains on next materializing pass" , async ( ) => {
169+ // Pressure math note (Oracle bg_e4d4c044): Pi pressure counts
170+ // input + cacheRead + cacheWrite (`pi-pressure.ts:80-93`).
171+ // The earlier version of this test set BOTH input_tokens AND
172+ // cache_creation_input_tokens to 90_000, which produced ~180%
173+ // pressure against a 100k limit, routing the next pass through the
174+ // ≥95% emergency path. Now we use a single 90_000 input bump with
175+ // zero cache_creation to stay at ~90% — well above the 40% execute
176+ // threshold but below the emergency cliff.
136177 const h = await PiTestHarness . create ( {
137178 modelContextLimit : 100_000 ,
138179 magicContextConfig : {
@@ -173,9 +214,12 @@ describe("pi compaction marker", () => {
173214 }
174215 expect ( sessionId ) . toBeTruthy ( ) ;
175216
217+ // Single-channel pressure spike: ~90% so the next pass crosses
218+ // the 40% execute threshold without entering the ≥95% emergency
219+ // recovery path.
176220 h . mock . setDefault ( {
177221 text : "big" ,
178- usage : { input_tokens : 90_000 , output_tokens : 20 , cache_creation_input_tokens : 90_000 } ,
222+ usage : { input_tokens : 90_000 , output_tokens : 20 , cache_creation_input_tokens : 0 } ,
179223 } ) ;
180224 await h . sendPrompt ( "pi marker trigger turn crosses execute threshold" , { timeoutMs : 60_000 } ) ;
181225
@@ -185,22 +229,25 @@ describe("pi compaction marker", () => {
185229 } ) ;
186230 await h . sendPrompt ( "pi marker post-trigger turn lets historian publish" , { timeoutMs : 60_000 } ) ;
187231
188- // Wait for the pending Pi marker blob to appear (this is the
189- // Phase 2 invariant: historian publish writes the blob INSIDE the
190- // publish transaction). The drain itself hasn't fired yet — that
191- // requires another materializing pass.
232+ // Wait for the durable post-publish checkpoint: a Pi compartment
233+ // row. This is the same signal pi-historian-success.test.ts uses
234+ // (`pi-historian-success.test.ts:139,154`), with the same 300s
235+ // budget Pi historian e2es allow for the slow background
236+ // subagent.
237+ //
238+ // We deliberately do NOT wait for `pending_pi_compaction_marker_state`
239+ // here — that blob is a transient internal queue cleared on the
240+ // next drain pass (`context-handler.ts:3015-3052`), so racing
241+ // against it is unreliable in e2e.
192242 await h . waitFor (
193- ( ) => {
194- const row = readMarkerRow ( h , sessionId ! ) ;
195- return row ?. pending_pi_compaction_marker_state ? row : null ;
196- } ,
197- { timeoutMs : 120_000 , label : "pending_pi_compaction_marker_state blob written" } ,
243+ ( ) => ( readCompartmentCount ( h , sessionId ! ) > 0 ? true : null ) ,
244+ { timeoutMs : 300_000 , label : "Pi historian publishes compartment row" } ,
198245 ) ;
199246
200- // Now trigger the drain by sending another materializing prompt.
201- // Pi's drain fires at end-of-pipeline when deferred-history is
202- // present and history was consumed this pass. This second
203- // post-trigger turn provides exactly that.
247+ // Force one more materializing pass so the deferred drain
248+ // definitely runs. Pi's drain fires at end-of-pipeline when
249+ // deferred-history is present and history was consumed this
250+ // pass; an additional simple prompt guarantees that condition .
204251 h . mock . setDefault ( {
205252 text : "drain-trigger" ,
206253 usage : { input_tokens : 600 , output_tokens : 10 , cache_creation_input_tokens : 0 , cache_read_input_tokens : 600 } ,
@@ -209,9 +256,10 @@ describe("pi compaction marker", () => {
209256 timeoutMs : 60_000 ,
210257 } ) ;
211258
212- // The drain should have applied appendCompaction. Wait for the
213- // JSONL compaction entry to appear AND for the pending blob to
214- // be CAS-cleared.
259+ // Now wait for the JSONL compaction entry. The drain may have
260+ // happened on the post-trigger turn itself (Phase 2's drain
261+ // gating allows it whenever history was consumed in the pass),
262+ // in which case this resolves immediately.
215263 const compactions = await h . waitFor (
216264 ( ) => {
217265 const entries = readCompactionEntries ( h ) ;
@@ -228,20 +276,21 @@ describe("pi compaction marker", () => {
228276 expect ( latest . fromHook ) . toBe ( true ) ;
229277
230278 // X1 fix invariant: firstKeptEntryId MUST be a non-empty string.
231- // This is the assertion that fails pre- fix when the ordinal
232- // counter divergence makes findFirstKeptEntryId return null OR
233- // when the synthetic-user fallback yields "" .
279+ // Pre-X1- fix, Pi's findFirstKeptEntryId ordinal-counter
280+ // divergence vs convertEntriesToRawMessages caused this to
281+ // silently return null in tool-heavy sessions .
234282 expect ( typeof latest . firstKeptEntryId ) . toBe ( "string" ) ;
235283 expect ( ( latest . firstKeptEntryId as string ) . length ) . toBeGreaterThan ( 0 ) ;
236284
237- // Post-drain assertions: the Pi pending blob is CAS-cleared, and
238- // OpenCode's deferred-marker column stays null (that field is
239- // OpenCode-only).
285+ // Drained/clean invariant: both deferred-marker columns are
286+ // null at the end. OpenCode's pending_compaction_marker_state
287+ // is OpenCode-only (Pi never writes there); Pi's own column
288+ // should be CAS-cleared by the drain.
240289 const row = readMarkerRow ( h , sessionId ! ) ;
241290 expect ( row ?. pending_compaction_marker_state ?? null ) . toBeNull ( ) ;
242291 expect ( row ?. pending_pi_compaction_marker_state ?? null ) . toBeNull ( ) ;
243292 } finally {
244293 await h . dispose ( ) ;
245294 }
246- } , 300_000 ) ;
295+ } , 600_000 ) ;
247296} ) ;
0 commit comments