agent-dealer 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/server/dist/adapters/agent-health.test.js +52 -0
- package/bundle/server/dist/capacity/service.js +66 -6
- package/bundle/server/dist/capacity/service.test.js +220 -0
- package/bundle/server/dist/db/index.js +8 -0
- package/bundle/server/dist/db/schema.sql +5 -1
- package/bundle/server/dist/repository/runtime-availability.js +29 -11
- package/bundle/server/dist/runners/usage-cap.js +66 -15
- package/bundle/server/dist/runners/usage-cap.test.js +96 -2
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-B_pNUDRY.js +64 -0
- package/bundle/server/static-ui/index.html +1 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +85 -0
- package/bundle/shared/dist/runtime-capacity.js +19 -0
- package/bundle/shared/package.json +1 -1
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-DQ5ivcyg.js +0 -64
|
@@ -9,7 +9,7 @@ process.env.USAGE_CAP_FALLBACK_COOLDOWN_MS = "1800000";
|
|
|
9
9
|
const fixtureDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const claudeFixture = fs.readFileSync(path.join(fixtureDir, "fixtures/claude-rate-limit-rejected.ndjson"), "utf8");
|
|
11
11
|
const { migrate } = await import("../db/index.js");
|
|
12
|
-
const { recordUsageCapFromEvents, detectUsageCapFromNdjson, detectUsageCapFromRawLog, detectUsageCapFromLog, extractUsageCapFromEvents, recordMuseUsageCap, } = await import("./usage-cap.js");
|
|
12
|
+
const { recordUsageCapFromEvents, recordUsageCapFromLog, detectUsageCapFromNdjson, detectUsageCapFromRawLog, detectUsageCapFromLog, extractUsageCapFromEvents, extractUsageCapReset, recordMuseUsageCap, } = await import("./usage-cap.js");
|
|
13
13
|
const { parseNdjson } = await import("./stream-json.js");
|
|
14
14
|
const { runtimeAvailability, clearAllRuntimeAvailability, } = await import("../repository/runtime-availability.js");
|
|
15
15
|
before(() => migrate());
|
|
@@ -104,7 +104,101 @@ test("NOT-117: successful Codex turn.completed skips text fallback even when std
|
|
|
104
104
|
].join("\n");
|
|
105
105
|
assert.ok(detectUsageCapFromRawLog(failed, "codex_local"));
|
|
106
106
|
});
|
|
107
|
-
//
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// NOT-439: every hard-cap path preserves the provider's future reset instead
|
|
109
|
+
// of the 30-minute fallback. Incident fixture: `2026-10-12T18:59:01Z` must
|
|
110
|
+
// persist exactly as `2026-10-12T18:59:01.000Z` on every path.
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
const NOT439_NOW = Date.parse("2026-10-12T18:16:20.154Z");
|
|
113
|
+
const NOT439_RESET = "2026-10-12T18:59:01.000Z";
|
|
114
|
+
test("NOT-439: Claude/Cursor result error text with a provider reset persists the exact boundary", () => {
|
|
115
|
+
const raw = '{"type":"result","is_error":true,"result":"Rate limit exceeded for this account. Your usage window resets at 2026-10-12T18:59:01Z."}';
|
|
116
|
+
const cap = recordUsageCapFromEvents(parseNdjson(raw), "claude_code", NOT439_NOW);
|
|
117
|
+
assert.ok(cap);
|
|
118
|
+
assert.equal(cap.unavailableUntil, NOT439_RESET);
|
|
119
|
+
const avail = runtimeAvailability("claude_code", NOT439_NOW);
|
|
120
|
+
assert.equal(avail.available, false);
|
|
121
|
+
if (!avail.available)
|
|
122
|
+
assert.equal(avail.until, NOT439_RESET);
|
|
123
|
+
});
|
|
124
|
+
test("NOT-439: Codex turn.failed and error.message with a provider reset persist the exact boundary", () => {
|
|
125
|
+
const failed = [
|
|
126
|
+
'{"type":"thread.started","thread_id":"t1"}',
|
|
127
|
+
'{"type":"turn.failed","error":{"message":"Rate limit exceeded. Your usage window resets at 2026-10-12T18:59:01Z."}}',
|
|
128
|
+
].join("\n");
|
|
129
|
+
const fromFailed = detectUsageCapFromRawLog(failed, "codex_local", NOT439_NOW);
|
|
130
|
+
assert.ok(fromFailed);
|
|
131
|
+
assert.equal(fromFailed.unavailableUntil, NOT439_RESET);
|
|
132
|
+
const errEvent = [
|
|
133
|
+
'{"type":"thread.started","thread_id":"t1"}',
|
|
134
|
+
'{"type":"error","message":"Usage limit reached. Your usage window resets at 2026-10-12T18:59:01Z."}',
|
|
135
|
+
].join("\n");
|
|
136
|
+
const fromError = detectUsageCapFromRawLog(errEvent, "codex_local", NOT439_NOW);
|
|
137
|
+
assert.ok(fromError);
|
|
138
|
+
assert.equal(fromError.unavailableUntil, NOT439_RESET);
|
|
139
|
+
});
|
|
140
|
+
test("NOT-439: guarded Cursor stderr fallback with a provider reset persists the exact boundary", () => {
|
|
141
|
+
const raw = '{"type":"result","is_error":true,"result":"failed"}\n--- stderr ---\nError: usage limit reached for this account; resets at 2026-10-12T18:59:01Z\n';
|
|
142
|
+
const detected = detectUsageCapFromRawLog(raw, "cursor_local", NOT439_NOW);
|
|
143
|
+
assert.ok(detected);
|
|
144
|
+
assert.equal(detected.unavailableUntil, NOT439_RESET);
|
|
145
|
+
const logPath = path.join(process.env.AGENT_DEALER_HOME, "cursor-cap-reset.ndjson");
|
|
146
|
+
fs.writeFileSync(logPath, raw);
|
|
147
|
+
const cap = recordUsageCapFromLog(logPath, "cursor_local", NOT439_NOW);
|
|
148
|
+
assert.ok(cap);
|
|
149
|
+
assert.equal(cap.unavailableUntil, NOT439_RESET);
|
|
150
|
+
const avail = runtimeAvailability("cursor_local", NOT439_NOW);
|
|
151
|
+
assert.equal(avail.available, false);
|
|
152
|
+
if (!avail.available)
|
|
153
|
+
assert.equal(avail.until, NOT439_RESET);
|
|
154
|
+
});
|
|
155
|
+
test("NOT-439: Muse usage-cap failure with the incident reset persists the exact boundary", () => {
|
|
156
|
+
const cap = recordMuseUsageCap({
|
|
157
|
+
kind: "usage_cap",
|
|
158
|
+
message: "Subscription quota exhausted. Your usage window resets at 2026-10-12T18:59:01Z",
|
|
159
|
+
}, NOT439_NOW);
|
|
160
|
+
assert.ok(cap);
|
|
161
|
+
assert.equal(cap.unavailableUntil, NOT439_RESET);
|
|
162
|
+
const avail = runtimeAvailability("muse_code", NOT439_NOW);
|
|
163
|
+
assert.equal(avail.available, false);
|
|
164
|
+
if (!avail.available)
|
|
165
|
+
assert.equal(avail.until, NOT439_RESET);
|
|
166
|
+
});
|
|
167
|
+
test("NOT-439: structured numeric resets accept seconds and milliseconds", () => {
|
|
168
|
+
const nowMs = Date.parse("2026-01-01T00:00:00.000Z");
|
|
169
|
+
const expected = new Date(1784283600 * 1000).toISOString();
|
|
170
|
+
assert.equal(extractUsageCapReset({ resets: [1784283600] }, nowMs), expected);
|
|
171
|
+
assert.equal(extractUsageCapReset({ resets: [1784283600 * 1000] }, nowMs), expected);
|
|
172
|
+
// Structured wins over text when both are present.
|
|
173
|
+
assert.equal(extractUsageCapReset({ resets: [1784283600], text: "resets at 2026-10-12T18:59:01Z" }, nowMs), expected);
|
|
174
|
+
});
|
|
175
|
+
test("NOT-439: past, malformed, or unrelated timestamps fall back and never create a cap", () => {
|
|
176
|
+
const nowMs = Date.parse("2026-10-12T18:16:20.154Z");
|
|
177
|
+
const fallback = new Date(nowMs + 1_800_000).toISOString();
|
|
178
|
+
// Past structured reset and past text timestamp → fallback.
|
|
179
|
+
assert.equal(extractUsageCapReset({ resets: [Date.parse("2020-01-01T00:00:00.000Z") / 1000] }, nowMs), null);
|
|
180
|
+
assert.equal(extractUsageCapReset({ text: "Rate limit exceeded, reset at 2020-01-01T00:00:00Z" }, nowMs), null);
|
|
181
|
+
// Malformed / non-ISO timestamps are ignored.
|
|
182
|
+
assert.equal(extractUsageCapReset({ text: "Rate limit exceeded, reset at 2026-13-99T99:99:99Z" }, nowMs), null);
|
|
183
|
+
assert.equal(extractUsageCapReset({ text: "Rate limit exceeded, reset soon (tomorrow at noon)" }, nowMs), null);
|
|
184
|
+
assert.equal(extractUsageCapReset({ resets: ["2026-10-12T18:59:01Z", NaN, Infinity] }, nowMs), null);
|
|
185
|
+
// A bare future timestamp with no cap signal is not a cap by itself.
|
|
186
|
+
const timestampOnly = '{"type":"result","is_error":true,"result":"Completed at 2026-10-12T18:59:01Z without errors."}';
|
|
187
|
+
assert.equal(detectUsageCapFromNdjson(timestampOnly, "claude_code", nowMs), null);
|
|
188
|
+
assert.equal(runtimeAvailability("claude_code", nowMs).available, true);
|
|
189
|
+
// An unrelated future stamp inside accepted cap text is not a reset.
|
|
190
|
+
const unrelatedStamp = '{"type":"result","is_error":true,"result":"[2026-10-12T19:30:00Z] Rate limit exceeded for this account."}';
|
|
191
|
+
const unrelatedCap = detectUsageCapFromNdjson(unrelatedStamp, "claude_code", nowMs);
|
|
192
|
+
assert.ok(unrelatedCap);
|
|
193
|
+
assert.equal(unrelatedCap.unavailableUntil, fallback);
|
|
194
|
+
// A cap signal whose reset is in the past still detects, but uses the fallback.
|
|
195
|
+
const pastReset = '{"type":"result","is_error":true,"result":"Rate limit exceeded. Your usage window resets at 2020-01-01T00:00:00Z."}';
|
|
196
|
+
const cap = detectUsageCapFromNdjson(pastReset, "claude_code", nowMs);
|
|
197
|
+
assert.ok(cap);
|
|
198
|
+
assert.equal(cap.unavailableUntil, fallback);
|
|
199
|
+
});
|
|
200
|
+
// NOT-181: Muse's cap comes from the parsed failure kind; NOT-439: a provider reset in the
|
|
201
|
+
// message is preserved, otherwise the fallback cooldown applies.
|
|
108
202
|
test("recordMuseUsageCap records muse_code availability only for a usage_cap failure", () => {
|
|
109
203
|
clearAllRuntimeAvailability();
|
|
110
204
|
const now = Date.parse("2026-09-20T00:00:00.000Z");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "AGENT_DEALER_SKIP_GITHUB_HEALTH=1 AGENT_DEALER_SKIP_AGENT_HEALTH=1 npm run build -w @agent-dealer/shared && AGENT_DEALER_SKIP_GITHUB_HEALTH=1 AGENT_DEALER_SKIP_AGENT_HEALTH=1 node ../../scripts/run-workspace-tests.mjs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@agent-dealer/shared": "1.3.
|
|
26
|
+
"@agent-dealer/shared": "1.3.3",
|
|
27
27
|
"@fastify/cors": "^11.0.1",
|
|
28
28
|
"@fastify/static": "^8.2.0",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.29.0",
|