Skip to content

Commit f789ed2

Browse files
committed
fix: tolerate bookkeeping during compaction and release 1.0.9
1 parent 2c482f9 commit f789ed2

16 files changed

Lines changed: 478 additions & 68 deletions

‎package-lock.json‎

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
4949
"engines": {
5050
"node": "^22.12.0 || >=23.3.0"
5151
},
52-
"version": "1.0.8",
52+
"version": "1.0.9",
5353
"dependencies": {
5454
"get-east-asian-width": "^1.6.0",
55-
"@ponythewhite/base-context": "1.0.8"
55+
"@ponythewhite/base-context": "1.0.9"
5656
},
5757
"overrides": {
5858
"rimraf": "6.1.2",

‎packages/agent/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ponythewhite/base-context-agent",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"description": "Synerise base-context agent runtime with context management and native recovery",
55
"type": "module",
66
"main": "./dist/index.js",
@@ -19,7 +19,7 @@
1919
"prepublishOnly": "npm --prefix ../.. run build:source"
2020
},
2121
"dependencies": {
22-
"@ponythewhite/base-context-ai": "1.0.8",
22+
"@ponythewhite/base-context-ai": "1.0.9",
2323
"typebox": "^1.3.9"
2424
},
2525
"keywords": [

‎packages/ai/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ponythewhite/base-context-ai",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"description": "Synerise base-context unified LLM API and provider configuration",
55
"type": "module",
66
"main": "./dist/index.js",

‎packages/coding-agent/CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [1.0.9] - 2026-09-19
4+
5+
- Fixed compaction and context checkpoint adoption rejecting concurrent child usage and session bookkeeping updates as branch changes. Real conversation, source, and branch changes still invalidate stale summaries.
6+
- Fixed repeated automatic summary attempts on unchanged context after a failed threshold compaction. Manual retries and new input remain available.
7+
38
## [1.0.8] - 2026-09-18
49

510
- Fixed harness-state saves to replace files atomically, preserve failed mutations, and report corrupt state without overwriting it.

‎packages/coding-agent/package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ponythewhite/base-context",
3-
"version": "1.0.8",
3+
"version": "1.0.9",
44
"description": "Synerise base-context: a coding and research agent with durable context and a persistent Python REPL",
55
"type": "module",
66
"bin": {
@@ -42,9 +42,9 @@
4242
},
4343
"dependencies": {
4444
"@agentclientprotocol/sdk": "^1.3.0",
45-
"@ponythewhite/base-context-agent": "1.0.8",
46-
"@ponythewhite/base-context-ai": "1.0.8",
47-
"@ponythewhite/base-context-tui": "1.0.8",
45+
"@ponythewhite/base-context-agent": "1.0.9",
46+
"@ponythewhite/base-context-ai": "1.0.9",
47+
"@ponythewhite/base-context-tui": "1.0.9",
4848
"@silvia-odwyer/photon-node": "^0.3.4",
4949
"chalk": "^5.5.0",
5050
"cli-highlight": "^2.1.11",

‎packages/coding-agent/src/core/agent-session.ts‎

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {

‎packages/coding-agent/src/core/inference-coordinator.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,12 @@ export class InferenceCoordinator {
397397
commit: RequestViewCommit,
398398
validate?: RequestViewValidate,
399399
fixedPrepare?: RequestViewFixedPrepare,
400+
isSourceCurrent?: () => boolean,
400401
): void {
401402
this.assertAdmission();
402403
if (!this.capturedSink) throw new Error("Request view boundary requires a captured inference owner");
403404
if (this.requestViewBoundary) throw new Error("Request view boundary is already bound");
404-
this.requestViewBoundary = captureRequestViewBoundary(messages, commit, validate, fixedPrepare);
405+
this.requestViewBoundary = captureRequestViewBoundary(messages, commit, validate, fixedPrepare, isSourceCurrent);
405406
}
406407

407408
/** Read through an explicit capture, never by recapturing the mutable current session. */

‎packages/coding-agent/src/core/request-view-selection.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export class PublicContextBudgetError extends RequestTokenBudgetError {
4343
readonly taskFrameRebaseAvailable = false,
4444
compactionKey?: string,
4545
summaryCapacity?: (retained: ReadonlySet<string>, wrapper: string) => number | undefined,
46+
readonly isSourceCurrent?: () => boolean,
4647
) {
4748
super(assessment);
4849
this.name = "PublicContextBudgetError";
@@ -93,13 +94,15 @@ export interface CapturedRequestViewBoundary extends CanonicalViewSelectionSourc
9394
readonly commit: RequestViewCommit;
9495
readonly validate?: RequestViewValidate;
9596
readonly fixedPrepare?: RequestViewFixedPrepare;
97+
readonly isSourceCurrent?: () => boolean;
9698
}
9799

98100
export function captureRequestViewBoundary(
99101
messages: readonly AgentMessage[],
100102
commit: RequestViewCommit,
101103
validate?: RequestViewValidate,
102104
fixedPrepare?: RequestViewFixedPrepare,
105+
isSourceCurrent?: () => boolean,
103106
): CapturedRequestViewBoundary {
104107
const source = getCanonicalViewSelectionSource(messages);
105108
if (!source) throw new Error("Request view boundary requires compiled canonical messages");
@@ -118,6 +121,7 @@ export function captureRequestViewBoundary(
118121
commit,
119122
validate,
120123
fixedPrepare,
124+
isSourceCurrent,
121125
};
122126
}
123127

@@ -255,7 +259,16 @@ function publicBudgetFailure(
255259
const body = JSON.parse(request.body!) as Record<string, unknown>;
256260
const input = body[inputKey];
257261
if (!Array.isArray(input) || input.length !== projection.messageIndices.length)
258-
return new PublicContextBudgetError(boundary.source, assessment, original);
262+
return new PublicContextBudgetError(
263+
boundary.source,
264+
assessment,
265+
original,
266+
undefined,
267+
false,
268+
undefined,
269+
undefined,
270+
boundary.isSourceCurrent,
271+
);
259272
const graph = prepareViewSelection(source.units, boundary.limits);
260273
const retainedInput = (retained: ReadonlySet<string>) => {
261274
const roots = source.units
@@ -302,6 +315,7 @@ function publicBudgetFailure(
302315
? undefined
303316
: Math.floor(measured.availableInputTokens - measured.estimatedInputTokens);
304317
},
318+
boundary.isSourceCurrent,
305319
);
306320
}
307321

0 commit comments

Comments
 (0)