Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
d2088ba
fix(integration): restore codex and memory pipeline contracts (#4474)
KooshaPari Jun 21, 2026
84626f6
perf(dashboard): shrink provider assets and fix usage rollup cutoff (…
KooshaPari Jun 21, 2026
ac65652
fix(antigravity): strip Claude/OpenAI thinking fields from Cloud Code…
diegosouzapw Jun 21, 2026
0299907
fix(auto): enforce quota cutoff before scoring (#4483)
megamen32 Jun 21, 2026
335df93
chore(release): open v3.8.33 development cycle
Jun 21, 2026
75a84b0
feat(mcp): add omniroute_web_fetch tool for URL content extraction (#…
ponkcore Jun 21, 2026
296b72d
feat(models): qualify duplicate model names with provider prefix (#4516)
Rahulsharma0810 Jun 21, 2026
3959242
chore(i18n): remove unused config helpers (#4482)
KooshaPari Jun 21, 2026
69f6389
chore(quality): reconcile file-size baseline for #4510 web_fetch tool…
diegosouzapw Jun 21, 2026
d3f1b23
feat(usage): API-key USD quota percent + reset hints, weekly window c…
Witroch4 Jun 21, 2026
694adf7
fix(combo): allow fallback on context-overflow & param-validation 400…
adivekar-utexas Jun 21, 2026
c5f3d5f
fix(db): scheduled VACUUM + persist lastVacuumAt (#4480, #4437)
KooshaPari Jun 21, 2026
cda5831
fix(command-code): cap max_tokens per model using registry maxOutputT…
adivekar-utexas Jun 21, 2026
5b15e01
chore(quality): reconcile file-size baseline for #4519 (#4528)
diegosouzapw Jun 21, 2026
bb4aa0d
chore(quality): rebaseline cognitiveComplexity 783->792 (#4529)
diegosouzapw Jun 21, 2026
f3253ee
fix(sse): crypto-secure RNG for combo/deck load-balancing selection (…
diegosouzapw Jun 21, 2026
185c474
feat(sse): route web_search requests to a configured model (#4509, #4…
diegosouzapw Jun 21, 2026
58d1cc8
feat(compression): Phase 2 — named profiles + active selector (#4521)
diegosouzapw Jun 21, 2026
aee1f74
feat(providers): expose gpt-4o on built-in GitHub Copilot (gh) provid…
diegosouzapw Jun 21, 2026
81016bd
feat(pricing): add default pricing for Qwen coder-model on qw provide…
diegosouzapw Jun 21, 2026
482433e
feat(translator): accept OpenAI audio input parts in Gemini translati…
diegosouzapw Jun 21, 2026
e1546c0
fix(api): dedupe exact-duplicate ids in /v1/models (#4506, #4424)
diegosouzapw Jun 21, 2026
4279751
fix(sse): skip disabled providers in combo fallback (#4500)
diegosouzapw Jun 21, 2026
cd45943
fix(usage): parse numeric-string quota reset timestamps as Unix sec/m…
diegosouzapw Jun 21, 2026
a37ee2f
feat(usage): surface Codex code-review weekly window + additional_rat…
diegosouzapw Jun 21, 2026
3a0c40b
fix(usage): clear auth-expired message for Kiro social-auth accounts …
diegosouzapw Jun 21, 2026
bb8104f
chore(quality): reconcile file-size baseline (owner batch1 #4488/#450…
diegosouzapw Jun 21, 2026
dac9f7d
feat(dashboard): per-provider dropdown filter on the quota dashboard …
diegosouzapw Jun 21, 2026
247e578
feat(dashboard): Done button in model picker for combo creation (#4496)
diegosouzapw Jun 21, 2026
89606a2
feat(dashboard): toggle-style model deselection in combo builder moda…
diegosouzapw Jun 21, 2026
4cf7342
fix(dashboard): migrate ManualConfigModal copy to shared useCopyToCli…
diegosouzapw Jun 21, 2026
d83e0a4
fix(dashboard): show API-Key-compatible providers in Antigravity CLI …
diegosouzapw Jun 21, 2026
bb16f88
fix(dashboard): enable Codex Apply/Reset buttons when the CLI is inst…
diegosouzapw Jun 21, 2026
840413f
feat(dashboard): inline show/hide toggle for API keys on API Manager …
diegosouzapw Jun 21, 2026
34d9335
fix(pricing): default cost rows for Antigravity Gemini 3.5 Flash tier…
diegosouzapw Jun 21, 2026
96d7f2a
fix(mitm): graceful sudo degradation in slim Docker / non-root contai…
diegosouzapw Jun 21, 2026
aa019bb
fix(mitm): gate sudo prompts on server platform, not browser UA (#4514)
diegosouzapw Jun 21, 2026
b28f6e9
refactor(chatCore): extract pure persistCodexQuotaState core (#4492, …
diegosouzapw Jun 21, 2026
2601a55
refactor(chatCore): extract Background Task Redirect decision (T41) (…
diegosouzapw Jun 21, 2026
9482138
chore(quality): reconcile file-size baseline (owner final) (#4535)
diegosouzapw Jun 21, 2026
9cf96d0
fix(combo): pass maxCooldownMs from settings to recordModelLockoutFai…
Chewji9875 Jun 21, 2026
9ade3e5
fix(no-think): normalize provider prefix to canonical in no-think var…
Rahulsharma0810 Jun 21, 2026
f91cf72
feat(bazaarlink): add authHint to existing APIKEY_PROVIDERS entry (#4…
adivekar-utexas Jun 21, 2026
2b21bb6
feat(webhooks): enrich telegram request notifications (#4524)
mppata-glitch Jun 21, 2026
4d3a9fd
fix(mcp): webFetchInput emits 'URL is required' for a missing url (#4…
diegosouzapw Jun 21, 2026
eb47b92
fix(compression): allow enginesExplicit through PUT validation schema…
DevEstacion Jun 21, 2026
059b7ef
chore(quality): reconcile file-size baseline (r3 #4522/#4530/#4524) (…
diegosouzapw Jun 21, 2026
8273dfd
chore(quality): rebaseline cognitiveComplexity 792->793 (r3) (#4543)
diegosouzapw Jun 21, 2026
0b402e9
refactor(chatCore): restore #4507 + #4511 leaves orphaned from releas…
diegosouzapw Jun 21, 2026
8fc3315
fix(dashboard): avoid overlapping provider health polls (#4557)
KooshaPari Jun 21, 2026
9447de0
fix(dashboard): make API Manager key table usable on mobile (#4556)
janeza2 Jun 21, 2026
448741f
feat(combos): provider-wildcard expansion in combo steps (#2562) (#4545)
Rahulsharma0810 Jun 21, 2026
cf6546e
perf(quota): skip redundant quota_snapshots from idle connections (#4…
diegosouzapw Jun 21, 2026
6b5590e
fix(auto-combo): respect model visibility (isHidden) in auto-combo ca…
herjarsa Jun 21, 2026
39b2bbf
feat(combo): add sticky weighted selection limit with exhaustion-awar…
adivekar-utexas Jun 21, 2026
6621803
chore(quality): reconcile complexity baseline 1906->1911 (review-prs …
diegosouzapw Jun 21, 2026
3ff08e2
fix(sse): RTK must preserve cache_control-marked tool_result blocks (…
diegosouzapw Jun 21, 2026
20099f7
fix(oauth): improve Cursor auto-import reliability on macOS (#4552)
diegosouzapw Jun 21, 2026
9746b20
fix(providers/test): probe real Codex /responses endpoint (#4551)
diegosouzapw Jun 21, 2026
3ffd6ff
fix(executors): decode Composer Cursor </think>-marked visible output…
diegosouzapw Jun 21, 2026
05c0c4e
refactor(chatCore): recupera os 4 leaves perdidos no churn de merge (…
diegosouzapw Jun 21, 2026
69c35bd
refactor(chatCore): extrai builder do usage-record de falha (#3501) (…
diegosouzapw Jun 21, 2026
4b34a75
refactor(chatCore): extrai extractSystemRoleMessages + buildUpstreamH…
diegosouzapw Jun 21, 2026
548f1fe
chore(quality): reconcile complexity baseline 1911->1913 (review-prs …
diegosouzapw Jun 21, 2026
09d8a4a
feat(combo): add nestedComboMode execute — black-box combo-ref execut…
adivekar-utexas Jun 21, 2026
7ef34c2
fix(combo): extract dynamic connection ID from headers on success pat…
diegosouzapw Jun 21, 2026
f124806
fix(api): resolve compatible provider node by base type, not only exa…
diegosouzapw Jun 22, 2026
fbcc43c
fix(cli): supervisor restarts on spontaneous exit-0 (OOM cgroup) + wa…
diegosouzapw Jun 22, 2026
57d9f64
chore(release): finalize v3.8.33 — reconcile CHANGELOG + fix 5 inheri…
Jun 22, 2026
7b7470f
fix(release): allow src/models/ in the pack-artifact policy allowlist
Jun 22, 2026
e056e07
fix(logs): make active request stale sweep configurable
rdself Jun 22, 2026
81b1ed9
Merge remote-tracking branch 'origin/release/v3.8.34' into coder/fix-…
rdself Jun 22, 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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,11 @@ APP_LOG_TO_FILE=true
# Default: 100000
# CALL_LOGS_TABLE_MAX_ROWS=100000

# Maximum age for orphaned active request log entries before the in-memory
# pending-request reaper removes them. Accepts milliseconds.
# Default: 3600000 (1 hour)
# MAX_PENDING_REQUEST_AGE_MS=3600000

# Whether call log pipeline capture stores stream chunks when enabled in settings.
# Only applies when call_log_pipeline_enabled=true.
# Default: true
Expand Down
1 change: 1 addition & 0 deletions docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ The logging system writes to both stdout and rotated log files. All configuratio
| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. |
| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. |
| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. |
| `MAX_PENDING_REQUEST_AGE_MS` | `3600000` (1 hour) | Max age for orphaned active request log entries before in-memory cleanup. |
| `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS` | `true` | Store stream chunks in pipeline artifacts when `call_log_pipeline_enabled=true`. |
| `CALL_LOG_PIPELINE_MAX_SIZE_KB` | `512` | Max pipeline call log artifact size in KB when `call_log_pipeline_enabled=true`. |
| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. |
Expand Down
31 changes: 12 additions & 19 deletions src/lib/usage/usageHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,26 +215,18 @@ const pendingRequests: {
*/
const pendingById = new Map<string, PendingRequestDetail>();

/**
* Orphaned-pending-request reaper.
*
* Pending details are normally removed when a request finalizes (clean completion,
* tracked error, client cancel). But a request that never finalizes cleanly — an
* upstream/fetch error thrown before the finalize call, a client disconnect, or a
* process-level timeout — leaves its detail in `pendingById` (and `pendingRequests.details`)
* forever, each retaining truncated request/response payload previews. Under real proxy
* traffic a steady fraction of requests terminate abnormally, so this grows monotonically
* (previously only an admin reset via clearPendingRequests() could free it).
*
* The reaper drops entries whose `startedAt` is older than MAX_PENDING_REQUEST_AGE_MS — far
* longer than any genuine request — so only truly-orphaned entries are evicted; a live
* request always finalizes long before that. MAX_PENDING_DETAILS is a hard backstop.
*/
const MAX_PENDING_REQUEST_AGE_MS = 15 * 60 * 1000;
const DEFAULT_MAX_PENDING_REQUEST_AGE_MS = 60 * 60 * 1000;
const MAX_PENDING_DETAILS = 5000;
const PENDING_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
let _pendingSweepTimer: ReturnType<typeof setInterval> | null = null;

export function getMaxPendingRequestAgeMs(
rawValue: string | undefined = process.env.MAX_PENDING_REQUEST_AGE_MS
): number {
const parsed = Number.parseInt(rawValue ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_PENDING_REQUEST_AGE_MS;
}

function ensurePendingSweepTimer(): void {
if (_pendingSweepTimer || typeof setInterval !== "function") return;
_pendingSweepTimer = setInterval(() => {
Expand All @@ -256,7 +248,7 @@ function ensurePendingSweepTimer(): void {
*/
export function sweepStalePendingRequests(
now: number = Date.now(),
maxAgeMs: number = MAX_PENDING_REQUEST_AGE_MS
maxAgeMs: number = getMaxPendingRequestAgeMs()
): number {
let removed = 0;

Expand Down Expand Up @@ -347,12 +339,13 @@ export function trackPendingRequest(
if (!pendingRequests.details[connectionId][modelKey]) {
pendingRequests.details[connectionId][modelKey] = [];
}
const now = Date.now();
const newDetail = {
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
id: `${now}-${Math.random().toString(36).slice(2, 8)}`,
model,
provider,
connectionId,
startedAt: Date.now(),
startedAt: now,
...normalizedMetadata,
};
pendingRequests.details[connectionId][modelKey].push(newDetail);
Expand Down
166 changes: 114 additions & 52 deletions tests/unit/usage-pending-sweep.test.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,114 @@
import test from "node:test";
import assert from "node:assert/strict";

const {
trackPendingRequest,
getPendingById,
getPendingRequests,
sweepStalePendingRequests,
clearPendingRequests,
} = await import("../../src/lib/usage/usageHistory.ts");

test("sweepStalePendingRequests evicts orphaned pending details and self-heals counts", () => {
clearPendingRequests();

// One request that will be treated as orphaned (never finalized), one fresh.
const staleId = trackPendingRequest("gpt-x", "openai", "conn-stale", true);
const freshId = trackPendingRequest("gpt-x", "openai", "conn-fresh", true);

assert.ok(staleId && freshId, "both started requests should produce ids");
assert.equal(getPendingById().size, 2);
assert.equal(getPendingRequests().byModel["gpt-x (openai)"], 2);

// Age the stale entry well beyond the max age.
const stale = getPendingById().get(staleId);
assert.ok(stale, "stale detail should exist");
stale.startedAt = Date.now() - 60 * 60 * 1000; // 1 hour ago

const removed = sweepStalePendingRequests(Date.now(), 15 * 60 * 1000);

assert.equal(removed, 1, "exactly one orphaned entry should be swept");
assert.equal(getPendingById().size, 1, "only the fresh entry should remain");
assert.ok(getPendingById().has(freshId), "fresh entry must survive");

// Counts must reflect the eviction (decremented, not left dangling).
assert.equal(getPendingRequests().byModel["gpt-x (openai)"], 1);
assert.equal(getPendingRequests().byAccount["conn-stale"], undefined);
assert.equal(getPendingRequests().byAccount["conn-fresh"]["gpt-x (openai)"], 1);

clearPendingRequests();
});

test("sweepStalePendingRequests is a no-op when nothing is stale", () => {
clearPendingRequests();
trackPendingRequest("m", "p", "c1", true);
trackPendingRequest("m", "p", "c2", true);

const removed = sweepStalePendingRequests(Date.now(), 15 * 60 * 1000);

assert.equal(removed, 0);
assert.equal(getPendingById().size, 2);
clearPendingRequests();
});
import test from "node:test";
import assert from "node:assert/strict";

const {
trackPendingRequest,
getPendingById,
getPendingRequests,
sweepStalePendingRequests,
getMaxPendingRequestAgeMs,
clearPendingRequests,
} = await import("../../src/lib/usage/usageHistory.ts");

const MINUTE_MS = 60 * 1000;
const HOUR_MS = 60 * MINUTE_MS;

test("sweepStalePendingRequests evicts orphaned pending details and self-heals counts", () => {
clearPendingRequests();

// One request that will be treated as orphaned (never finalized), one fresh.
const staleId = trackPendingRequest("gpt-x", "openai", "conn-stale", true);
const freshId = trackPendingRequest("gpt-x", "openai", "conn-fresh", true);

assert.ok(staleId && freshId, "both started requests should produce ids");
assert.equal(getPendingById().size, 2);
assert.equal(getPendingRequests().byModel["gpt-x (openai)"], 2);

// Age the stale entry well beyond the max age.
const stale = getPendingById().get(staleId);
assert.ok(stale, "stale detail should exist");
stale.startedAt = Date.now() - 2 * HOUR_MS;

const removed = sweepStalePendingRequests(Date.now(), HOUR_MS);

assert.equal(removed, 1, "exactly one orphaned entry should be swept");
assert.equal(getPendingById().size, 1, "only the fresh entry should remain");
assert.ok(getPendingById().has(freshId), "fresh entry must survive");

// Counts must reflect the eviction (decremented, not left dangling).
assert.equal(getPendingRequests().byModel["gpt-x (openai)"], 1);
assert.equal(getPendingRequests().byAccount["conn-stale"], undefined);
assert.equal(getPendingRequests().byAccount["conn-fresh"]["gpt-x (openai)"], 1);

clearPendingRequests();
});

test("sweepStalePendingRequests is a no-op when nothing is stale", () => {
clearPendingRequests();
trackPendingRequest("m", "p", "c1", true);
trackPendingRequest("m", "p", "c2", true);

const removed = sweepStalePendingRequests(Date.now(), HOUR_MS);

assert.equal(removed, 0);
assert.equal(getPendingById().size, 2);
clearPendingRequests();
});

test("sweepStalePendingRequests defaults to a one hour max pending age", () => {
clearPendingRequests();

const staleId = trackPendingRequest("m", "p", "old", true);
const recentId = trackPendingRequest("m", "p", "recent", true);
assert.ok(staleId && recentId);

const now = Date.now();
const stale = getPendingById().get(staleId);
const recent = getPendingById().get(recentId);
assert.ok(stale && recent);

stale.startedAt = now - 61 * MINUTE_MS;
recent.startedAt = now - 59 * MINUTE_MS;

const removed = sweepStalePendingRequests(now);

assert.equal(removed, 1);
assert.equal(getPendingById().has(staleId), false);
assert.equal(getPendingById().has(recentId), true);
clearPendingRequests();
});

test("pending sweep max age can be overridden through environment", () => {
clearPendingRequests();
const previous = process.env.MAX_PENDING_REQUEST_AGE_MS;
process.env.MAX_PENDING_REQUEST_AGE_MS = String(2 * HOUR_MS);

try {
const requestId = trackPendingRequest("m", "p", "custom-age", true);
assert.ok(requestId);

const detail = getPendingById().get(requestId);
assert.ok(detail);
detail.startedAt = Date.now() - 90 * MINUTE_MS;

assert.equal(getMaxPendingRequestAgeMs(), 2 * HOUR_MS);
assert.equal(sweepStalePendingRequests(Date.now()), 0);
assert.equal(getPendingById().has(requestId), true);
} finally {
if (previous === undefined) delete process.env.MAX_PENDING_REQUEST_AGE_MS;
else process.env.MAX_PENDING_REQUEST_AGE_MS = previous;
clearPendingRequests();
}
});

test("invalid pending sweep max age falls back to one hour", () => {
const previous = process.env.MAX_PENDING_REQUEST_AGE_MS;
process.env.MAX_PENDING_REQUEST_AGE_MS = "not-a-number";

try {
assert.equal(getMaxPendingRequestAgeMs(), HOUR_MS);
} finally {
if (previous === undefined) delete process.env.MAX_PENDING_REQUEST_AGE_MS;
else process.env.MAX_PENDING_REQUEST_AGE_MS = previous;
}
});
Loading