agent-dealer 1.1.0 → 1.1.1
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/runners/usage-cap.js +4 -5
- package/bundle/server/dist/runners/usage-cap.test.js +9 -0
- package/bundle/server/package.json +2 -2
- package/bundle/shared/package.json +1 -1
- package/dist/start.js +28 -10
- package/dist/stop.js +14 -6
- package/dist/stop.test.d.ts +1 -0
- package/dist/stop.test.js +131 -0
- package/package.json +1 -1
|
@@ -29,12 +29,11 @@ function fallbackUntil(nowMs = Date.now()) {
|
|
|
29
29
|
function capReason(runtime, detail) {
|
|
30
30
|
return `${runtime} usage capped — ${detail}`;
|
|
31
31
|
}
|
|
32
|
+
// `status` is the verdict on the plan window. `overageStatus` only says whether pay-as-you-go
|
|
33
|
+
// overage is available: orgs with overage disabled report "rejected" on every event of a healthy
|
|
34
|
+
// session (status "allowed", isUsingOverage false), so it must not open a deferral by itself.
|
|
32
35
|
function isHardCapRateLimitInfo(info) {
|
|
33
|
-
|
|
34
|
-
return true;
|
|
35
|
-
if (info.overageStatus === "rejected")
|
|
36
|
-
return true;
|
|
37
|
-
return false;
|
|
36
|
+
return info.status === "rejected";
|
|
38
37
|
}
|
|
39
38
|
function signalFromRateLimitInfo(runtime, info, evidence, nowMs) {
|
|
40
39
|
if (!isHardCapRateLimitInfo(info))
|
|
@@ -26,6 +26,15 @@ test("Claude rate_limit_event rejected writes runtime_availability with resetsAt
|
|
|
26
26
|
assert.equal(avail.until, cap.unavailableUntil);
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
|
+
test("allowed rate_limit_event with overage rejected (org disabled overage) is not a cap", () => {
|
|
30
|
+
const raw = [
|
|
31
|
+
'{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1784283600,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled_until","isUsingOverage":false,"unifiedWindows":{"five_hour":{"utilization":0.17,"resetsAt":1784283600}}}}',
|
|
32
|
+
'{"type":"result","is_error":false,"result":"done"}',
|
|
33
|
+
].join("\n");
|
|
34
|
+
assert.equal(detectUsageCapFromNdjson(raw, "claude_code"), null);
|
|
35
|
+
assert.equal(recordUsageCapFromEvents(parseNdjson(raw), "claude_code"), null);
|
|
36
|
+
assert.equal(runtimeAvailability("claude_code").available, true);
|
|
37
|
+
});
|
|
29
38
|
test("allowed_warning rate_limit_event is ignored (hard cap only)", () => {
|
|
30
39
|
const raw = [
|
|
31
40
|
'{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1784283600,"rateLimitType":"five_hour"}}',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-dealer/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agent-dealer/shared": "1.1.
|
|
25
|
+
"@agent-dealer/shared": "1.1.1",
|
|
26
26
|
"@fastify/cors": "^11.0.1",
|
|
27
27
|
"@fastify/static": "^8.2.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
package/dist/start.js
CHANGED
|
@@ -124,6 +124,24 @@ async function ensurePortAvailable(host, port, probe) {
|
|
|
124
124
|
}
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* `--force` restart: stop the existing instance, then require the port to actually be free.
|
|
129
|
+
* `runStop()` can return 0 while a listener survives (an explicit AGENT_DEALER_HOME without run.json
|
|
130
|
+
* refuses to sweep the port), and `ensurePortAvailable` treats a healthy agent-dealer as available —
|
|
131
|
+
* so without this check the replacement server would launch onto an occupied port.
|
|
132
|
+
*/
|
|
133
|
+
async function stopForForcedRestart(host, port) {
|
|
134
|
+
console.log("[agent-dealer] Restarting existing instance (--force) ...");
|
|
135
|
+
await runStop();
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
137
|
+
if ((await probeAgentDealer(host, port)).up) {
|
|
138
|
+
console.error(`[agent-dealer] --force could not stop the agent-dealer still listening on ${host}:${port}.`);
|
|
139
|
+
console.error("[agent-dealer] It is not owned by this AGENT_DEALER_HOME, so it was left running.");
|
|
140
|
+
console.error(`[agent-dealer] Stop it from its own home, or kill it manually: lsof -ti :${port} -sTCP:LISTEN | xargs kill`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
127
145
|
export async function runStart(options = {}) {
|
|
128
146
|
const envFile = loadProdEnvFile();
|
|
129
147
|
const home = prodHomeDir();
|
|
@@ -142,9 +160,10 @@ export async function runStart(options = {}) {
|
|
|
142
160
|
const probe = await probeAgentDealer(host, port);
|
|
143
161
|
if (probe.up) {
|
|
144
162
|
if (options.force) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
163
|
+
const stopError = await stopForForcedRestart(host, port);
|
|
164
|
+
if (stopError !== null) {
|
|
165
|
+
return stopError;
|
|
166
|
+
}
|
|
148
167
|
}
|
|
149
168
|
else {
|
|
150
169
|
printRunningEndpoints(base, uiDist);
|
|
@@ -152,8 +171,7 @@ export async function runStart(options = {}) {
|
|
|
152
171
|
return 0;
|
|
153
172
|
}
|
|
154
173
|
}
|
|
155
|
-
const
|
|
156
|
-
const portError = await ensurePortAvailable(host, port, refreshedProbe);
|
|
174
|
+
const portError = await ensurePortAvailable(host, port, options.force ? await probeAgentDealer(host, port) : probe);
|
|
157
175
|
if (portError !== null) {
|
|
158
176
|
return portError;
|
|
159
177
|
}
|
|
@@ -163,9 +181,10 @@ export async function runStart(options = {}) {
|
|
|
163
181
|
const probe = await probeAgentDealer(host, port);
|
|
164
182
|
if (probe.up) {
|
|
165
183
|
if (options.force) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
const stopError = await stopForForcedRestart(host, port);
|
|
185
|
+
if (stopError !== null) {
|
|
186
|
+
return stopError;
|
|
187
|
+
}
|
|
169
188
|
}
|
|
170
189
|
else {
|
|
171
190
|
printRunningEndpoints(base, uiDist);
|
|
@@ -173,8 +192,7 @@ export async function runStart(options = {}) {
|
|
|
173
192
|
return 0;
|
|
174
193
|
}
|
|
175
194
|
}
|
|
176
|
-
const
|
|
177
|
-
const portError = await ensurePortAvailable(host, port, refreshedProbe);
|
|
195
|
+
const portError = await ensurePortAvailable(host, port, options.force ? await probeAgentDealer(host, port) : probe);
|
|
178
196
|
if (portError !== null) {
|
|
179
197
|
return portError;
|
|
180
198
|
}
|
package/dist/stop.js
CHANGED
|
@@ -39,17 +39,25 @@ export async function runStop() {
|
|
|
39
39
|
}
|
|
40
40
|
clearRunState();
|
|
41
41
|
}
|
|
42
|
+
// An explicit AGENT_DEALER_HOME means "only this home" (smoke tests, side-by-side installs).
|
|
43
|
+
// Its run.json is the sole proof of ownership, so never sweep the port: with no run state the
|
|
44
|
+
// port falls back to the bundled 2222, which belongs to the default install.
|
|
45
|
+
const isolatedHome = Boolean(process.env.AGENT_DEALER_HOME?.trim());
|
|
42
46
|
let probe = await probeAgentDealer(host, port);
|
|
43
47
|
if (probe.up) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
if (!isolatedHome) {
|
|
49
|
+
for (const pid of listListeningPids(port)) {
|
|
50
|
+
if (terminatePid(pid, `listener on :${port}`)) {
|
|
51
|
+
stopped += 1;
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
if (!isolatedHome || state) {
|
|
56
|
+
await waitForShutdown(host, port);
|
|
57
|
+
probe = await probeAgentDealer(host, port);
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
|
-
if (probe.up) {
|
|
60
|
+
if (probe.up && (!isolatedHome || state)) {
|
|
53
61
|
console.warn("[agent-dealer] agent-dealer still responds on configured port. Kill remaining processes manually:");
|
|
54
62
|
console.warn(` lsof -ti :${port} -sTCP:LISTEN | xargs kill`);
|
|
55
63
|
return 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { runStart } from "./start.js";
|
|
8
|
+
import { runStop } from "./stop.js";
|
|
9
|
+
const HEALTH_SERVER = `
|
|
10
|
+
const http = require("node:http");
|
|
11
|
+
const server = http.createServer((req, res) => {
|
|
12
|
+
res.setHeader("content-type", "application/json");
|
|
13
|
+
res.end(JSON.stringify({ ok: true }));
|
|
14
|
+
});
|
|
15
|
+
server.listen(0, "127.0.0.1", () => console.log(server.address().port));
|
|
16
|
+
`;
|
|
17
|
+
/** A separate process answering /health, standing in for an agent-dealer server. */
|
|
18
|
+
async function startFakeServer() {
|
|
19
|
+
const child = spawn(process.execPath, ["-e", HEALTH_SERVER], { stdio: ["ignore", "pipe", "inherit"] });
|
|
20
|
+
const port = await new Promise((resolve, reject) => {
|
|
21
|
+
child.once("error", reject);
|
|
22
|
+
child.stdout.once("data", (chunk) => resolve(Number(String(chunk).trim())));
|
|
23
|
+
});
|
|
24
|
+
return { child, port };
|
|
25
|
+
}
|
|
26
|
+
function isAlive(pid) {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function waitUntilDead(pid) {
|
|
36
|
+
for (let i = 0; i < 25; i += 1) {
|
|
37
|
+
if (!isAlive(pid))
|
|
38
|
+
return true;
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
async function withEnv(env, fn) {
|
|
44
|
+
const saved = Object.fromEntries(Object.keys(env).map((key) => [key, process.env[key]]));
|
|
45
|
+
for (const [key, value] of Object.entries(env)) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
delete process.env[key];
|
|
48
|
+
else
|
|
49
|
+
process.env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await fn();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
delete process.env[key];
|
|
58
|
+
else
|
|
59
|
+
process.env[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function tempHome() {
|
|
64
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "agent-dealer-stop-"));
|
|
65
|
+
}
|
|
66
|
+
test("stop with an explicit AGENT_DEALER_HOME and no run state never kills a listener on the fallback port", async () => {
|
|
67
|
+
const { child, port } = await startFakeServer();
|
|
68
|
+
const home = tempHome();
|
|
69
|
+
try {
|
|
70
|
+
await withEnv({ AGENT_DEALER_HOME: home, PORT: String(port), WEB_PORT: undefined }, async () => {
|
|
71
|
+
assert.equal(await runStop(), 0);
|
|
72
|
+
});
|
|
73
|
+
assert.equal(isAlive(child.pid), true, "the unrelated listener must survive an isolated-home stop");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
child.kill("SIGKILL");
|
|
77
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
test("stop with an explicit AGENT_DEALER_HOME still terminates the pids recorded in its run.json", async () => {
|
|
81
|
+
const { child, port } = await startFakeServer();
|
|
82
|
+
const home = tempHome();
|
|
83
|
+
fs.writeFileSync(path.join(home, "run.json"), JSON.stringify({ host: "127.0.0.1", port, serverPid: child.pid, cliPid: 2 ** 22, startedAt: new Date().toISOString() }));
|
|
84
|
+
try {
|
|
85
|
+
await withEnv({ AGENT_DEALER_HOME: home, PORT: undefined, WEB_PORT: undefined }, async () => {
|
|
86
|
+
assert.equal(await runStop(), 0);
|
|
87
|
+
});
|
|
88
|
+
assert.equal(await waitUntilDead(child.pid), true);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
child.kill("SIGKILL");
|
|
92
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
test("stop on the default home keeps sweeping the port when run.json is lost", async () => {
|
|
96
|
+
const { child, port } = await startFakeServer();
|
|
97
|
+
const home = tempHome();
|
|
98
|
+
try {
|
|
99
|
+
await withEnv({ AGENT_DEALER_HOME: undefined, HOME: home, USERPROFILE: home, PORT: String(port), WEB_PORT: undefined }, async () => {
|
|
100
|
+
assert.equal(await runStop(), 0);
|
|
101
|
+
});
|
|
102
|
+
assert.equal(await waitUntilDead(child.pid), true);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
child.kill("SIGKILL");
|
|
106
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
async function assertForcedStartRefusesUnownedListener(options) {
|
|
110
|
+
const { child, port } = await startFakeServer();
|
|
111
|
+
const home = tempHome();
|
|
112
|
+
try {
|
|
113
|
+
await withEnv({ AGENT_DEALER_HOME: home, PORT: String(port), WEB_PORT: undefined }, async () => {
|
|
114
|
+
// The isolated-home stop leaves the listener up (no run.json = no proof of ownership).
|
|
115
|
+
// Forced start must fail instead of launching a second server onto the occupied port.
|
|
116
|
+
assert.equal(await runStart({ force: true, daemon: options.daemon, port }), 1);
|
|
117
|
+
});
|
|
118
|
+
assert.equal(isAlive(child.pid), true, "the unrelated listener must survive a forced start");
|
|
119
|
+
assert.equal(fs.existsSync(path.join(home, "run.json")), false, "no run state may be written for a server that never launched");
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
child.kill("SIGKILL");
|
|
123
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
test("start --force in the foreground fails when the isolated-home stop leaves a listener up", async () => {
|
|
127
|
+
await assertForcedStartRefusesUnownedListener({ daemon: false });
|
|
128
|
+
});
|
|
129
|
+
test("start --daemon --force fails when the isolated-home stop leaves a listener up", async () => {
|
|
130
|
+
await assertForcedStartRefusesUnownedListener({ daemon: true });
|
|
131
|
+
});
|