Skip to content

[pull] main from danny-avila:main - #160

Merged
pull[bot] merged 4 commits into
innFactory:mainfrom
danny-avila:main
Aug 5, 2026
Merged

[pull] main from danny-avila:main#160
pull[bot] merged 4 commits into
innFactory:mainfrom
danny-avila:main

Conversation

@pull

@pull pull Bot commented Aug 5, 2026

Copy link
Copy Markdown

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 : )

dustinhealy and others added 4 commits August 4, 2026 17:01
… 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.
@pull pull Bot locked and limited conversation to collaborators Aug 5, 2026
@pull pull Bot added the ⤵️ pull label Aug 5, 2026
@pull
pull Bot merged commit 2a7c24b into innFactory:main Aug 5, 2026
2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants