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.
@@ -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
- if (info.status === "rejected")
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.0",
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.0",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/shared",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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
- console.log("[agent-dealer] Restarting existing instance (--force) ...");
146
- await runStop();
147
- await new Promise((resolve) => setTimeout(resolve, 500));
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 refreshedProbe = options.force ? await probeAgentDealer(host, port) : probe;
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
- console.log("[agent-dealer] Restarting existing instance (--force) ...");
167
- await runStop();
168
- await new Promise((resolve) => setTimeout(resolve, 500));
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 refreshedProbe = options.force ? await probeAgentDealer(host, port) : probe;
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
- for (const pid of listListeningPids(port)) {
45
- if (terminatePid(pid, `listener on :${port}`)) {
46
- stopped += 1;
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
- await waitForShutdown(host, port);
50
- probe = await probeAgentDealer(host, port);
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dealer",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Human control plane for agent execution — queue, plan approval, audit",
5
5
  "license": "MIT",
6
6
  "type": "module",