fix(codex): fail fast and release per-account Responses WS leases (#12911)

* fix(codex): fail fast and release per-account Responses WS leases

* chore(changelog): add fragment for Codex WS lease fail-fast fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(codex): carry the reasoning-rule context through the leased WS path

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
initguru
2026-09-17 22:56:01 +09:00
committed by GitHub
parent c3e966eeb9
commit 36df9e505a
9 changed files with 642 additions and 92 deletions

View File

@@ -430,6 +430,9 @@ class ResponsesWsSession {
this.firstResponseBody = null;
this.currentRequestBody = null;
this.preparedContext = null;
this.leaseId = null;
this.leaseReleased = false;
this.leaseReleaseInFlight = false;
// #7388: logging must be scoped per logical turn (one `response.create`
// through its terminal event), not once for the lifetime of the WS
// connection — a single boolean here silently dropped every turn after
@@ -640,6 +643,23 @@ class ResponsesWsSession {
toStringOrNull(responseBody.service_tier) || toStringOrNull(responseBody.serviceTier),
};
// A reused WS connection re-runs prepare per logical turn, and each prepare
// acquires a fresh per-account lease. Release the previous turn before
// adopting the new lease so one session cannot hoard account slots.
const previousLeaseId = this.leaseId;
const newLeaseId = toStringOrNull(prepared.json?.leaseId);
if (this.closed) {
this.leaseId = null;
this.releaseLeaseId(newLeaseId);
return prepared;
}
this.leaseId = newLeaseId;
if (previousLeaseId && previousLeaseId !== newLeaseId) {
this.releaseLeaseId(previousLeaseId);
}
this.leaseReleased = false;
this.leaseReleaseInFlight = false;
return prepared;
}
@@ -770,6 +790,35 @@ class ResponsesWsSession {
}
}
releaseLease() {
if (this.leaseReleased || this.leaseReleaseInFlight || !this.leaseId) return;
this.leaseReleaseInFlight = true;
const leaseId = this.leaseId;
void callInternal(this.fetchImpl, this.baseUrl, this.bridgeSecret, "release", { leaseId })
.then((response) => {
if (!response.ok) throw new Error("lease release rejected");
this.leaseReleased = true;
this.leaseId = null;
})
.catch(() => {
this.leaseReleaseInFlight = false;
const retry = setTimeout(() => this.releaseLease(), 1000);
retry.unref?.();
});
}
releaseLeaseId(leaseId) {
if (!leaseId) return;
void callInternal(this.fetchImpl, this.baseUrl, this.bridgeSecret, "release", { leaseId })
.then((response) => {
if (!response.ok) throw new Error("lease release rejected");
})
.catch(() => {
const retry = setTimeout(() => this.releaseLeaseId(leaseId), 1000);
retry.unref?.();
});
}
async persistHistory({
status = 200,
success = true,
@@ -820,6 +869,7 @@ class ResponsesWsSession {
close(code = 1000, reason = "normal_closure") {
if (this.closed) return;
this.closed = true;
this.releaseLease();
clearInterval(this.pingTimer);
this.cleanupBuffers();
@@ -847,6 +897,7 @@ class ResponsesWsSession {
dispose() {
if (this.closed) return;
this.closed = true;
this.releaseLease();
clearInterval(this.pingTimer);
this.cleanupBuffers();
try {