Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
174eeed
fix(inference): a compile refused for budget holds its turn until com…
Oct 14, 2026
13bd4f4
fix(inference): the budget hold's note is an injection; its check tel…
Oct 14, 2026
41afd11
test(inference): the budget hold's note is in the released turn's fir…
Oct 14, 2026
452fdbb
fix(inference): the budget hold's check reruns once after a refusal, …
Oct 14, 2026
d8e3325
fix(inference): a budget hold keeps a batch's turn, lets withdrawn wa…
Oct 14, 2026
bc3bb8e
test(inference): a held continuation's resumed turn leaves the event …
Oct 14, 2026
442c5f0
fix(inference): the budget hold releases on context-manager's preview…
Oct 15, 2026
bfcff59
test(inference): a redo brings a held turn's release check forward
Oct 15, 2026
84e8a09
Merge main (22bb6ca7) into #304's budget hold, now that #352 has landed
Oct 15, 2026
76d4a2e
test(inference): the hold's drain kick stops when a surgery takes the…
Oct 15, 2026
48aaf95
test(inference): the mid-tick surgery test lets its tick go when it f…
Oct 15, 2026
2abb9b4
fix(inference): the budget hold's signature covers every strategy and…
Oct 16, 2026
aafdc9e
test(inference): the hold's context-refusal alert goes out at its sta…
Oct 16, 2026
65359f3
Merge main (91e70eda) into #304's budget hold
Oct 16, 2026
9c4677c
test(inference): pin what the description says hosts and agents see o…
Oct 16, 2026
08303ad
test(inference): render a long wait in minutes, see the gate stay ope…
Oct 16, 2026
26d5ff0
fix(inference): a refusal the preview rethrows, such as a context ref…
Oct 16, 2026
ace7072
docs(inference): the bound's comment and the changelog claim only wha…
Oct 16, 2026
9951805
test(inference): a failing surgery or restart test fails rather than …
Oct 16, 2026
a8b789b
test(inference): the idle-agent test sees the surgery's own look
Oct 16, 2026
01fff39
test(inference): the real-strategy context refusal asserts the looks …
Oct 16, 2026
3c85275
docs(inference): say a dry compile can resolve a plan the turn's comp…
Oct 16, 2026
a2274b2
fix(inference): the budget hold sees every change to the store, a sta…
Oct 16, 2026
b94fb03
docs(changelog): give the budget hold's runtime settings their agent
Oct 16, 2026
b160267
fix(inference): the release check reads whether compression is pendin…
Oct 17, 2026
10ceb24
test(inference): the hold file's look counter runs against main's sou…
Oct 17, 2026
845fd49
docs(changelog): a hold that ends without a fit still lets a turn tha…
Oct 17, 2026
564e942
Merge main (7125cb86) into #304's budget hold, now that #377 has landed
Oct 17, 2026
2cf69a0
test(inference): a held turn's drain kick once stop() has begun, and …
Oct 17, 2026
a1b459c
fix(inference): a held turn under host quiesce, a batch's wake beside…
Oct 17, 2026
53d4609
fix(inference): a released continuation parks at quiesce's admission …
Oct 17, 2026
c7ae3d8
fix(inference): a released restart waits for a token another holds, a…
Oct 17, 2026
d3538d1
Merge main 0d19f7c8 into the budget hold
Oct 18, 2026
5c6d0ea
Merge main 4fd2bfb9 into the budget hold
Oct 19, 2026
a6800f9
test(inference): a held turn's drop as a stop step, and stops that re…
Oct 19, 2026
54565d8
refactor(inference): the hold takes the refused start's trigger as ma…
Oct 19, 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
65 changes: 65 additions & 0 deletions changelog.d/budget-refusal-holds-turn.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
- A turn whose compile is refused for budget waits for compression and then
runs, instead of ending (#304). The refusal comes before any provider call,
for example after an immediate budget decrease. Before, the error policy
retried it into the same refusal within seconds and ended the turn, and once
compression had made the context fit, nothing ran it again, so the agent
stayed silent until its next wake. Now a resident's or a conversation
agent's turn is held while its context manager has compression pending.
Later wakes join it. The hold leaves the provider gate open, so compression
keeps running. An ephemeral run, a turn in a framework that `start()`
hasn't started (only `runUntilIdle()` runs its turns, so nothing would run
a released one), and a refusal with nothing pending fail as before.
- **When it runs:** as one fresh compile, once context-manager's preview of
that compile says it fits. A dry compile can't say so, since it can
resolve a plan the turn's compile refuses. A refusal the preview rethrows,
such as a context refusal, counts as a refusal. A strategy with no preview
gets the dry compile. The hierarchical strategy's preview reports a
context that doesn't fit, and for one that does, the dry compile decides.
The hold looks after every maintenance pass, every drain kick it starts,
and every surgery, undo and redo. It takes a fresh verdict only once
something that could make the context fit has changed: compression's
progress or whether it still has compression pending, a message added,
edited or removed, the agent's runtime settings, or a surgery on the
store. A turn released and refused again is held again with its first
deadline, and isn't released again until one of those changes.
- **When it ends without a fit:** 30 minutes after the refusal that
started it, or at once when compression has nothing pending. The turn
then runs if its context fits by then, and otherwise fails as a refused
compile did, its reason saying when it was first held. The 30 minutes
also bound a compression that is pending but doesn't move. The hold lives
in memory: a restart drops it, and the agent's next wake compiles afresh.
- **What the agent sees:** the held turn's compile carries a note saying
why it waited, for that compile alone: "[budget-hold] Your context didn't
fit its budget (100,201 tokens against 48,568), so this turn waited
7 min. It fits now." It's never stored. One held turn is one undo step,
which keeps the messages that arrived while it waited. The refused start
gives back the checkpoint it recorded, with the oldest one it evicted at
the limit of 20, and the redo its new work invalidated stays invalidated,
as at any turn's first start. An undo while a turn is held rewinds past
that turn's messages, and the held turn then runs on what the undo left
(#381 is that for every pending wake).
- **What hosts see:** the refused attempt emits `inference:failed`, and the
turn's next start emits `inference:started` again. The turn's terminal
event comes when the held turn ends, after a hold of up to 30 minutes,
rather than as an exhaustion within seconds. New traces `inference:held`
and `inference:hold_ended`, and `healthSnapshot()` gives each agent's
`budgetHold` (or null). The `context-refusal` ops alert goes out when the
hold starts (its `ops:alert` trace each time, its webhook post at most
once per 15 minutes for an agent), and says the turn is held until its
deadline. A hold's drain kicks log as `[budget-hold]`. While the host is
quiesced, a turn a hold releases waits for resume as a parked wake does,
a continuation included, whether it's still queued or already waiting
on provider admission, so no such turn starts while the host is
quiesced. `getHostModeStatus()` counts it among `gatedRequests`, and its
new `heldTurns` counts the turns still held, as quiesce's log line does.
A budget restart a hold releases waits, as a wake does, for a turn token
another holds, such as a puppeted tool call's.
- A deferred batch's wake that waited behind a turn which then restarted for
its context budget was consumed by the restarted turn, which continues the
same logical turn and skips batch assembly, so the batch never rendered
until some later turn, or was dropped with it when the inference policy
declined the restart. It now stays queued, with its own policy decision,
and wakes a fresh turn of its own once the restarted turn ends. A wake
that joins a held continuation (above) is kept out of it the same way,
and a guard retry a hold releases is its turn's trigger wherever it was
queued, as a budget restart already was.
13 changes: 11 additions & 2 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS,
} from './tool-result-history.js';

/** ContextManager.compile's own budget when it is given none. */
const CONTEXT_MANAGER_DEFAULT_BUDGET: TokenBudget = { maxTokens: 100_000, reserveForResponse: 4_000 };

export interface ActivationRequestOptions {
/** The silent heartbeat tick this request answers, if any. */
silentHeartbeat?: SilentHeartbeatTick;
Expand Down Expand Up @@ -384,11 +387,17 @@ export class Agent {
const resolved = this.resolveBudget(budget);
const reserve = this.toolResultGuard.pendingWireReserveTokens;
if (reserve === 0) return resolved;
// Mirrors ContextManager.compile's own default when no budget is set.
const base = resolved ?? { maxTokens: 100_000, reserveForResponse: 4_000 };
const base = resolved ?? CONTEXT_MANAGER_DEFAULT_BUDGET;
return { ...base, reserveForResponse: base.reserveForResponse + reserve };
}

/** The budget the agent's next compile plans at (compileBudget), for a
* preview of that compile: context-manager's own default when the agent
* sets none, as its compile then gets. */
nextCompileBudget(): TokenBudget {
return this.compileBudget(undefined) ?? CONTEXT_MANAGER_DEFAULT_BUDGET;
}

/** Structural compatibility keeps lightweight test/host ContextManager
* doubles working while making the new capability optional at runtime. */
private getHotContextSettings(): HotContextSettingsStatus | null {
Expand Down
Loading
Loading