[pull] main from danny-avila:main - #160
Merged
Merged
Conversation
… Requests (#377) * feat(search): allow callers to inject an http(s) agent for outbound requests Add optional httpAgent and httpsAgent fields to the web-search tool config and apply them to every outbound axios request made by the search, scrape, and rerank clients. The fields thread from SearchToolConfig through each provider client. The five scraper configs and the reranker config accept them directly, the Tavily, Keenable, and fastCRW search factories carry them on their existing options bags, and the Serper and SearXNG factories receive them through an internal config so no public positional signature is widened. Both agents are passed on every call so a redirect that changes protocol still uses a caller-controlled agent. Backward compatible: both fields are optional and default to undefined, so with no agent supplied axios behaves exactly as before. * fix(search): fall back to option-bag http agents instead of clobbering with undefined The scraper and Tavily/Keenable/CRW search constructors spread the per-provider option bag and then assigned httpAgent/httpsAgent from the top-level config, which are undefined when omitted, silently dropping a caller-provided agent set inside the bag. Fall back to the bag value when no top-level agent is set, matching the existing timeout/apiKey/apiUrl fallback pattern in the same literals; a top-level agent still overrides. * ♻️ refactor(search): Consolidate shared scraper config fields into a base The five provider scraper configs each repeated apiKey, apiUrl, timeout, and logger on top of HttpAgentConfig. Introduce BaseSearchProviderConfig (extends HttpAgentConfig with that quartet) and repoint SerperScraperConfig, TavilyScraperConfig, KeenableScraperConfig, CrwScraperConfig, and FirecrawlScraperConfig at it, keeping only provider-specific fields on each. HttpAgentConfig stays the agents-only mixin for the search-option bags (TavilySearchOptions, CrwSearchOptions, KeenableSearchOptions), which share the agents and timeout but not apiKey/apiUrl/logger, and for SearchConfig, whose credentials are provider-prefixed. Keenable's apiUrl doc comment stays inline. The Omit-based scrape-option helper types are unchanged since Omit resolves inherited keys.
…#379) The code-execution tools applied HttpsProxyAgent to whatever PROXY held: if (process.env.PROXY != null && process.env.PROXY !== '') { fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY); }
* 🚦 feat: Circuit-Break Runaway Streamed Tool-Call Arguments
* 🚦 fix: Address Codex round 1 (seal restatement dedupe, pre-dispatch enforcement, generation-stable keys, index-less chunks, type imports)
* 🚦 fix: Address Codex round 2 (attempt-scoped budgets, non-retryable breaker, early event count, subagent propagation, anonymous call budgets)
* 🚦 fix: Address Codex round 3 (attempt-stamped keys, summarization event cap, child trip propagation, single-count redispatch)
* 🚦 fix: Address Codex round 4 (unique attempt stamps replace resets, summarization byte cap + per-attempt keys)
* 🚦 fix: Address Codex round 5 (summarization trips fail the run, split surrogate reconciliation)
* 🚦 fix: Address Codex round 6 (local-branch replay counting, manual compaction limits)
* 🚦 feat: Per-tool overrides for the streamed tool-argument byte cap
Thirty days of production data show tool-argument sizes are strongly
tool-shaped: every tool class sits at p99 under 10 KiB except file
creation (p99 80.6 KiB), whose whole-document arguments are legitimate.
A single global cap must either loosen protection on the dominant
traffic or fail real file writes.
StreamLimits gains maxToolCallArgBytesByTool, keyed by model-facing
tool name: a matching entry replaces the global cap for that tool's
calls, 0 disables the guard for that tool only, and entries normalize
exactly like the global field. Enforcement resolves the limit from the
call's tallied name at check time; unnamed calls use the global cap.
No overrides configured means one extra null check per chunk, and a
configured map costs one Object.hasOwn lookup per chunk.
* 🚦 fix: re-judge late-named tallies and keep override table prototype-free
Two per-tool override gaps: bytes tallied before a call's name arrived
were only ever held against the global cap, so an empty chunk that
finally supplied the name (including a sealing one) could release a
call that exceeded its lower per-tool limit; and normalizing overrides
into a plain object dropped a __proto__ tool name via the prototype
setter, silently reverting that tool to the global cap. Late-name
chunks now re-judge the tally against the newly applicable limit
before any seal release, and the override table is built on a
null-prototype object.
* 🚦 fix: Address Codex round 8 (key stability, redispatch args, server-result ordering)
Three enforcement gaps in the argument breaker:
- An index-less adapter that puts the call id only on the first chunk
split later anonymous deltas onto the batch-position fallback key,
giving one call two budgets. Id-keyed tallies now also register under
their position key (same tally object), and both entries release
together on seal.
- The OpenRouter redispatch marker exempted only the event counter, so
transformed re-dispatches charged tool-call argument bytes a second
time and could falsely abort a legitimate call at half the cap. The
argument guard now skips marked redispatches; originals that carry
tool calls always survive the content-specific skips and remain the
single authoritative charge.
- The server-tool-result early return ran before the argument guard, so
a coalesced event carrying client tool_call_chunks alongside a server
result released unjudged calls into the accumulated message. The
guard is hoisted next to the event counter, ahead of every
content-specific early return; the other early returns refuse
tool-call-bearing chunks, so the hoist introduces no double-counting.
All three ship regression tests that fail before this commit.
* 🚦 fix: Address Codex round 9 (producer-sync limits, complete calls, import order)
- The registered-handler dispatch branch charged wire chunks only when
the decoupled streamEvents reader caught up, so a lagging reader could
let an oversized complete call return to LangGraph and reach ToolNode
before the queued handler threw. The producer loop now charges each
wire chunk synchronously (event budget, streamed chunks, and complete
calls) and marks the object so the handler echo skips accounting.
- Complete parsed tool_calls arriving without a raw chunk representation
dispatched without consuming any byte budget; they are now judged
standalone against their tool's limit, in the handler and in the
producer path.
- Stream-limit type imports in Graph.ts move to the type-import section
per AGENTS.md import ordering.
Both behavioral fixes ship regression tests that fail with the wiring
reverted.
* 🚦 fix: Address Codex round 10 (claim-based charging, replay-skip args, summarization complete calls)
- Chunk deduplication was one-directional: the producer loop marked
after charging, so a handler echo that won the race charged first and
the producer charged again, falsely tripping legitimate calls at half
the configured cap. Charging is now claim-based (claimStreamLimitCharge):
whichever path observes the chunk object first charges it, the other
skips.
- The local dispatch branch charged replay-skipped chunks only against
the opt-in event counter, but a cumulative OpenRouter replay can carry
tool_call_chunks or complete tool_calls that are still appended; those
chunks now go through full wire-chunk accounting.
- The summarization onChunk path duplicated the handler's enforcement
trio and missed complete parsed tool_calls; it now delegates to
enforceStreamLimitsForWireChunk, gaining the complete-call guard.
* 🚦 fix: Address Codex round 11 (credit-balanced charging for reused chunk objects)
A lifetime WeakSet claimed a chunk object once, but streaming models may
mutate and re-yield the same object across emissions (a supported
adapter pattern), so every emission after the first bypassed both
limits. Deduplication is now a signed per-object credit balance:
producer visits increment, consumer (handler echo) visits decrement,
and a visit charges only when the other side has not pre-charged that
emission. Single-sided paths keep charging every visit, and the local
dispatch branch claims as consumer throughout because its
handler-handled and replay-skipped emissions of one reused object
alternate rather than pair. Also moves the summarization stream-limit
type import into the type section per AGENTS.md.
Both reuse scenarios ship regression tests that fail against the
lifetime-set implementation.
* 🚦 fix: Address Codex round 12 (alias ownership, generation-scoped credits)
- Parallel index-less calls all land on batch position #i, so starting
a new call displaced the previous call's alias while the old tally
still recorded ownership; sealing the old call then blindly deleted
the displaced alias and split the live call's budget. Alias handoff
now disowns the previous holder, release only deletes an alias the
tally still owns, and sealing chunks never take the alias from a
still-live parallel call.
- Charge credits were scoped by chunk object alone, so parallel
generations sharing one reused mutable chunk object could cancel each
other's charges and leave a generation entirely unaccounted. Balances
are now keyed by generation identity within each object.
Both scenarios ship regression tests that fail against the previous
implementation.
* 🚦 fix: Address Codex round 13 (parsed-call coverage, id aliasing, credit reset, legacy typing)
- Complete parsed tool_calls are now judged whenever present, not only
when the raw chunk array is absent: an adapter can pair an empty or
partial raw chunk with an oversized parsed call, and the standalone
check is stateless so the common both-present case is not
double-tallied.
- A call that streams with id+index and later drops the index keyed its
fragments separately, splitting the budget; chunks carrying both
identifiers now register the id as the tally's alias so either
identifier reaches the same tally.
- streamLimitChargeCredits is reinitialized by both graph reset paths;
a retained reused chunk object otherwise accumulates one
attempt-stamped generation entry per model call for the graph's life.
- LegacyGraphConfig no longer accepts graphConfig.streamLimits, which
createLegacyGraph would silently ignore; legacy runs configure limits
via RunConfig.streamLimits only.
Both behavioral fixes ship regression tests that fail with the fixes
stashed.
* 🚦 fix: Address Codex round 14 (dual aliases, summarization credit forwarding)
- A call starting with both id and index registered only the id alias,
so deltas that later dropped BOTH identifiers fell to the untracked
batch-position key and split the budget. Tallies now hold a list of
alias keys: id-bearing chunks register the batch position, and chunks
with both identifiers additionally register the id, closing the last
identifier-transition permutation. Disown-on-handoff and guarded
release apply per alias.
- The summarization node's graph adapter copied the (undefined) credit
field by value, so the guards' lazy initialization installed the
credit map on the adapter, where it survived both graph resets and
grew per compaction attempt for a retained reused chunk object. The
adapter now forwards the field through an accessor pair onto the
graph, restoring reset semantics.
* 🚦 fix: Address Codex round 15 (tally adoption, parsed-name overrides, sibling abort)
- A later delta that ADDS an identifier (id-only start, id+index delta)
changed the primary key and split the budget. A missing primary now
adopts the call's existing tally through its id — and only its id:
the batch position is a weak identity that parallel calls share, so
adopting through it would merge distinct budgets.
- Anonymous raw chunks paired with a named parsed call in the same
event were judged at the global cap before the name could select a
per-tool override; the argument guard now resolves names from the
event's parsed calls (by id) before judging.
- Parallel sibling subagents kept streaming on the parent's signal
after one child tripped a limit and the batch rejected. The executor
composes a per-executor abort with the parent signal and aborts it
before rethrowing a StreamLimitExceededError, one-way by design since
a tripped breaker ends the run.
* 🚦 fix: Address Codex round 16 (breaker scope, abort ordering, key namespaces)
- The sibling abort was executor-private, but MultiAgentGraph builds one
executor per agent node; the graph now owns a run-scoped breaker
controller shared by every executor (recreated by both reset paths so
a reused graph starts unaborted), and executors compose it into child
signals via accessors read per spawn.
- The abort fired only after forwarding.drain() and the terminal update
were awaited, leaving siblings streaming for that interval; the scope
now trips first in the catch, before any observational work.
- Tally keys are namespaced by identity kind (i:/c:/#) so index 0 and
id "0" cannot alias distinct calls onto one tally.
- Id-less indexed chunks adopt an existing anonymous batch-position
tally (singular per position, so the association is unambiguous),
covering the anonymous-start-then-indexed transition.
- Id-less raw chunks resolve their name from the event's parsed call
when exactly one is present, so per-tool overrides apply without an id
to correlate on.
All three behavioral streamLimits fixes ship regression tests that fail
with the changes stashed.
* test: update co-located tally key assertions to namespaced keys
The round-16 identity-kind namespacing (i:/c:/#) changed raw tally key
shapes; the co-located unit assertions still expected the un-prefixed
format.
* 🚦 fix: Address Codex round 17 (graph-wide breaker, dual-identifier seals)
- The top-level limit rethrow in createCallModel never aborted parallel
work: the run-scoped breaker (generalized from the subagent scope) is
now composed into every model invocation's signal and tripped before
the rethrow, so sibling agent nodes' in-flight provider calls stop
consuming quota while the rejection propagates.
- A single-call seal carrying both index and id never matched a sealing
chunk that carried only the id, so an OpenAI-style full-argument
restatement was added to the tally (false trips) and an empty seal
left the tally unreleased. Either supplied identifier now suffices,
matching the eager-call seal handling.
- Tallies track every key they are registered under (primary, migrated
primaries, aliases) and release deletes them all — sealing through
one identity previously left entries behind under another.
* 🚦 fix: Address Codex round 18 (canonical chunk links, anonymous-id adoption, onChunk enforcement)
- Bedrock Converse yields an enriched chunk while emitting the original
object through the callback path, so identity-keyed credits charged
both representations of one emission: tool arguments falsely tripped
near half the cap and event budgets halved. The adapter now links the
callback copy to its canonical message via a non-enumerable symbol,
and claim accounting dereferences the link.
- An anonymous call whose later delta adds only an id moved to a fresh
id-keyed budget; adoption now also considers the batch-position tally,
guarded to tallies CREATED anonymous (first key is the position) so a
live id-bearing parallel call's alias can never be merged.
- The public attemptInvoke onChunk branch bypassed every guard for
external consumers; it now charges producer-side before invoking the
callback, and the summarization onChunk charges consumer-side so the
two pair to exactly one effective charge whether the closure runs
through attemptInvoke or directly.
* 🚦 fix: Address Codex round 19 (fallback breaker, invalid calls, index-only aliases)
- A fallback stream tripping a limit propagated through the nested
fallback catch without aborting the run-scoped breaker; it now trips
before rethrowing, same as the primary path.
- invalid_tool_calls bypassed the complete-call argument check even
though ToolNode processes and promotes them — a malformed oversized
call is the exact incident pathology. Both complete-call sites now
judge the combined parsed+invalid view.
- Index-only calls register their batch-position alias once, at tally
creation, so later deltas that drop the index share the budget while
the per-delta hot path stays free of alias work.
* 🚦 fix: Address Codex round 20 (keep tripped breaker aborted through cleanup)
A rejected parallel batch reaches processStream cleanup while sibling
subagents can still be pre-invoke; recreating the tripped controller in
clearHeavyState handed those stragglers a fresh un-aborted signal, so a
provider request could start after the run had already failed. The
tripped controller now survives end-of-run cleanup and is recreated
only when resetValues starts the next run.
* 🚦 fix: Address Codex round 21 (breaker-abort translation, invalid-call names, name fragments)
- A sibling tripping the shared breaker can surface in another branch as
a generic provider abort error, which fell through to overflow
planning and fallbacks; both catches now detect the tripped breaker
and rethrow its stream-limit reason before any recovery work.
- Raw-chunk name correlation now sees the combined parsed+invalid call
view, so an unnamed raw chunk twinned with a named invalid call
selects that tool's override instead of the global cap.
- Tool names streamed in fragments accumulate (identical repeats are
no-ops, seal restatements replace, mirroring byte semantics), and a
changed name re-judges the tallied bytes, so per-tool overrides are
selected by the completed name rather than its first fragment.
Declined: adopting an id-created position alias from an index-only
chunk — that is the parallel-merge ambiguity the round-15 regressions
pinned, and kind-switching single calls cannot compose through standard
chunk merging.
* 🚦 fix: Address Codex round 22 (seal identifier veto, child-graph breaker translation)
- A seal carrying both identifiers no longer lets an index mismatch
veto a matching id: either identifier agreeing seals the chunk,
consistent with the eager-call seal handling, so restatements with
drifted indices replace the tally instead of doubling it.
- Child graphs own separate breaker controllers, so a parent trip
arriving through the composed constructor signal as a translated
generic abort could enter child recovery. Both recovery guards now
resolve the tripped reason from either the graph's own controller or
its constructor signal via one shared helper.
* 🚦 fix: Address Codex round 23 (consumer-path breaker trip, zero-cost text deltas)
- A breach detected on the handler's consumer path threw without
tripping the shared graph breaker, and the producer deliberately skips
once the consumer has claimed the emission, so createCallModel's trip
never fired for it: in a fan-out, siblings kept consuming quota. The
handler now trips graph.breakerAbort before rethrowing.
- The claim mechanism allocated a WeakMap entry plus a nested Map for
every ordinary text delta on both paths, violating the documented
zero-cost-disabled behavior. A shared predicate now skips charge
accounting entirely when the event cap is off and the chunk carries no
raw, parsed, or invalid tool calls; both sides use the same predicate
on the same chunk, so claim pairing is unaffected.
* 🚦 fix: Address Codex round 24 (execution-bound signals, summarization breaker)
- Child signals are captured once per subagent execution instead of
re-resolved at each use: a failed run's graph reset replaces the
breaker controller, and a straggling execution re-resolving later
would read the next run's un-aborted controller and revive old-run
work.
- Summarization model attempts now compose the run's shared breaker
signal (forwarded through the summarize-node adapter) into their
invoke configs, so a sibling branch tripping a stream limit also
cancels in-flight primary and fallback summaries.
* 🚦 fix: Address Codex round 25 (attempt-bound breakers, fallback-loop and summary guards)
The generic-abort-translation guard now covers every recovery entry
point:
- Model attempts capture the breaker controller at attempt start (like
subagent executions), so trips and reason reads bind to the attempt's
own run and a reset cannot let an unwinding old branch miss its trip.
- tryFallbackProviders checks the composed signal's reason before
advancing to the next fallback, so a sibling's trip surfacing as a
generic abort cannot start further fallback work.
- Summarization recovery resolves the breaker reason before initializing
fallbacks or degrading to the metadata stub.
* 🚦 fix: Address Codex round 26 (entry-captured breakers, tool-execution breaker propagation)
- createCallModel captures the run breaker at node entry, before the
ON_CONTEXT_USAGE dispatch awaits, so a reset during those awaits cannot
rebind the attempt to a fresh controller
- the summarize node captures the breaker signal at entry and delegates the
accessor via Object.create, keeping charge-credit accessors live while
freezing the signal against mid-node resets
- ToolNode composes the run breaker into its batch config once per run():
direct tool.invoke runtimes, the ON_TOOL_EXECUTE dispatch, and eager
prestart requests now all observe breakerAbort; ToolExecuteBatchRequest
gains an optional signal hosts can forward to their executions
- composeAbortSignals moved to @/utils/misc for reuse across Graph,
ToolNode, and the eager stream path
* 🚦 fix: Address Codex round 27 (run-bound subagent trips, claim pairing, correlated names)
- SubagentExecutor captures the breaker CONTROLLER (not just its signal) at
execute() entry: breakerScope is now a controller accessor, and a
straggling child's stream-limit trip aborts the controller its execution
started under, never a later run's fresh controller
- createSummarizationChunkHandler claims producer-side so it pairs with the
run's registered wire consumer instead of double-charging every summary
chunk (which halved an opt-in event cap and double-counted tool-call
bytes); standalone callers keep single-sided enforcement
- enforceStreamedToolCallArgLimit prefers an id-correlated complete call's
name over a raw fragment, so a chunk named 'create_' with a same-id
'create_file' parsed call resolves the per-tool override instead of
tripping the global cap
* 🚦 fix: Address Codex round 28 (at-entry trip rejection, per-run breaker replacement)
- model nodes resolve the captured breaker's tripped reason at entry and
rethrow before hooks or the provider call, so a custom provider that does
not synchronously reject an aborted signal cannot start another model
request on a failed run
- the summarize node performs the same at-entry check on its captured
signal before dispatching steps, hooks, or the model call
- resetValues installs a fresh breaker controller at every non-resume run
start, not just after a trip: a run failing on an ordinary error leaves
the controller un-aborted, and stragglers still settling hold their
entry-time capture — a late trip on that old controller must not cancel
the new run
* 🚦 fix: Address Codex round 29 (batch/queued-event trip rejection, parent-signal coverage, placeholder-id and positional-name identity)
- ToolNode.run() rethrows an already-tripped composed signal's stream-limit
reason at batch entry, before hooks, direct tool.invoke calls, or
ON_TOOL_EXECUTE dispatch
- ChatModelStreamHandler stops queued sibling events once the shared
breaker has tripped, before content handling or eager-tool dispatch
- summarization checks BOTH the graph breaker and the composed config
signal (a subagent child graph receives a root sibling's trip only
through its invocation signal) at node entry and before fallback/stub
recovery, via a shared findStreamLimitAbortReason helper
- empty-string tool-call ids are placeholders, not identities: keying,
aliases, adoption, seals, and name correlation now treat them as absent
(mirroring getEagerToolChunkKey), so parallel index-less calls with ''
ids stay on separate budgets
- positional single-parsed-call name correlation now also requires exactly
one raw chunk in the event, so a lone parseable call's override cannot
leak to unrelated id-less partial calls
* 🚦 fix: Address Codex round 30 (summary-breach trips, stage rechecks, epoch-bound consumer trips, pre-invoke recheck)
- the summarization chunk handler trips the run breaker (entry-captured
controller via the adapter's getBreakerController) before rethrowing a
breach: its producer claim wins the race, so the wire consumer's
breaker-aborting catch never fires for the same chunk and siblings kept
consuming quota
- ToolNode rechecks the composed signal before each later stage — the
regular group after the interrupting group settles, and dispatchToolEvents
entry — so a tool that ignores cancellation cannot lead the batch into
fresh side effects on a failed run
- consumer-side breaker trips in ChatModelStreamHandler bind to the event's
run: attempts stamp a breaker epoch (STREAM_LIMIT_EPOCH_KEY) into their
metadata, resetValues increments it with the controller swap, and a
stale-epoch straggler neither trips nor consults the controller now
serving a newer run
- createCallModel rechecks the captured attemptBreaker immediately before
attemptInvoke, closing the window where a sibling trips during the
pre-invoke awaits
* 🚦 fix: Address Codex round 31 (stale-epoch event drops, hook-window rechecks, summary epoch stamps)
- ChatModelStreamHandler drops stamped events whose breaker epoch
mismatches the live one outright: a dead run's final tool-call chunk
could otherwise reach the eager paths, which compose the NEW run's live
controller
- the handler rechecks the run breaker after its awaits (tool-call
handling, run-step dispatch) immediately before both eager dispatch
paths
- ToolNode rechecks at the last moment before tool.invoke and before
dispatching the approved host batch, closing the PreToolUse-hook window
on both execution paths
- the summarize node captures the breaker epoch at entry (adapter
getBreakerEpoch) and stamps it into summary attempt metadata, so the
wire consumer epoch-gates old-run summary chunks like model-attempt
chunks
Declined (with rationale on the PR): cumulative-restatement
reconciliation in the byte tally — the tally deliberately measures the
canonical verbatim concatenation langchain builds; prefix-restatement
replacement reopens an unbounded undercount (k restatements at constant
tally = k× canonical bytes), and sealed restatements already replace.
* 🚦 fix: Address Codex round 32 (sparse-position identity, summarize pre-call and fallback pre-invoke rechecks)
- batch-position aliases register only for single-chunk events: sparse
parallel events reuse position 0 for whichever call continues, and an
anonymous continuation landing on an identified call's tally would
inherit that tool's raised override and bypass the global cap
- the summarize node rechecks the entry-captured breaker (and the composed
config signal) immediately before executeSummarizationWithFallback,
after the dispatchRunStep/ON_SUMMARIZE_START/PreCompact awaits
- tryFallbackProviders checks the composed signal after message
preparation and before every fallback invocation — the catch only sees
attempts that throw, so a provider ignoring an aborted signal and
succeeding would resolve a run that must reject
* 🚦 chore: Clean Up Stream Limit Lint Warnings
* 🚦 fix: Address Codex round 33 (ambiguous-delta limits, sole-tally continuity, straggler accounting, drain stop, disabled-guard bookkeeping)
- ambiguous sparse deltas are judged under each live candidate's own
name-specific limit, not just the global cap — a call with a LOWER
per-tool override could otherwise stream past it while its chunks stay
anonymous
- with exactly one live call, an anonymous continuation charges that
tally instead of opening a fresh position tally that would reset the
sole remaining call's byte budget
- stream-limit accounting (tallies, event counts, charge credits) now
survives clearHeavyState: cleanup runs while sibling attempts can still
be unwinding on the retained breaker, and clearing would hand a
cancellation-ignoring provider a fresh budget; resetValues clears at
the next run start, where the epoch bump already drops stamped
straggler events
- all three attemptInvoke drain loops check the composed signal on every
yielded chunk and rethrow a stream-limit trip, so adapters that ignore
cancellation stop consuming the provider stream
- requiresStreamLimitAccounting returns false when the byte cap is off
with no per-tool overrides and the event cap is off — fully disabled
guards no longer pay per-chunk claim allocations
* 🚦 fix: Epoch-Graced Accounting Sweep, Zero-Override Bookkeeping Skip, Lint Zero
- resetValues sweeps stream-limit accounting by creation epoch instead of
clearing: producer loops of straggling attempts use the graph's maps
directly and sit outside the consumer-only epoch gate, so a clear handed
a cancellation-ignoring provider a fresh allowance at every run start.
Entries tagged with the ending epoch survive exactly one reset (keeping
stragglers on their original budgets); older entries are swept. Event
counts carry the same tag (Map values are now {count, epoch}), and the
summarize adapter forwards the live epoch so summary tallies participate
- resolveStreamLimits precomputes hasEnforceableToolCallArgLimit; the
accounting gate and both enforcement early-returns use it, so a config
of global 0 plus only zero-valued per-tool disables allocates nothing
- lint: zero warnings — the disposed-graph guards in getRunMessages/
getContentParts/getRunSteps read through runtime-honest widened locals,
and the no-allocation tests assert toBeUndefined directly
* 🚦 fix: Immutable Run Scope — Attempt Leases, Post-Await Scope Revalidation, Batch-Bound Subagents
Round 34 (all three P1s), unified under one immutable run/attempt scope:
- attempt-lifetime accounting: attemptInvoke leases its generation at
entry and releases it (with its tallies and event counts) from finally;
the resetValues sweep exempts leased generations, so retention follows
the ATTEMPT — a cancellation-ignoring straggler keeps its original
budget across any number of run resets, not a fixed grace count.
Summarization attempts lease via a new streamLimitState param (accounting
only, never claim-side), with adapter accessors for the active set
- graph.runScope: frozen {epoch, controller} replaced as ONE object per
reset. The stream handler captures it at entry and revalidates by
REFERENCE after every awaited step, dropping the event before either
eager dispatch path — a handler resuming after a reset can no longer
dispatch a stale tool with the new run's controller and config
- subagents bind to the TOOL BATCH's entry-captured controller: ToolNode
stamps the batch scope into its derived config (before PreToolUse
hooks), the subagent tool passes it as params.breaker, and execute
prefers it over the live accessor — a reset during a hook cannot rebind
the child to the new run's controller. The scope key is stripped from
host batch requests and child-graph configurables
- lint: touched files at zero warnings (runtime-honest widening for
stub-tolerant checks; redundant seal-kind arm dropped)
* 🚦 fix: Preserve Sparse Call Identity and Late Names
* 🚦 fix: Preserve Sole Anonymous Budget on Late Index
* 🚦 fix: Guard Sole Anonymous Late-Index Fallback
* 🚦 fix: Gate the Attempt Lease When Every Guard Is Disabled (self-review sweep)
Self-review across the six accumulated bug classes found one residual
instance, in disabled-path allocation: attemptInvoke took its accounting
lease (generation-key computation + active-set entry) even when no guard
could fire. New streamLimitAccountingEnabled predicate gates the lease;
regression test pins that a fully disabled graph takes no lease. All
other classes verified clean — evidence in the PR discussion.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )