Skip to content

Commit 6447827

Browse files
committed
feat: pmxt.server namespace + fix ServerManager parallel-init race
Adds a single namespaced entry point for sidecar lifecycle in both the TypeScript and Python SDKs: pmxt.server.status() structured snapshot (running, pid, port, version, uptime) pmxt.server.health() fast /health probe pmxt.server.start() idempotent start pmxt.server.stop() stop and clean up the lock file pmxt.server.restart() stop + start pmxt.server.logs(n) tail ~/.pmxt/server.log pmxt-ensure-server now redirects spawned sidecar stdio to ~/.pmxt/server.log so logs() has something to read. Previously stdio was dropped and boot crashes left no trace. Also fixes a long-standing race in ServerManager.ensureServerRunning(). Creating multiple Exchange instances in parallel caused every request to return 401 Unauthorized: each Exchange constructed its own ServerManager, each one called ensureServerRunning() concurrently, every call saw 'no server running', every call spawned its own sidecar via pmxt-ensure-server, and the lock file ended up pointing at whichever spawn wrote last. Each Exchange had already captured its basePath at construction time, so most requests hit a sidecar whose access token did NOT match the token later read from the lock file. Fix is process-wide coalescing: - TypeScript: static Promise<void> | null cache on ServerManager. Concurrent callers await the same in-flight promise; the cache is cleared on settle so later calls can re-check sidecar state. - Python: class-level threading.Lock wrapping the entire check-and-spawn critical section. The is-alive check is re-evaluated inside the lock so threads that lose the race observe the sidecar that the winning thread just started. Fully backwards compatible: pmxt.stopServer() / pmxt.restartServer() (and stop_server / restart_server in Python) remain first-class aliases. No deprecation, no warnings.
1 parent e0467ae commit 6447827

13 files changed

Lines changed: 959 additions & 59 deletions

changelog.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.26.0] - 2026-04-08
6+
7+
### Fixed
8+
9+
- **`ServerManager.ensureServerRunning()` race condition (TypeScript and Python SDKs)**: Creating multiple `Exchange` instances in parallel (e.g. `const p = new Polymarket(); const k = new Kalshi(); const l = new Limitless();`) caused every request to return `401 Unauthorized`. Each `Exchange` constructed its own `ServerManager` and each one called `ensureServerRunning()` concurrently. Every call saw "no server running", every call spawned its own sidecar via `pmxt-ensure-server`, and the lock file ended up pointing at whichever spawn wrote last — but each `Exchange` had already captured its own `basePath` at construction time, so most requests hit a sidecar whose access token did NOT match the token later read from the lock file.
10+
11+
Fix is process-wide coalescing inside `ServerManager`:
12+
- **TypeScript**: `ensureServerRunning()` now uses a static `Promise | null` cache. Concurrent callers await the same in-flight promise; the cache is cleared on settle so later calls can re-check the sidecar state.
13+
- **Python**: `ensure_server_running()` now holds a class-level `threading.Lock` for the entire check-and-spawn critical section. The "is the server already running?" check is re-evaluated inside the lock so threads that lose the race observe the sidecar that the winning thread just started.
14+
15+
### Added
16+
17+
- **`pmxt.server` namespace for sidecar lifecycle management** (TypeScript and Python SDKs): A single, discoverable namespace for managing the background sidecar. All six commands are available identically in both SDKs:
18+
- `pmxt.server.status()` — Structured snapshot: `{ running, pid, port, version, uptimeSeconds, lockFile }`. Returns a fresh object on every call (no shared mutable state).
19+
- `pmxt.server.health()` — Returns `true` if the sidecar responds to `/health`, `false` otherwise. Fast, no side effects.
20+
- `pmxt.server.start()` — Idempotently starts the sidecar. No-op if one is already running.
21+
- `pmxt.server.stop()` — Stops the sidecar and removes the lock file.
22+
- `pmxt.server.restart()` — Stop + start.
23+
- `pmxt.server.logs(n = 50)` — Returns the last `n` lines from `~/.pmxt/server.log`, or an empty list if the launcher never wrote a log file.
24+
25+
Motivation: sidecar lifecycle is a real surface users hit regularly — stale lock files, zombie sidecars from crashed parents, version mismatches, and race conditions when multiple `Exchange` instances boot in parallel. Previously users had to reach into `ServerManager` directly or shell out to `ps` / `lsof` to diagnose. `pmxt.server.*` makes the lifecycle observable and controllable from a single entry point. Example:
26+
27+
```typescript
28+
import pmxt from 'pmxtjs';
29+
const s = await pmxt.server.status();
30+
if (!s.running) await pmxt.server.start();
31+
console.log(await pmxt.server.logs(20));
32+
```
33+
34+
- **Sidecar writes stdout/stderr to `~/.pmxt/server.log`**: `pmxt-ensure-server` now redirects the spawned sidecar's stdio to a log file in the `~/.pmxt/` directory so `pmxt.server.logs()` has something to read. Previously stdio was dropped (`stdio: 'ignore'`) and any crash during boot left no trace.
35+
36+
Fully backwards compatible: the existing flat helpers `pmxt.stopServer()` / `pmxt.restartServer()` (TypeScript) and `pmxt.stop_server()` / `pmxt.restart_server()` (Python) remain first-class, fully-supported aliases for `pmxt.server.stop()` / `pmxt.server.restart()`. No deprecation, no warnings — both spellings work and will keep working.
37+
538
## [2.25.3] - 2026-04-08
639

740
### Added

core/bin/pmxt-ensure-server

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,23 @@ async function startServer() {
113113
serverCmd = localBinServer;
114114
}
115115

116+
// Open a log file for the sidecar so SDK consumers can call
117+
// `pmxt.server.logs()` and see real output. Falls back to 'ignore' if the
118+
// file cannot be opened (e.g. read-only home directory).
119+
const logFile = path.join(os.homedir(), '.pmxt', 'server.log');
120+
let stdio = 'ignore';
121+
try {
122+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
123+
const fd = fs.openSync(logFile, 'a');
124+
stdio = ['ignore', fd, fd];
125+
} catch (err) {
126+
// Keep stdio: 'ignore' on failure - logging is best-effort.
127+
}
128+
116129
// Spawn server as detached process
117130
const serverProcess = spawn(serverCmd, args, {
118131
detached: true,
119-
stdio: 'ignore',
132+
stdio,
120133
env: process.env
121134
});
122135

core/bin/pmxt-ensure-server.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,23 @@ async function startServer() {
113113
serverCmd = localBinServer;
114114
}
115115

116+
// Open a log file for the sidecar so SDK consumers can call
117+
// `pmxt.server.logs()` and see real output. Falls back to 'ignore' if the
118+
// file cannot be opened (e.g. read-only home directory).
119+
const logFile = path.join(os.homedir(), '.pmxt', 'server.log');
120+
let stdio = 'ignore';
121+
try {
122+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
123+
const fd = fs.openSync(logFile, 'a');
124+
stdio = ['ignore', fd, fd];
125+
} catch (err) {
126+
// Keep stdio: 'ignore' on failure - logging is best-effort.
127+
}
128+
116129
// Spawn server as detached process
117130
const serverProcess = spawn(serverCmd, args, {
118131
detached: true,
119-
stdio: 'ignore',
132+
stdio,
120133
env: process.env
121134
});
122135

scripts/templates/api-reference.python.md.hbs

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,74 @@ print(markets[0].title)
2929
3030
## Server Management
3131
32-
The SDK provides global functions to manage the background sidecar server. This is useful for clearing state, resolving "port busy" errors, or ensuring a clean slate in interactive environments like Jupyter.
32+
The SDK exposes a `pmxt.server` namespace for managing the background sidecar server. Use these commands for clearing state, resolving "port busy" errors, inspecting server health, or tailing logs from interactive environments like Jupyter.
3333
34-
### `stop_server`
34+
```python
35+
import pmxt
36+
37+
pmxt.server.status() # snapshot dict: running, pid, port, version, uptime_seconds, lock_file
38+
pmxt.server.health() # bool - True if /health responds ok
39+
pmxt.server.start() # idempotent - no-op if already running
40+
pmxt.server.stop() # stop the sidecar and clean up the lock file
41+
pmxt.server.restart() # stop and start the sidecar
42+
pmxt.server.logs(50) # last N log lines from ~/.pmxt/server.log (default 50)
43+
```
44+
45+
### `pmxt.server.status`
46+
47+
Returns a fresh dict describing the sidecar state. Useful for diagnosing "is it running?" before issuing API calls.
48+
49+
```python
50+
import pmxt
51+
info = pmxt.server.status()
52+
print(info["running"], info["pid"], info["port"], info["uptime_seconds"])
53+
```
54+
55+
### `pmxt.server.health`
56+
57+
Returns `True` if the sidecar's `/health` endpoint responds with status `ok`, otherwise `False`. Lighter than `status()` when you only need a boolean liveness check.
58+
59+
```python
60+
import pmxt
61+
if not pmxt.server.health():
62+
pmxt.server.restart()
63+
```
64+
65+
### `pmxt.server.start`
66+
67+
Idempotently start the sidecar. Returns immediately if a healthy server is already running. Use this when you want to fail fast on startup rather than letting the first API call lazily boot the server.
68+
69+
```python
70+
import pmxt
71+
pmxt.server.start()
72+
```
73+
74+
### `pmxt.server.stop`
75+
76+
Stop the running sidecar and clean up its lock file.
77+
78+
```python
79+
import pmxt
80+
pmxt.server.stop()
81+
```
82+
83+
### `pmxt.server.restart`
3584
36-
Stop the background PMXT sidecar server and clean up lock files.
85+
Stop the sidecar (if running) and start a fresh one. Equivalent to `stop()` followed by `start()`.
3786
3887
```python
3988
import pmxt
40-
pmxt.stop_server()
89+
pmxt.server.restart()
4190
```
4291
43-
### `restart_server`
92+
### `pmxt.server.logs`
4493
45-
Restart the background PMXT sidecar server. Equivalent to calling `stop_server()` followed by a fresh start.
94+
Return the last `n` lines (default `50`) from the sidecar log file at `~/.pmxt/server.log`. Returns an empty list if no log file exists yet. Invaluable when the server is misbehaving and you need to see what it actually printed.
4695
4796
```python
4897
import pmxt
49-
pmxt.restart_server()
98+
for line in pmxt.server.logs(100):
99+
print(line)
50100
```
51101
52102
---

scripts/templates/api-reference.typescript.md.hbs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,76 @@ console.log(markets[0].title);
3030
3131
## Server Management
3232
33-
The SDK provides global functions to manage the background sidecar server. This is useful for clearing state or
34-
resolving "port busy" errors.
33+
The SDK exposes a `pmxt.server` namespace for managing the background sidecar server. Use these commands for clearing state, resolving "port busy" errors, inspecting health, or tailing logs.
3534
36-
### `stopServer`
35+
```typescript
36+
import pmxt from 'pmxtjs';
37+
38+
await pmxt.server.status(); // snapshot: { running, pid, port, version, uptimeSeconds, lockFile }
39+
await pmxt.server.health(); // boolean - true if /health responds ok
40+
await pmxt.server.start(); // idempotent - no-op if already running
41+
await pmxt.server.stop(); // stop the sidecar and clean up the lock file
42+
await pmxt.server.restart(); // stop and start the sidecar
43+
pmxt.server.logs(50); // last N log lines from ~/.pmxt/server.log (default 50)
44+
```
45+
46+
### `pmxt.server.status`
47+
48+
Returns a fresh object describing the sidecar state. Useful for diagnosing "is it running?" before issuing API calls.
49+
50+
```typescript
51+
import pmxt from 'pmxtjs';
52+
const info = await pmxt.server.status();
53+
console.log(info.running, info.pid, info.port, info.uptimeSeconds);
54+
```
3755
38-
Stop the background PMXT sidecar server and clean up lock files.
56+
### `pmxt.server.health`
57+
58+
Returns `true` if the sidecar's `/health` endpoint responds with status `ok`, otherwise `false`. Lighter than `status()` when you only need a boolean liveness check.
3959
4060
```typescript
4161
import pmxt from 'pmxtjs';
42-
await pmxt.stopServer();
62+
if (!(await pmxt.server.health())) {
63+
await pmxt.server.restart();
64+
}
4365
```
4466
45-
### `restartServer`
67+
### `pmxt.server.start`
4668
47-
Restart the background PMXT sidecar server. Equivalent to calling `stopServer()` followed by a fresh start.
69+
Idempotently start the sidecar. Returns immediately if a healthy server is already running. Use this when you want to fail fast on startup rather than letting the first API call lazily boot the server.
4870
4971
```typescript
5072
import pmxt from 'pmxtjs';
51-
await pmxt.restartServer();
73+
await pmxt.server.start();
74+
```
75+
76+
### `pmxt.server.stop`
77+
78+
Stop the running sidecar and clean up its lock file.
79+
80+
```typescript
81+
import pmxt from 'pmxtjs';
82+
await pmxt.server.stop();
83+
```
84+
85+
### `pmxt.server.restart`
86+
87+
Stop the sidecar (if running) and start a fresh one. Equivalent to `stop()` followed by `start()`.
88+
89+
```typescript
90+
import pmxt from 'pmxtjs';
91+
await pmxt.server.restart();
92+
```
93+
94+
### `pmxt.server.logs`
95+
96+
Return the last `n` lines (default `50`) from the sidecar log file at `~/.pmxt/server.log`. Returns an empty array if no log file exists yet. Invaluable when the server is misbehaving and you need to see what it actually printed.
97+
98+
```typescript
99+
import pmxt from 'pmxtjs';
100+
for (const line of pmxt.server.logs(100)) {
101+
console.log(line);
102+
}
52103
```
53104
54105
---

sdks/python/API_REFERENCE.md

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,74 @@ print(markets[0].title)
2929

3030
## Server Management
3131

32-
The SDK provides global functions to manage the background sidecar server. This is useful for clearing state, resolving "port busy" errors, or ensuring a clean slate in interactive environments like Jupyter.
32+
The SDK exposes a `pmxt.server` namespace for managing the background sidecar server. Use these commands for clearing state, resolving "port busy" errors, inspecting server health, or tailing logs from interactive environments like Jupyter.
3333

34-
### `stop_server`
34+
```python
35+
import pmxt
36+
37+
pmxt.server.status() # snapshot dict: running, pid, port, version, uptime_seconds, lock_file
38+
pmxt.server.health() # bool - True if /health responds ok
39+
pmxt.server.start() # idempotent - no-op if already running
40+
pmxt.server.stop() # stop the sidecar and clean up the lock file
41+
pmxt.server.restart() # stop and start the sidecar
42+
pmxt.server.logs(50) # last N log lines from ~/.pmxt/server.log (default 50)
43+
```
44+
45+
### `pmxt.server.status`
46+
47+
Returns a fresh dict describing the sidecar state. Useful for diagnosing "is it running?" before issuing API calls.
48+
49+
```python
50+
import pmxt
51+
info = pmxt.server.status()
52+
print(info["running"], info["pid"], info["port"], info["uptime_seconds"])
53+
```
3554

36-
Stop the background PMXT sidecar server and clean up lock files.
55+
### `pmxt.server.health`
56+
57+
Returns `True` if the sidecar's `/health` endpoint responds with status `ok`, otherwise `False`. Lighter than `status()` when you only need a boolean liveness check.
3758

3859
```python
3960
import pmxt
40-
pmxt.stop_server()
61+
if not pmxt.server.health():
62+
pmxt.server.restart()
4163
```
4264

43-
### `restart_server`
65+
### `pmxt.server.start`
4466

45-
Restart the background PMXT sidecar server. Equivalent to calling `stop_server()` followed by a fresh start.
67+
Idempotently start the sidecar. Returns immediately if a healthy server is already running. Use this when you want to fail fast on startup rather than letting the first API call lazily boot the server.
4668

4769
```python
4870
import pmxt
49-
pmxt.restart_server()
71+
pmxt.server.start()
72+
```
73+
74+
### `pmxt.server.stop`
75+
76+
Stop the running sidecar and clean up its lock file.
77+
78+
```python
79+
import pmxt
80+
pmxt.server.stop()
81+
```
82+
83+
### `pmxt.server.restart`
84+
85+
Stop the sidecar (if running) and start a fresh one. Equivalent to `stop()` followed by `start()`.
86+
87+
```python
88+
import pmxt
89+
pmxt.server.restart()
90+
```
91+
92+
### `pmxt.server.logs`
93+
94+
Return the last `n` lines (default `50`) from the sidecar log file at `~/.pmxt/server.log`. Returns an empty list if no log file exists yet. Invaluable when the server is misbehaving and you need to see what it actually printed.
95+
96+
```python
97+
import pmxt
98+
for line in pmxt.server.logs(100):
99+
print(line)
50100
```
51101

52102
---
@@ -1144,6 +1194,7 @@ class UnifiedMarket:
11441194
market_id: str # The unique identifier for this market
11451195
title: str #
11461196
description: str #
1197+
slug: str #
11471198
outcomes: List[MarketOutcome] #
11481199
event_id: str # Link to parent event
11491200
resolution_date: str #
@@ -1155,6 +1206,9 @@ url: str #
11551206
image: str #
11561207
category: str #
11571208
tags: List[string] #
1209+
tick_size: float # Minimum price increment (e.g., 0.01, 0.001)
1210+
status: str # Venue-native lifecycle status (e.g. 'active', 'closed', 'archived').
1211+
contract_address: str # On-chain contract / condition identifier where applicable (Polymarket conditionId, etc.).
11581212
yes: MarketOutcome #
11591213
no: MarketOutcome #
11601214
up: MarketOutcome #
@@ -1190,6 +1244,8 @@ title: str #
11901244
description: str #
11911245
slug: str #
11921246
markets: List[UnifiedMarket] #
1247+
volume24h: float #
1248+
volume: float # Total / Lifetime volume (sum across markets; undefined if no market provides it)
11931249
url: str #
11941250
image: str #
11951251
category: str #
@@ -1491,6 +1547,8 @@ type: str #
14911547
amount: float #
14921548
price: float #
14931549
fee: float #
1550+
tick_size: float # Optional override for Limitless/Polymarket
1551+
neg_risk: bool # Optional override to skip neg-risk lookup (Polymarket)
14941552
```
14951553

14961554
---

0 commit comments

Comments
 (0)