@@ -110,16 +110,29 @@ describe("maybeCompactCodexAppServerSession", () => {
110110 method : "thread/compacted" ,
111111 params : { threadId : "thread-1" , turnId : "turn-1" } ,
112112 } ) ;
113+ fake . emit ( {
114+ method : "thread/tokenUsage/updated" ,
115+ params : {
116+ threadId : "thread-1" ,
117+ tokenUsage : {
118+ last_token_usage : {
119+ total_tokens : 27_170 ,
120+ } ,
121+ } ,
122+ } ,
123+ } ) ;
113124 const result = requireCompactResult ( await pendingResult ) ;
114125
115126 expect ( result . ok ) . toBe ( true ) ;
116127 expect ( result . compacted ) . toBe ( true ) ;
117128 expect ( result . result ?. tokensBefore ) . toBe ( 123 ) ;
129+ expect ( result . result ?. tokensAfter ) . toBe ( 27_170 ) ;
118130 const details = compactDetails ( result ) ;
119131 expect ( details . backend ) . toBe ( "codex-app-server" ) ;
120132 expect ( details . threadId ) . toBe ( "thread-1" ) ;
121133 expect ( details . signal ) . toBe ( "thread/compacted" ) ;
122134 expect ( details . turnId ) . toBe ( "turn-1" ) ;
135+ expect ( details . tokenUsageSource ) . toBe ( "thread/tokenUsage/updated" ) ;
123136 } ) ;
124137
125138 it ( "blocks native app-server compaction when the current OpenClaw session is sandboxed" , async ( ) => {
@@ -137,7 +150,73 @@ describe("maybeCompactCodexAppServerSession", () => {
137150 expect ( fake . request ) . not . toHaveBeenCalled ( ) ;
138151 } ) ;
139152
140- it ( "accepts native context-compaction item completion as success" , async ( ) => {
153+ it ( "uses native token usage that arrives before compaction completion" , async ( ) => {
154+ const fake = createFakeCodexClient ( ) ;
155+ setCodexAppServerClientFactoryForTest ( async ( ) => fake . client ) ;
156+ const sessionFile = await writeTestBinding ( ) ;
157+
158+ const pendingResult = startCompaction ( sessionFile , { currentTokenCount : 123 } ) ;
159+ await vi . waitFor ( ( ) => {
160+ expect ( fake . request ) . toHaveBeenCalledWith ( "thread/compact/start" , { threadId : "thread-1" } ) ;
161+ } ) ;
162+
163+ fake . emit ( {
164+ method : "thread/tokenUsage/updated" ,
165+ params : {
166+ threadId : "thread-1" ,
167+ tokenUsage : {
168+ last_token_usage : {
169+ total_tokens : 18_004 ,
170+ } ,
171+ } ,
172+ } ,
173+ } ) ;
174+ fake . emit ( {
175+ method : "thread/compacted" ,
176+ params : { threadId : "thread-1" , turnId : "turn-1" } ,
177+ } ) ;
178+ const result = requireCompactResult ( await pendingResult ) ;
179+
180+ expect ( result . ok ) . toBe ( true ) ;
181+ expect ( result . compacted ) . toBe ( true ) ;
182+ expect ( result . result ?. tokensAfter ) . toBe ( 18_004 ) ;
183+ expect ( compactDetails ( result ) . tokenUsageSource ) . toBe ( "thread/tokenUsage/updated" ) ;
184+ } ) ;
185+
186+ it ( "accepts native current token usage with a total alias" , async ( ) => {
187+ const fake = createFakeCodexClient ( ) ;
188+ setCodexAppServerClientFactoryForTest ( async ( ) => fake . client ) ;
189+ const sessionFile = await writeTestBinding ( ) ;
190+
191+ const pendingResult = startCompaction ( sessionFile , { currentTokenCount : 123 } ) ;
192+ await vi . waitFor ( ( ) => {
193+ expect ( fake . request ) . toHaveBeenCalledWith ( "thread/compact/start" , { threadId : "thread-1" } ) ;
194+ } ) ;
195+
196+ fake . emit ( {
197+ method : "thread/tokenUsage/updated" ,
198+ params : {
199+ threadId : "thread-1" ,
200+ tokenUsage : {
201+ last : {
202+ total : 16_384 ,
203+ } ,
204+ } ,
205+ } ,
206+ } ) ;
207+ fake . emit ( {
208+ method : "thread/compacted" ,
209+ params : { threadId : "thread-1" , turnId : "turn-1" } ,
210+ } ) ;
211+ const result = requireCompactResult ( await pendingResult ) ;
212+
213+ expect ( result . ok ) . toBe ( true ) ;
214+ expect ( result . compacted ) . toBe ( true ) ;
215+ expect ( result . result ?. tokensAfter ) . toBe ( 16_384 ) ;
216+ expect ( compactDetails ( result ) . tokenUsageSource ) . toBe ( "thread/tokenUsage/updated" ) ;
217+ } ) ;
218+
219+ it ( "accepts native context-compaction item completion with unknown token count as success" , async ( ) => {
141220 const fake = createFakeCodexClient ( ) ;
142221 setCodexAppServerClientFactoryForTest ( async ( ) => fake . client ) ;
143222 const sessionFile = await writeTestBinding ( ) ;
@@ -158,11 +237,44 @@ describe("maybeCompactCodexAppServerSession", () => {
158237 const result = requireCompactResult ( await pendingResult ) ;
159238 expect ( result . ok ) . toBe ( true ) ;
160239 expect ( result . compacted ) . toBe ( true ) ;
240+ expect ( result . result ?. tokensAfter ) . toBeUndefined ( ) ;
161241 const details = compactDetails ( result ) ;
162242 expect ( details . signal ) . toBe ( "item/completed" ) ;
163243 expect ( details . itemId ) . toBe ( "compact-1" ) ;
164244 } ) ;
165245
246+ it ( "does not treat zero native token usage as an authoritative post-compaction count" , async ( ) => {
247+ const fake = createFakeCodexClient ( ) ;
248+ setCodexAppServerClientFactoryForTest ( async ( ) => fake . client ) ;
249+ const sessionFile = await writeTestBinding ( ) ;
250+
251+ const pendingResult = startCompaction ( sessionFile , { currentTokenCount : 123 } ) ;
252+ await vi . waitFor ( ( ) => {
253+ expect ( fake . request ) . toHaveBeenCalledWith ( "thread/compact/start" , { threadId : "thread-1" } ) ;
254+ } ) ;
255+ fake . emit ( {
256+ method : "thread/compacted" ,
257+ params : { threadId : "thread-1" , turnId : "turn-1" } ,
258+ } ) ;
259+ fake . emit ( {
260+ method : "thread/tokenUsage/updated" ,
261+ params : {
262+ threadId : "thread-1" ,
263+ tokenUsage : {
264+ last_token_usage : {
265+ total_tokens : 0 ,
266+ } ,
267+ } ,
268+ } ,
269+ } ) ;
270+
271+ const result = requireCompactResult ( await pendingResult ) ;
272+ expect ( result . ok ) . toBe ( true ) ;
273+ expect ( result . compacted ) . toBe ( true ) ;
274+ expect ( result . result ?. tokensAfter ) . toBeUndefined ( ) ;
275+ expect ( compactDetails ( result ) . tokenUsageSource ) . toBeUndefined ( ) ;
276+ } ) ;
277+
166278 it ( "reuses the bound auth profile for native compaction" , async ( ) => {
167279 const fake = createFakeCodexClient ( ) ;
168280 let seenAuthProfileId : string | undefined ;
@@ -185,6 +297,39 @@ describe("maybeCompactCodexAppServerSession", () => {
185297 expect ( seenAuthProfileId ) . toBe ( "openai-codex:work" ) ;
186298 } ) ;
187299
300+ it ( "reports missing thread bindings as failed native compaction" , async ( ) => {
301+ const sessionFile = path . join ( tempDir , "missing-binding.jsonl" ) ;
302+
303+ const result = requireCompactResult (
304+ await startCompaction ( sessionFile , { currentTokenCount : 123 } ) ,
305+ ) ;
306+
307+ expect ( result . ok ) . toBe ( false ) ;
308+ expect ( result . compacted ) . toBe ( false ) ;
309+ expect ( result . reason ) . toBe ( "no codex app-server thread binding" ) ;
310+ expect ( result . failure ?. reason ) . toBe ( "missing_thread_binding" ) ;
311+ expect ( result . result ) . toBeUndefined ( ) ;
312+ } ) ;
313+
314+ it ( "clears stale thread bindings and reports failed native compaction" , async ( ) => {
315+ const fake = createFakeCodexClient ( ) ;
316+ fake . request . mockRejectedValueOnce ( new Error ( "thread not found: thread-1" ) ) ;
317+ setCodexAppServerClientFactoryForTest ( async ( ) => fake . client ) ;
318+ const sessionFile = await writeTestBinding ( ) ;
319+
320+ const result = requireCompactResult (
321+ await startCompaction ( sessionFile , { currentTokenCount : 456 } ) ,
322+ ) ;
323+
324+ expect ( fake . request ) . toHaveBeenCalledWith ( "thread/compact/start" , { threadId : "thread-1" } ) ;
325+ expect ( await readCodexAppServerBinding ( sessionFile ) ) . toBeUndefined ( ) ;
326+ expect ( result . ok ) . toBe ( false ) ;
327+ expect ( result . compacted ) . toBe ( false ) ;
328+ expect ( result . reason ) . toBe ( "thread not found: thread-1" ) ;
329+ expect ( result . failure ?. reason ) . toBe ( "stale_thread_binding" ) ;
330+ expect ( result . result ) . toBeUndefined ( ) ;
331+ } ) ;
332+
188333 it ( "warns when stale OpenClaw compaction overrides are ignored" , async ( ) => {
189334 const warn = vi . spyOn ( embeddedAgentLog , "warn" ) . mockImplementation ( ( ) => undefined ) ;
190335 const fake = createFakeCodexClient ( ) ;
@@ -541,6 +686,58 @@ describe("maybeCompactCodexAppServerSession", () => {
541686 ) ;
542687 } ) ;
543688
689+ it ( "honors explicit force for budget-triggered owning context-engine compaction" , async ( ) => {
690+ const info = vi . spyOn ( embeddedAgentLog , "info" ) . mockImplementation ( ( ) => undefined ) ;
691+ const sessionFile = await writeTestBinding ( ) ;
692+ const compact = vi . fn ( async ( ) => ( {
693+ ok : true ,
694+ compacted : true ,
695+ result : {
696+ summary : "engine summary" ,
697+ firstKeptEntryId : "entry-1" ,
698+ tokensBefore : 900 ,
699+ tokensAfter : 100 ,
700+ } ,
701+ } ) ) ;
702+ const contextEngine : ContextEngine = {
703+ info : { id : "lossless-claw" , name : "Lossless Claw" , ownsCompaction : true } ,
704+ assemble : vi . fn ( ) as never ,
705+ ingest : vi . fn ( ) as never ,
706+ compact,
707+ } ;
708+
709+ const result = requireCompactResult (
710+ await maybeCompactCodexAppServerSession ( {
711+ sessionId : "session-1" ,
712+ sessionKey : "agent:main:session-1" ,
713+ sessionFile,
714+ workspaceDir : tempDir ,
715+ contextEngine,
716+ contextTokenBudget : 777 ,
717+ currentTokenCount : 900 ,
718+ trigger : "budget" ,
719+ force : true ,
720+ } ) ,
721+ ) ;
722+
723+ expect ( result . ok ) . toBe ( true ) ;
724+ expect ( result . compacted ) . toBe ( true ) ;
725+ expect ( compact ) . toHaveBeenCalledWith (
726+ expect . objectContaining ( {
727+ compactionTarget : "budget" ,
728+ force : true ,
729+ } ) ,
730+ ) ;
731+ expect ( info ) . toHaveBeenCalledWith (
732+ "starting context-engine-owned Codex app-server compaction" ,
733+ expect . objectContaining ( {
734+ trigger : "budget" ,
735+ compactionTarget : "budget" ,
736+ force : true ,
737+ } ) ,
738+ ) ;
739+ } ) ;
740+
544741 it ( "adopts successor transcript handles after owning context-engine compaction" , async ( ) => {
545742 const sessionFile = await writeTestBinding ( ) ;
546743 const successorFile = path . join ( tempDir , "session.compacted.jsonl" ) ;
0 commit comments