@@ -1244,6 +1244,12 @@ export class AgentSession {
12441244 tools: Agent["state"]["tools"];
12451245 settings: string;
12461246 };
1247+ private _failedThresholdCompaction?: {
1248+ isCurrent: BoundCompactionSink["isCurrent"];
1249+ configuration: string;
1250+ systemPrompt: string;
1251+ tools: Agent["state"]["tools"];
1252+ };
12471253 private readonly _contextEpochsEnabled: boolean;
12481254 private readonly _initialContextMode: ContextMode;
12491255 private _contextMode: ContextMode;
@@ -1748,7 +1754,7 @@ export class AgentSession {
17481754 acknowledged = { entryId, result };
17491755 try {
17501756 assertResourceCurrent(resource);
1751- if (this.sessionManager !== epochManager || epochManager.getLeafId() !== entryId )
1757+ if (this.sessionManager !== epochManager || !compaction.isCurrent() )
17521758 throw new Error("Context epoch source changed before adoption");
17531759 this.agent.state.messages = prepared.messages;
17541760 committed = prepared.checkpoint;
@@ -1864,7 +1870,7 @@ export class AgentSession {
18641870 committedEntry = { sessionId: epochContext.source.sessionId, entryId };
18651871 requestContract = nextContract;
18661872 assertResourceCurrent(resource);
1867- if (this.sessionManager !== epochManager || epochManager.getLeafId() !== entryId )
1873+ if (this.sessionManager !== epochManager || !compaction.isCurrent() )
18681874 throw new Error("Context contract source changed before acceptance");
18691875 }
18701876 if (!sameSelectedSkills(committed?.selectedSkills, epochContext.selectedSkills))
@@ -1881,6 +1887,7 @@ export class AgentSession {
18811887 }
18821888 }
18831889 : undefined,
1890+ compaction.isCurrent,
18841891 );
18851892 }
18861893 this._compactionSetupFailure = undefined;
@@ -1909,7 +1916,7 @@ export class AgentSession {
19091916 !error.source.persistent ||
19101917 error.source.sessionId !== owner.sessionId ||
19111918 error.source.sessionFile !== owner.sessionFile ||
1912- error.source.leafId !== owner.manager.getLeafId()
1919+ !( error.isSourceCurrent?.() ?? error. source.leafId === owner.manager.getLeafId() )
19131920 )
19141921 return false;
19151922 // Rebase this captured frame before paying for a summary. The replacement still
@@ -2258,7 +2265,7 @@ export class AgentSession {
22582265 };
22592266 // ACK is authoritative even if later setup or release fails.
22602267 this._contextMode = mode;
2261- if (this.sessionManager !== manager || manager.getLeafId() !== entryId )
2268+ if (this.sessionManager !== manager || !compaction.isCurrent() )
22622269 throw new Error("Context mode source changed before adoption");
22632270 assertResourceCurrent(resource);
22642271 this.agent.state.messages = prepared.messages;
@@ -4149,6 +4156,28 @@ export class AgentSession {
41494156 });
41504157 }
41514158
4159+ private _thresholdCompactionConfiguration(): string {
4160+ return JSON.stringify({
4161+ model: this.model,
4162+ thinkingLevel: this.thinkingLevel,
4163+ settings: this.settingsManager.getCompactionSettings(),
4164+ });
4165+ }
4166+
4167+ private _hasFailedThresholdCompaction(): boolean {
4168+ const failed = this._failedThresholdCompaction;
4169+ if (!failed) return false;
4170+ if (
4171+ failed.isCurrent() &&
4172+ failed.configuration === this._thresholdCompactionConfiguration() &&
4173+ failed.systemPrompt === this.agent.state.systemPrompt &&
4174+ failed.tools === this.agent.state.tools
4175+ )
4176+ return true;
4177+ this._failedThresholdCompaction = undefined;
4178+ return false;
4179+ }
4180+
41524181 private async _thresholdCompactionNeeded(
41534182 context: ShouldStopAfterTurnContext,
41544183 owner = this._captureCompactionOwner(),
@@ -4167,6 +4196,7 @@ export class AgentSession {
41674196 if (compactionTimestamp !== undefined && context.message.timestamp <= compactionTimestamp) return false;
41684197 const contextTokens = this._getThresholdContextTokens(context.message, compactionTimestamp);
41694198 if (contextTokens === undefined || !shouldCompact(contextTokens, contextWindow, settings)) return false;
4199+ if (this._hasFailedThresholdCompaction()) return false;
41704200 // Keep the existing threshold-specific goal winner; do not import W74's natural wait policy here.
41714201 if (!this._isGoalContinuationOwnerCurrent(goalOwner)) return true;
41724202 if (!(await this._queueGoalContinuationForThresholdCompaction(context.message, goalOwner))) {
@@ -5546,6 +5576,7 @@ export class AgentSession {
55465576 */
55475577 async disposeAsync(options?: { kernelSnapshot?: boolean }): Promise<void> {
55485578 this._failedAutomaticCompaction = undefined;
5579+ this._failedThresholdCompaction = undefined;
55495580 this._contextCompiler.clear();
55505581 this._nativeRecoveryCursors.clear();
55515582 for (const admission of this._rlmChildAdmissions) admission.cancel("Parent session disposed");
@@ -5751,6 +5782,7 @@ export class AgentSession {
57515782
57525783 dispose(): void {
57535784 this._failedAutomaticCompaction = undefined;
5785+ this._failedThresholdCompaction = undefined;
57545786 this._contextCompiler.clear();
57555787 this._nativeRecoveryCursors.clear();
57565788 if (this._disposed) {
@@ -11015,6 +11047,7 @@ export class AgentSession {
1101511047 const contextTokens = this._getThresholdContextTokens(assistantMessage, compactionTimestamp);
1101611048 if (contextTokens === undefined) return false;
1101711049 if (shouldCompact(contextTokens, contextWindow, settings)) {
11050+ if (this._hasFailedThresholdCompaction()) return false;
1101811051 if (!this._pendingCheckpoint && queueAutonomousContinuation) {
1101911052 if (
1102011053 !(await this._queueGoalContinuationForThresholdCompaction(assistantMessage, goalOwner)) &&
@@ -11146,6 +11179,24 @@ export class AgentSession {
1114611179 let compaction: BoundCompactionSink | undefined;
1114711180 let committed: CompactionCommit | undefined;
1114811181 let failure: unknown;
11182+ const thresholdConfiguration =
11183+ reason === "threshold" && !resumeInPlace
11184+ ? {
11185+ configuration: this._thresholdCompactionConfiguration(),
11186+ systemPrompt: this.agent.state.systemPrompt,
11187+ tools: this.agent.state.tools,
11188+ }
11189+ : undefined;
11190+ const rememberThresholdFailure = () => {
11191+ if (
11192+ thresholdConfiguration &&
11193+ !abort.signal.aborted &&
11194+ this._isCompactionOwnerCurrent(owner) &&
11195+ compaction?.isCurrent()
11196+ ) {
11197+ this._failedThresholdCompaction = { ...thresholdConfiguration, isCurrent: compaction.isCurrent };
11198+ }
11199+ };
1114911200 try {
1115011201 const selected = this._resolveCompactionModel();
1115111202 const model = selected?.model;
@@ -11170,6 +11221,7 @@ export class AgentSession {
1117011221 reason === "threshold" && shouldContinueAfterCompaction,
1117111222 queuedAutonomousContinuationsForThisCompaction,
1117211223 );
11224+ rememberThresholdFailure();
1117311225 resumeAfterFailure();
1117411226 return false;
1117511227 }
@@ -11262,6 +11314,7 @@ export class AgentSession {
1126211314 { customInstructions, owner },
1126311315 );
1126411316 }
11317+ rememberThresholdFailure();
1126511318 resumeAfterFailure();
1126611319 return false;
1126711320 } finally {
0 commit comments