Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
88f34fe
chore(deps): context-manager ^0.14.0
Oct 13, 2026
eefc10c
fix(maintenance): an agent whose context manager refuses its readines…
Oct 13, 2026
d475206
fix(recovery): offline recovery reads a body as context-manager 0.14 …
Oct 13, 2026
0ed307d
fix(maintenance, recovery): answer the internal review of the context…
Oct 13, 2026
32bfb3c
Merge main 7cd7af5f into ada/cm-0.14
Oct 14, 2026
a600170
test(maintenance): a surgery that begins inside a pass
Oct 14, 2026
22a82af
docs(changelog): name the alert a failed pass raised before
Oct 14, 2026
fb47fab
fix(maintenance): maintenanceTick answers ran: false while a surgery …
Oct 14, 2026
2a9463c
test(maintenance): hold the store before start(), whose first pass ti…
Oct 14, 2026
ea4139a
fix(maintenance, removals): surgeries wait for a running pass; a last…
Oct 14, 2026
d24a981
fix(hide): a message stored as a body is hidden whole
Oct 14, 2026
2fd8d6e
docs(changelog): the round's maintenance, resume and removal changes;…
Oct 14, 2026
c7ae130
Merge main cd036e0d into ada/cm-0.14
Oct 14, 2026
82859d5
fix(resume, surgeries): wait only for a maintenance pass that is running
Oct 14, 2026
9d61bbc
fix(maintenance): switchBranch waits for a running pass before it swi…
Oct 14, 2026
41a2a32
fix(maintenance, rewind): drain kicks are maintenance too; resume che…
Oct 14, 2026
525a318
test(maintenance): a lasting failure's re-alerts follow its whole tex…
Oct 14, 2026
758743f
fix(hide): every stored copy of a message is hidden; a body counts on…
Oct 14, 2026
7df647f
fix(removals): a module's and the coalescer's removal of a message in…
Oct 14, 2026
9de6d89
test(maintenance): a rollback waits for an over-budget drain kick's t…
Oct 14, 2026
a170308
docs(changelog), test: the operator note says where to tell the first…
Oct 14, 2026
2558dc9
test(discord-awareness): a resumed suppression's two crash states; th…
Oct 14, 2026
5165fe5
test(inference-failure-bounds): the stub framework has the drain-kick…
Oct 14, 2026
d71370d
fix(hide): refused while a surgery holds the store; a missing Discord…
Oct 14, 2026
3abf2bd
fix(maintenance): a surgery withdraws maintenance provider calls rath…
Oct 14, 2026
ff94afa
fix(api): a branch switch that waits for running maintenance announce…
Oct 14, 2026
ebef76e
fix(maintenance): a lasting failure's reminder counts from the webhoo…
Oct 14, 2026
5985987
perf(removals): a message's body is found in blob-free windows, not a…
Oct 14, 2026
609b247
fix(hide): a single hide's operator-log entry keeps at most 50 ids, a…
Oct 14, 2026
194b17a
docs(changelog): the round's maintenance withdrawal, the API's branch…
Oct 14, 2026
fb8a6c4
fix(maintenance): a parked call woken with its gate still closed park…
Oct 14, 2026
ca2f806
test(maintenance): read the first pass the timer recorded, not the ne…
Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,32 @@ framework.
grows quadratically with session length (context-manager#148). Stores written
under 0.13.x still load. Patch release from the v0.21.0 tag with this
dependency bump only; the work on `main` since 0.21.0 is not included.
- **Operators of stores that learned an estimator calibration under
context-manager 0.13 or earlier** (added after the release): 0.14.0 stamps
its persisted calibration multiplier with the pricing it was learned under.
An unstamped multiplier is kept and re-stamped, unless the store holds
XML-history carriers, in which case it restarts from 1.0 and the value it
discarded is kept as `reset.discardedMultiplier` in the calibration state
(shown in `RenderStats.calibration`). Two kinds of store can then refuse
every compile with `OverBudgetError` before inference. One holds no
carriers and kept a multiplier last learned before its host moved to
context-manager 0.11.0. Its record can't show that; the host's own
history can, in its lockfile's or its deployments' history: a host that
went from a context-manager older than 0.11.0 straight to 0.14.0 can
hold one. The other holds carriers and had learned a multiplier
below 1, which its `reset.discardedMultiplier` shows. With the host
stopped, reset the first kind's multiplier once, and re-stamp the second
kind's checked value, as the **Operators** entry of context-manager
0.14.0's changelog describes.
- **What else hosts meet with context-manager 0.14.0** (added after the
release): a preview, or any compile made with `dryRun`, leaves the context
manager's estimate, its calibration and a pending kv-unified draft's marker
indices as it found them. The context manager's state queries (`isReady`,
`getPendingWork`, `getHotContextSettings`, `previewContext` and others)
throw while its strategy's initialization is pending or has failed, where
they used to answer. A reposted sharded document renders as two bodies, so
a stored conversation holding one misses the prompt cache once after the
upgrade.

## 0.21.0 — 2026-10-03

Expand Down
1 change: 1 addition & 0 deletions changelog.d/body-removals.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Removing a single message that belongs to a body, a one-shard body's included, no longer fails: context-manager's `removeMessage` refuses any message in a body, so agent-framework now removes one as a range in offline recovery's suppressions, and in a Discord suppression resumed after a crash. The host `hide` command now hides a message stored as a body (a long one, in shards) whole, with every stored copy of it (a write cut short leaves a partial copy before its retry), and its operator-log entry lists at most 50 of the ids it removed, with `removedIdsTruncated` when there were more. It's refused as busy while a surgery holds the store, and a message with no Discord id never matches one. A module's removal of a message it added, and the coalescer's, take its whole body too, and an automatic rewind whose newest message is a shard of a body takes the whole body.
2 changes: 2 additions & 0 deletions changelog.d/cm-0.14.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- A context-maintenance pass no longer fails as a whole when one agent's readiness can't be read. context-manager 0.14, which agent-framework has required since 0.21.1, refuses its readiness queries while a strategy's initialization is pending (inside `switchBranch`, `fork`, `setStrategy` or `ContextManager.open`) or has failed, and a strategy can refuse on its own, as the autobiographical one does for a branch it hasn't reloaded. That agent's run now records the error, the agent gets its own `context-maintenance-failed` alert (`scope: 'agent'`), and the other agents run. Before, such a throw ended the pass for every agent, with one alert for the whole pass (`scope: 'pass'`). Anything else that throws while one agent's maintenance is prepared is that agent's failure in the same way. A failure that lasts, met by every pass, is recorded on each run, and logged and alerted once, then again when its text changes, every 15 minutes while it lasts (the alert webhook's cooldown for it, counted from the webhook's last post of it), or after that agent's maintenance has gone without a failure. `getContextMaintenanceSnapshot()` reports such an agent as `ready: false` with an `error`, and a strategy that can't give a progress snapshot reads as having none.
- While an operator surgery (`rollbackToMessage` or `suppressMessages`) or `switchBranch` holds the store, no maintenance pass starts and no over-budget drain kick is started, a running pass or drain kick starts no new tick, and the operator call waits for them to finish before it switches or forks the branch, so no maintenance work overlaps the move. Maintenance makes no provider call meanwhile: one made then, or one waiting at the agent's provider gate when the hold is taken, is withdrawn without being sent. That's no maintenance failure, and context-manager counts no attempt for it. A merge keeps its place in the queue. A chunk's compression logs context-manager's "Failed to compress chunk (hierarchical)" line with the withdrawal, and the chunk is queued again at that manager's next rebuild, such as its next message or compile. The operator call never waits on a provider cooldown, or on the retry turn a cooldown queues, which the hold keeps from starting. `switchBranch` can therefore move the store after the call returns, and the API server's `branch.switch` announces the move it made once it has, while `branch.current`, the branch list, `/health` and `/status` read the store's branch whenever they report one. `maintenanceTick()` answers `ran: false` meanwhile, and `resume()` refuses with `OperatorActionError` (`agent-busy`), as the other operator calls do, including when the surgery took the store while `resume()` waited. Resume once the surgery has finished.
2 changes: 2 additions & 0 deletions changelog.d/cm-0.14.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `resume()` no longer throws when one agent's runtime settings can't be read, as when its context manager (0.14, required since 0.21.1) refuses after its strategy failed to initialize. It warns, naming the agent and the error, and proceeds without a feasibility verdict for that agent: the verdict guards against an agent that would run over budget, and an agent whose manager refuses its compiles can't run. That manager refuses the agent's turns and writes until a `switchBranch` or `setStrategy` succeeds, and maintenance reports it.
- Offline recovery (`createOfflineRecoveryBranch`) reads a sharded message's body as context-manager 0.14.0 does: a message continues the body before it only when it carries the same group id at a higher shard index. A document posted twice stores two copies under one group id, so an anchor at the end of one copy, or a suppression of one copy alone, is no longer refused. A selection inside one copy is still refused. An anchor at the end of a write that was cut short keeps that partial body and leaves out its retry, which context-manager 0.14.0 reads as a body of its own.
47 changes: 23 additions & 24 deletions src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,20 @@ export class ApiServer {
private wss: WebSocketServer | null = null;
private httpServer: ReturnType<typeof createServer> | null = null;
private clients: Set<WebSocket> = new Set();
private currentBranch = 'main';
/** The store's branch as last read: what the API reports once the store has closed. */
private lastKnownBranch = 'main';

/**
* The store's current branch, read from the store while it's open. A cached
* name would go stale behind any move the API server didn't make itself (a
* rollback, a host's switch), and behind a switch whose move comes after
* its call returns, as one does once running maintenance has finished.
*/
private get currentBranch(): string {
const store = this.framework.getStore();
if (!store.isClosed()) this.lastKnownBranch = store.currentBranch().name;
return this.lastKnownBranch;
}

/** Map from client to their subscription IDs */
private clientSubscriptions: Map<WebSocket, Set<string>> = new Map();
Expand Down Expand Up @@ -572,43 +585,31 @@ export class ApiServer {
throw new Error('name is required');
}

const store = this.framework.getStore();
const from = store.isClosed() ? this.currentBranch : store.currentBranch().name;
const from = this.currentBranch;

// The framework switches the store, starts the delivery of its Discord
// awareness marks behind the data-plane gate, and re-materializes the
// config mount. It refuses during a live turn or once stop() has begun,
// and stop() waits for one already running.
// The store has moved when the call returns, before its promise settles
// (a refusal moves nothing). The branch read then is this switch's
// destination: another switch can move the store before this one's
// awaited steps end, so the event names what was read here.
const switching = this.framework.switchBranch(params.name);
void switching.catch(() => {}); // awaited below; handled even if the next line throws
this.followStoreBranch();
const to = this.currentBranch;
const moved = to !== from;
// and stop() waits for one already running. It moves the store once any
// maintenance already running has finished, so the move can come after
// this call returns. The event names this switch's own move, from the
// branch read here to the one it was asked for: another switch can move
// the store again before this one continues.
let switched = false;
try {
await switching;
await this.framework.switchBranch(params.name);
switched = true;
} finally {
// A later step can fail after the store has moved: clients still hear
// where it went. A refusal moved nothing, so there's nothing to tell.
if (switched || moved) {
this.broadcast('branch:switched', { from, to });
if (switched || (this.currentBranch === params.name && params.name !== from)) {
this.broadcast('branch:switched', { from, to: params.name });
}
}

return { switched: true };
}

/** Take the current branch from the store, while it's open. */
private followStoreBranch(): void {
const store = this.framework.getStore();
if (!store.isClosed()) this.currentBranch = store.currentBranch().name;
}

private async cmdBranchCurrent(): Promise<{ name: string }> {
return { name: this.currentBranch };
}
Expand Down Expand Up @@ -646,7 +647,6 @@ export class ApiServer {
const result = this.framework.undoLastTurn(params.agentName);

if (result.undone) {
this.currentBranch = result.toBranch!;
// Marks follow behind the data-plane gate; the undo doesn't wait for Discord.
this.framework.startDiscordAwarenessDelivery('undo', params.agentName);
this.broadcast('branch:switched', {
Expand All @@ -667,7 +667,6 @@ export class ApiServer {
const result = this.framework.redo(params.agentName);

if (result.redone) {
this.currentBranch = result.toBranch!;
// Marks follow behind the data-plane gate; the redo doesn't wait for Discord.
this.framework.startDiscordAwarenessDelivery('redo', params.agentName);
this.broadcast('branch:switched', {
Expand Down
Loading
Loading