Skip to content

fix: send budget notices newest-first and mark them sent only after a successful request - #11448

Merged
lpcox merged 6 commits into
mainfrom
copilot/fix-budget-notices-order
Nov 11, 2026
Merged

lpcox merged 6 commits into
mainfrom
copilot/fix-budget-notices-order

Conversation

Copilot AI commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Each crossed budget threshold (credits or time) waits in a pending set until it is sent as a notice. This change fixes two problems in how those notices are sent:

  • Out of order. If one request crosses both 80% and 90%, the 90% notice goes first and the 80% notice follows on the next request. That tells the agent it has more room than it was just told.
  • Lost. A notice was marked as sent while the request was being rewritten, before the provider answered. If that request then failed, the notice was never sent again.

Notice state (guards/steering-thresholds.js)

Both guards (ai-credits-guard.js, timeout-steering.js) share three new helpers:

  • selectPendingSteeringThreshold returns the highest pending threshold that is above every notice of the same kind still on an unfinished request. So a request never gets the same notice or an older one while another request is carrying one.
  • acknowledgeSteeringThreshold marks a notice as sent and removes every pending threshold at or below it, so superseded notices are dropped.
  • claimSteeringThreshold reserves a notice for one request and returns a handle that resolves once: acknowledge() on success, release() on failure.

Request lifecycle

  • body-handler.js: when it adds a notice to a request, it now reserves it (req.awfSteeringDelivery = claim(...)) instead of marking it sent.

  • upstream-response.js: when the upstream response is 2xx and finishes, it calls acknowledge().

  • proxy-request.js: it calls release() when:

    • rewriting the request body throws;
    • a guard blocks the request;
    • the response closes without finishing (upstream error or client disconnect);
    • the client disconnected while the body was being rewritten, before dispatch.

    The response close comes after the response-end handlers, so the release does nothing on success.

pending {80, 90}  → request A carries 90 (reserved)
  concurrent B    → no notice (80 and 90 are not above the reserved 90)
  A → 500         → release; pending stays {80, 90}
  C carries 90    → 2xx → acknowledge; pending {} (80 dropped, never sent)

Review notes

  • Trade-off: a long streaming request carrying 80% holds back the 80% notice from other requests until it ends. A newly crossed 90% can still go out meanwhile. Dropping the reservation would allow duplicate notices under concurrency, which breaks the at-most-once check (T2) in gh-aw's routed-steering smoke test. That smoke test's check T1 still sees an 80% notice, because it crosses 80% on its own.
  • AI-credit config change: the pending, emitted and in-flight sets are now replaced rather than cleared. A request still holding a notice from before the change therefore cannot change the new state.
  • Updated tests: four existing tests asserted the old behaviour (older notices following newer ones, and a rejected request using up its notice) and were updated. docs/api-proxy-sidecar.md is updated to describe the new rules.

Copilot AI and others added 2 commits November 11, 2026 05:13
Co-authored-by: rabeckett <5445735+rabeckett@users.noreply.github.com>
Co-authored-by: rabeckett <5445735+rabeckett@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix issue with budget notices arriving out of order api-proxy: send budget notices newest-first and mark them sent only after a successful request Nov 11, 2026
Copilot AI requested a review from rabeckett November 11, 2026 05:17
@rabeckett
rabeckett requested a max review from Copilot November 11, 2026 05:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Notice acknowledgment and release do not consistently account for downstream completion and streaming terminal outcomes, allowing notices to be lost on disconnects or failures, or repeated after successful delivery.

2 open issues
What changed in this PR

Updates the API proxy’s time and AI-credit budget notices to prioritize newer thresholds and track delivery across request outcomes, addressing #11444.

Changes:

  • Add shared threshold selection, supersession, and request-reservation helpers.
  • Connect notice acknowledgement and release to the request lifecycle.
  • Update regression coverage and delivery documentation.
File Description
docs/​api-proxy-sidecar.md Documents notice ordering, delivery, and retry rules.
containers/​api-proxy/​upstream-response.js Acknowledges notices when 2xx upstream responses end.
containers/​api-proxy/​server.token-steering.test.js Covers failed-request redelivery and client disconnects.
containers/​api-proxy/​proxy-request.js Releases notice claims on rejection, transformation failure, or closure.
containers/​api-proxy/​guards/​timeout-steering.js Uses shared selection and reservation helpers for time notices.
containers/​api-proxy/​guards/​steering-thresholds.js Implements threshold selection, supersession, and one-shot claim handles.
containers/​api-proxy/​guards/​ai-credits-guard.test.js Tests supersession and in-flight credit-notice handling.
containers/​api-proxy/​guards/​ai-credits-guard.js Tracks credit-notice claims and isolates reconfigured state.
containers/​api-proxy/​body-handler.test.js Tests notice ordering, reservation, release, and acknowledgement.
containers/​api-proxy/​body-handler.js Reserves injected notices instead of immediately consuming them.

Review effort: Max

Comment thread containers/api-proxy/proxy-request.js Resolved
@rabeckett

Copy link
Copy Markdown
Collaborator

@copilot please address the two open review threads on this PR. Both come down to one rule for when a notice counts as delivered: when a complete, successful reply exists, not when the upstream connection happens to end.

  • Streaming responses: acknowledge the notice once the upstream stream's terminal success event has been seen (Responses response.completed, Messages message_stop, the final Chat Completions chunk or [DONE]), even if the client closes before upstream EOF. Codex does close early. A 2xx stream that ends with response.failed or an error event, or that is cut off before any terminal event, counts as a failure and releases the notice.
  • Non-streaming responses: acknowledge only after the 2xx upstream body has ended and the downstream response has finished; if the client disconnects before that, the close handler releases the notice.

Please keep this simple. The token tracker already inspects SSE streams for usage and terminal events, including through the Chat/Responses wire translation, so reuse that detection instead of adding a second stream parser. Keep the existing release paths (transform failure, guard block, close before dispatch). Add regression tests for:

  1. a stream closed by the client after response.completed but before upstream EOF (notice not repeated);
  2. a 200 stream ending in response.failed (notice sent again on the next request);
  3. a non-streaming 2xx where the client disconnects before finish (notice sent again).

Keep the scope to this issue (#11444); #11443 is changing body-transform.js in parallel. Please update the delivery rules in docs/api-proxy-sidecar.md to match.

Co-authored-by: rabeckett <5445735+rabeckett@users.noreply.github.com>

Copilot AI commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please address the two open review threads on this PR. Both come down to one rule for when a notice counts as delivered: when a complete, successful reply exists, not when the upstream co...

Implemented and pushed as 219e1ab. Streaming notices now settle from shared terminal-event inspection; non-streaming notices wait for both upstream end and downstream finish. Added the three requested regressions and updated the delivery docs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread containers/api-proxy/upstream-token.js Resolved
Comment thread containers/api-proxy/upstream-token.js Resolved
Comment thread containers/api-proxy/upstream-token.js Resolved
Co-authored-by: rabeckett <5445735+rabeckett@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved acknowledgement gaps can duplicate steering records for usage-bearing replies and redeliver notices for Gemini JSON streams after client disconnects and retries.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Defer steering attribution until delivery is acknowledged

containers/​api-proxy/​upstream-response.js:161

For a usage-bearing non-streaming 2xx reply, token-tracker-http.js records the request's steering field on upstream end, before downstream finish acknowledges the notice. If the client disconnects in that gap, the notice is released and its retry produces a second record for the same threshold, failing the routed-steering T2 at-most-once check. Preserve usage accounting, but defer steering attribution until delivery is acknowledged, and cover this disconnect/retry with usage-bearing responses.

Medium severity Recognize Gemini JSON stream responses for acknowledgment

containers/​api-proxy/​upstream-response.js:381

Gemini's :streamGenerateContent also supports streaming JSON responses without ?alt=sse; those arrive as application/json, so isStreaming is false here. A client that consumes the completed stream and closes before EOF releases the already-delivered notice, and the next request repeats it. Recognize this endpoint as streaming for steering acknowledgement while preserving the existing JSON response handling.

6 resolved since last review

Review effort: Max

Co-authored-by: rabeckett <5445735+rabeckett@users.noreply.github.com>
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Smoke Claude passed

Generated by Smoke Claude for #11448

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Security Guard completed successfully!

Security Guard

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Security Guard · auto (Claude Sonnet 5.5) · ~11 AIC · ⊞ 9.0K · ◷

Generated by Security Guard for #11448

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

🔌 Smoke Services — All services reachable! ✅

🔌 Service connectivity validated by Smoke Services

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

🛡️ Smoke Copilot Network Isolation confirmed the egress allowlist is enforced. ✅

Smoke Copilot Network Isolation

Outcome: reported_action

The agent completed and reported one or more actions. The no-op output is a completion signal, not a claim that no action was taken.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Smoke Copilot Network Isolation · auto (Claude Sonnet 5.5) · ~15 AIC · ⊞ 9.8K · ◷

🛡️ Egress verdict from Smoke Copilot Network Isolation

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Smoke Cloud Hypervisor completed. Cloud Hypervisor + Copilot passed.

Cloud Hypervisor + Copilot smoke test by Smoke Cloud Hypervisor

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Build Test Suite completed successfully!

Generated by Build Test Suite for #11448

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟

Smoke Codex

Outcome: reported_action

The agent completed and reported one or more actions. The no-op output is a completion signal, not a claim that no action was taken.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Smoke Codex · gpt54 · ~14 AIC · ⊞ 22.5K · ◷

🔮 The oracle has spoken through Smoke Codex

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤

📰 BREAKING: Report filed by Smoke Copilot

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.

Smoke Chroot

Outcome: reported_action

The agent completed and reported one or more actions. The no-op output is a completion signal, not a claim that no action was taken.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Smoke Chroot · auto (Claude Sonnet 5.5) · ~9 AIC · ⊞ 8.2K · ◷

Tested by Smoke Chroot

@github-actions

Copy link
Copy Markdown
Contributor

Cloud Hypervisor + Copilot smoke test

  1. Merged PR lookup: PASS
  2. github.com connectivity: PASS (HTTP 200)
  3. File write/read: PASS
  4. example.com blocked: PASS (HTTP 000)

Overall: PASS

Cloud Hypervisor + Copilot smoke test by Smoke Cloud Hypervisor
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Smoke Test: Claude Engine

Check Result
API status ✅ PASS
GitHub check ✅ PASS
File status ✅ PASS

Overall result: PASS

Generated by Smoke Claude for #11448 · claude · haiku45 · ~36 AIC · ⊞ 6.3K · ◷
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Merged PRs: chore: upgrade gh-aw workflows; [Test Quality] Pin compose sanitizer structure preservation
PR details: ✅
Playwright: ❌
File write/read: ✅
Discussion lookup/comment: ✅
Build AWF: ✅
Overall: FAIL

🔮 The oracle has spoken through Smoke Codex
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Smoke Test: Copilot BYOK Mode ✅ PASS

Test Result Details
GitHub MCP ✅ Connected; fetched 2 recent PRs
HTTP Connectivity ✅ HTTP 200 to github.com
File I/O ✅ Read smoke test marker file
BYOK Inference ✅ Direct mode via api-proxy → api.githubcopilot.com

Mode: Direct BYOK (COPILOT_PROVIDER_API_KEY) — agent sees placeholder, sidecar holds real key.

CC: @lpcox

🔑 BYOK report filed by Smoke Copilot BYOK
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

OTEL smoke (re-verified locally):

  • ✅ 1 Module: otel.js loads (startRequestSpan, setTokenAttributes, setBudgetAttributes, endSpan, endSpanError, routerCallTracing, shutdown, isEnabled, ...)
  • ✅ 2 Tests: 3 suites, 75/75 passed
  • ✅ 3 Env forwarding: trace ID/parent span ID in env-passthrough.ts; OTLP vars in api-proxy-env-config.ts
  • ✅ 4 Token tracker: onUsage hook present in token-tracker-http.js
  • ✅ 5 Diagnostics: /tmp/gh-aw/otel.jsonl present (see run output for span count)

📡 OTel tracing validated by Smoke OTel Tracing
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Smoke test (Copilot):

  • Latest PR: "Use published AWF artifacts for refactoring scanner"
  • GitHub MCP ✅
  • GitHub.com connectivity ✅
  • File write/read ✅

Overall: PASS — @Copilot @rabeckett

📰 BREAKING: Report filed by Smoke Copilot
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia ✅ 1/1 passed ✅ PASS
Bun hono ✅ 1/1 passed ✅ PASS
C++ fmt ✅ N/A ✅ PASS
C++ json ✅ N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world ✅ N/A ✅ PASS
.NET json-parse ✅ N/A ✅ PASS
Go color ✅ ok ✅ PASS
Go env ✅ ok ✅ PASS
Go uuid ✅ ok ✅ PASS
Java gson ❌ 0/0 (not run) ❌ FAIL
Java caffeine ❌ 0/0 (not run) ❌ FAIL
Node.js clsx ✅ passed ✅ PASS
Node.js execa ✅ passed ✅ PASS
Node.js p-limit ✅ passed ✅ PASS
Rust fd ✅ 1/1 passed ✅ PASS
Rust zoxide ✅ 1/1 passed ✅ PASS

Overall: 7/8 ecosystems passed — FAIL

Java failure: Maven could not create ~/.m2/repository (LocalRepositoryNotAccessibleException). With a writable repo dir, dependency resolution failed: java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty (Java truststore is empty in this environment). The build-test label was not added.

.NET needed a writable DOTNET_CLI_HOME/XDG_RUNTIME_DIR (/run/user/1001 is missing) to pass.

Generated by Build Test Suite for #11448 · copilot · auto (Claude Sonnet 5.5) · ~17 AIC · ⊞ 5.9K · ◷
Add label ready-for-aw to run again

@github-actions github-actions Bot added the smoke-copilot-network-isolation Copilot network-isolation egress smoke test label Nov 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

EGRESS_RESULT allow=pass deny=pass

  • ✅ Allowed domain (api.github.com): HTTP 200
  • ✅ Blocked domain (example.com): blocked
  • Overall: PASS

cc @lpcox

🛡️ Egress verdict from Smoke Copilot Network Isolation
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Chroot Version Comparison

Runtime Host Version Chroot Version Match?
Python Python 3.12.16 Python 3.12.16 ✅ YES
Node.js v24.22.2 v22.23.2 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Result: Not all tests passed (Node.js version mismatch), so the smoke-chroot label was not added.

Tested by Smoke Chroot
Add label ready-for-aw to run again

@github-actions

Copy link
Copy Markdown
Contributor

Services smoke test: PASS

  • ✅ Redis PING → PONG
  • ✅ pg_isready → accepting connections
  • ✅ psql SELECT 1 → 1

🔌 Service connectivity validated by Smoke Services
Add label ready-for-aw to run again

@lpcox
lpcox enabled auto-merge (squash) November 11, 2026 18:36
@lpcox
lpcox merged commit 64d8dcc into main Nov 11, 2026
155 of 170 checks passed
@lpcox
lpcox deleted the copilot/fix-budget-notices-order branch November 11, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Budget notices can arrive out of order or get lost when a request fails

4 participants