@exaudeus/workrail 3.24.0 → 3.24.2

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>WorkRail Console</title>
7
- <script type="module" crossorigin src="/console/assets/index-CWETdPGj.js"></script>
8
- <link rel="stylesheet" crossorigin href="/console/assets/index-ByW7d9qr.css">
7
+ <script type="module" crossorigin src="/console/assets/index-TMfptYpQ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/console/assets/index-BXRk3te_.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -303,7 +303,7 @@ async function registerV2Services() {
303
303
  const dataDir = c.resolve(tokens_js_1.DI.V2.DataDir);
304
304
  const fs = c.resolve(tokens_js_1.DI.V2.FileSystem);
305
305
  const clock = c.resolve(tokens_js_1.DI.V2.TimeClock);
306
- return new LocalSessionLockV2(dataDir, fs, clock);
306
+ return new LocalSessionLockV2(dataDir, fs, clock, 'mcp-server');
307
307
  }),
308
308
  });
309
309
  const { ExecutionSessionGateV2 } = await Promise.resolve().then(() => __importStar(require('../v2/usecases/execution-session-gate.js')));
@@ -392,6 +392,17 @@ let HttpServer = class HttpServer {
392
392
  if (!lockData.pid || !lockData.port || !lockData.startedAt) {
393
393
  return { reclaim: true, reason: 'invalid lock structure' };
394
394
  }
395
+ if (lockData.projectId) {
396
+ const currentProjectId = this.sessionManager.getProjectId();
397
+ if (lockData.projectId !== currentProjectId) {
398
+ try {
399
+ process.kill(lockData.pid, 0);
400
+ return { reclaim: false, reason: `different project, primary alive (lock=${lockData.projectId}, current=${currentProjectId})` };
401
+ }
402
+ catch {
403
+ }
404
+ }
405
+ }
395
406
  if (lockData.version !== CURRENT_VERSION) {
396
407
  return { reclaim: true, reason: `version mismatch (lock=${lockData.version}, current=${CURRENT_VERSION})` };
397
408
  }
@@ -369,16 +369,16 @@
369
369
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
370
370
  "bytes": 8011
371
371
  },
372
- "console/assets/index-ByW7d9qr.css": {
373
- "sha256": "1336e0c417833c4fff933ac519b763ab5945ef86c97d25e31c2f82d83b45d0c1",
374
- "bytes": 59845
372
+ "console/assets/index-BXRk3te_.css": {
373
+ "sha256": "2151b10aedd5da99f5bac5cdb54c4cb73894b9069eab0fb859db50b9aa71fba2",
374
+ "bytes": 59906
375
375
  },
376
- "console/assets/index-CWETdPGj.js": {
377
- "sha256": "01ac1ee867b8260ea371d73fee41187368253ac60299ed94846dbcaf1317182d",
378
- "bytes": 742813
376
+ "console/assets/index-TMfptYpQ.js": {
377
+ "sha256": "475c71c4cca93f8ed30139569638b9ec272b43eb44feccb4ce08c6f7e2064dc7",
378
+ "bytes": 744551
379
379
  },
380
380
  "console/index.html": {
381
- "sha256": "26f9f87ae94cb739151d893db85be046dc4fe015d30306f80f4ca8e04e375a48",
381
+ "sha256": "1ff870763f5df29998d56597a3b138e07e23b8fb054c93f6ce9f7f5ae4a0c9a5",
382
382
  "bytes": 417
383
383
  },
384
384
  "core/error-handler.d.ts": {
@@ -394,8 +394,8 @@
394
394
  "bytes": 620
395
395
  },
396
396
  "di/container.js": {
397
- "sha256": "7956cb925d40f61746ccdcec245c5ee40d7d317893eff240a10cff595304d467",
398
- "bytes": 22026
397
+ "sha256": "a3ac972a1d9d6412084cebffb1e612f855f8042c861223d9b849c6569af68f5c",
398
+ "bytes": 22040
399
399
  },
400
400
  "di/tokens.d.ts": {
401
401
  "sha256": "04db6db34348aa36d897c85ab07eb1a386cb04e3595dbcb33525bd33974111ef",
@@ -546,8 +546,8 @@
546
546
  "bytes": 2025
547
547
  },
548
548
  "infrastructure/session/HttpServer.js": {
549
- "sha256": "3f2bc1fb56b8191da261567eb7fb8e78f4c3e33b54c71da361aeea51d047ccd8",
550
- "bytes": 30743
549
+ "sha256": "5d526849ef6e97e5341df73fb9b9789a4bfa7e132f2751b9892f3cbfe09aa2c9",
550
+ "bytes": 31215
551
551
  },
552
552
  "infrastructure/session/SessionDataNormalizer.d.ts": {
553
553
  "sha256": "c89bb5e00d7d01fb4aa6d0095602541de53c425c6b99b67fa8367eb29cb63e9e",
@@ -670,12 +670,12 @@
670
670
  "bytes": 6002
671
671
  },
672
672
  "mcp-server.d.ts": {
673
- "sha256": "7ead2e703f41c763d04b37a1cf433380bec3551fbde206af6694fe9286ad4714",
674
- "bytes": 203
673
+ "sha256": "4ef3c55720fc5deb6fff14a1be5958d9cddd94ce4037af0cb842e3f3ce0cfac0",
674
+ "bytes": 295
675
675
  },
676
676
  "mcp-server.js": {
677
- "sha256": "a825b696428c32b87fdef23722574acb965d8319928b121a287800991a715172",
678
- "bytes": 1599
677
+ "sha256": "ef9c98e49ea95d556e8e8328ff235d7b631a6ef72d1480c22b9409ae4b1c9b5b",
678
+ "bytes": 2687
679
679
  },
680
680
  "mcp/assert-output.d.ts": {
681
681
  "sha256": "f1b821c3652423b15a09d2d1c5a042ee565a503c3d7196bd8220fbe697e0dc75",
@@ -1085,13 +1085,21 @@
1085
1085
  "sha256": "bdea37dfe3f2ef98be01899b067f841c645bda69c23dddd9e181f94b4b157c5e",
1086
1086
  "bytes": 8822
1087
1087
  },
1088
+ "mcp/transports/bridge-entry.d.ts": {
1089
+ "sha256": "83be835fd6beba67c8eb6f1ec23a998599ca13008522dd472d443d824601bbbe",
1090
+ "bytes": 2496
1091
+ },
1092
+ "mcp/transports/bridge-entry.js": {
1093
+ "sha256": "fe8e3c2bbe4890d4715c4665f13d531fff648b24ba3666ef5b0af498e2bef5fc",
1094
+ "bytes": 11811
1095
+ },
1088
1096
  "mcp/transports/http-entry.d.ts": {
1089
1097
  "sha256": "35d313b120dcf38643de9462559163581b89943fe432706986252e8b698b9507",
1090
1098
  "bytes": 70
1091
1099
  },
1092
1100
  "mcp/transports/http-entry.js": {
1093
- "sha256": "053280da2abe9a6ef047aa4c866e5db717aedf5f73d59966c7a7578741b0ad64",
1094
- "bytes": 3205
1101
+ "sha256": "fdebcd58e111f3051081003599e8608fa1becf20abf97a666a957f8048c95334",
1102
+ "bytes": 3314
1095
1103
  },
1096
1104
  "mcp/transports/http-listener.d.ts": {
1097
1105
  "sha256": "6c6cd6dcfe110ed8fa1dc2f9c96caba55959555f98048d3694ac104f42d6d51a",
@@ -2278,12 +2286,12 @@
2278
2286
  "bytes": 7393
2279
2287
  },
2280
2288
  "v2/infra/local/session-lock/index.d.ts": {
2281
- "sha256": "e180a4202587af03e09dc82e2d8557ee2d3139ac6c5f1edee9a277a7032b9986",
2282
- "bytes": 883
2289
+ "sha256": "a0c2e5cbafa9633023f571d89bb41da3a7660d8371473a6344269801c001cbfc",
2290
+ "bytes": 966
2283
2291
  },
2284
2292
  "v2/infra/local/session-lock/index.js": {
2285
- "sha256": "a1cd3eaea35faa014922fe1502a9e029a58528421586c67ab44dc7df0183a0fe",
2286
- "bytes": 2793
2293
+ "sha256": "4832225abcf2fcff1d7562b15a83cab02c8ea9d000b5d9e60ea1e4ef296394a9",
2294
+ "bytes": 3651
2287
2295
  },
2288
2296
  "v2/infra/local/session-store/index.d.ts": {
2289
2297
  "sha256": "33b18ccce92374f8e444a61c63238634087d222f09e834b49dd2ea38d2be3796",
@@ -2666,12 +2674,12 @@
2666
2674
  "bytes": 1572
2667
2675
  },
2668
2676
  "v2/usecases/console-service.js": {
2669
- "sha256": "bbc9fc2e64605cd3025c02a20d73c68a454b3e40a59c251b62ea698fd9252f27",
2670
- "bytes": 27215
2677
+ "sha256": "99f25ac6595cddccb6941c99e9773b075c665eed8b820303efd41a1da1d95e05",
2678
+ "bytes": 30332
2671
2679
  },
2672
2680
  "v2/usecases/console-types.d.ts": {
2673
- "sha256": "36209a82bcd037b50b71ce6559e1e1835af3c3df50392ca4ac0f20990d9fcbae",
2674
- "bytes": 7224
2681
+ "sha256": "9f1060c1689eb1fa6e23e6fbba00ad406c1956e196110c8eab7f95e8ceae81c9",
2682
+ "bytes": 7386
2675
2683
  },
2676
2684
  "v2/usecases/console-types.js": {
2677
2685
  "sha256": "d43aa81f5bc89faa359e0f97c814ba25155591ff078fbb9bfd40f8c7c9683230",
@@ -0,0 +1,70 @@
1
+ import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
2
+ export interface BridgeConfig {
3
+ readonly reconnectBaseDelayMs: number;
4
+ readonly reconnectMaxAttempts: number;
5
+ readonly forwardTimeoutMs: number;
6
+ readonly maxRespawnAttempts: number;
7
+ }
8
+ export declare const DEFAULT_BRIDGE_CONFIG: BridgeConfig;
9
+ type HttpBridgeTransport = {
10
+ readonly send: (msg: JSONRPCMessage) => Promise<void>;
11
+ readonly close: () => Promise<void>;
12
+ };
13
+ export type ConnectionState = {
14
+ readonly kind: 'connecting';
15
+ } | {
16
+ readonly kind: 'connected';
17
+ readonly transport: HttpBridgeTransport;
18
+ } | {
19
+ readonly kind: 'reconnecting';
20
+ readonly attempt: number;
21
+ readonly maxAttempts: number;
22
+ readonly respawnBudget: number;
23
+ } | {
24
+ readonly kind: 'closed';
25
+ };
26
+ export type ReconnectOutcome = {
27
+ readonly kind: 'reconnected';
28
+ } | {
29
+ readonly kind: 'exhausted';
30
+ } | {
31
+ readonly kind: 'aborted';
32
+ };
33
+ export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
34
+ export type SpawnLike = (command: string, args: ReadonlyArray<string>, opts: {
35
+ readonly env: NodeJS.ProcessEnv;
36
+ readonly detached: boolean;
37
+ readonly stdio: 'ignore';
38
+ }) => {
39
+ unref: () => void;
40
+ };
41
+ export declare function detectHealthyPrimary(port: number, opts?: {
42
+ retries?: number;
43
+ baseDelayMs?: number;
44
+ fetch?: FetchLike;
45
+ }): Promise<number | null>;
46
+ export declare function spawnPrimary(port: number, deps: {
47
+ spawn: SpawnLike;
48
+ fetch?: FetchLike;
49
+ }): Promise<void>;
50
+ type ReconnectDeps = {
51
+ readonly detect: (attempt: number) => Promise<boolean>;
52
+ readonly config: Pick<BridgeConfig, 'reconnectBaseDelayMs' | 'reconnectMaxAttempts'>;
53
+ readonly signal: AbortSignal;
54
+ };
55
+ export declare function reconnectWithBackoff(deps: ReconnectDeps): Promise<ReconnectOutcome>;
56
+ type OutcomeHandlerDeps = {
57
+ readonly setConnectionState: (state: ConnectionState) => void;
58
+ readonly performShutdown: (reason: string) => void;
59
+ readonly startReconnectLoop: () => void;
60
+ readonly triggerSpawn: () => Promise<void>;
61
+ readonly config: Pick<BridgeConfig, 'reconnectMaxAttempts'>;
62
+ };
63
+ export declare function handleReconnectOutcome(outcome: ReconnectOutcome, reconnectingState: Extract<ConnectionState, {
64
+ kind: 'reconnecting';
65
+ }>, deps: OutcomeHandlerDeps): Promise<void>;
66
+ export declare function startBridgeServer(primaryPort: number, config?: BridgeConfig, deps?: {
67
+ spawn?: SpawnLike;
68
+ fetch?: FetchLike;
69
+ }): Promise<void>;
70
+ export {};
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEFAULT_BRIDGE_CONFIG = void 0;
37
+ exports.detectHealthyPrimary = detectHealthyPrimary;
38
+ exports.spawnPrimary = spawnPrimary;
39
+ exports.reconnectWithBackoff = reconnectWithBackoff;
40
+ exports.handleReconnectOutcome = handleReconnectOutcome;
41
+ exports.startBridgeServer = startBridgeServer;
42
+ exports.DEFAULT_BRIDGE_CONFIG = {
43
+ reconnectBaseDelayMs: 250,
44
+ reconnectMaxAttempts: 8,
45
+ forwardTimeoutMs: 30000,
46
+ maxRespawnAttempts: 3,
47
+ };
48
+ async function detectHealthyPrimary(port, opts = {}) {
49
+ const retries = opts.retries ?? 3;
50
+ const baseDelayMs = opts.baseDelayMs ?? 200;
51
+ const fetchFn = opts.fetch ?? globalThis.fetch;
52
+ for (let attempt = 0; attempt < retries; attempt++) {
53
+ try {
54
+ const response = await fetchFn(`http://localhost:${port}/workrail-health`, {
55
+ method: 'GET',
56
+ signal: AbortSignal.timeout(500),
57
+ });
58
+ if (response.ok) {
59
+ const body = (await response.json().catch(() => null));
60
+ if (body?.service === 'workrail')
61
+ return port;
62
+ }
63
+ }
64
+ catch {
65
+ }
66
+ if (attempt < retries - 1) {
67
+ await sleep(baseDelayMs * (attempt + 1));
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ async function spawnPrimary(port, deps) {
73
+ await sleep(Math.random() * 300);
74
+ const alreadyUp = await detectHealthyPrimary(port, { retries: 1, fetch: deps.fetch });
75
+ if (alreadyUp != null) {
76
+ console.error('[Bridge] Primary already available after jitter — skipping spawn');
77
+ return;
78
+ }
79
+ const scriptPath = process.argv[1];
80
+ if (scriptPath == null) {
81
+ console.error('[Bridge] Cannot spawn primary: process.argv[1] is undefined');
82
+ return;
83
+ }
84
+ console.error('[Bridge] Spawning new WorkRail primary process');
85
+ try {
86
+ const child = deps.spawn(process.execPath, [scriptPath], {
87
+ env: {
88
+ ...process.env,
89
+ WORKRAIL_TRANSPORT: 'http',
90
+ WORKRAIL_HTTP_PORT: String(port),
91
+ },
92
+ detached: true,
93
+ stdio: 'ignore',
94
+ });
95
+ child.unref();
96
+ }
97
+ catch (err) {
98
+ console.error('[Bridge] Failed to spawn primary:', err);
99
+ }
100
+ }
101
+ async function reconnectWithBackoff(deps) {
102
+ const { detect, config, signal } = deps;
103
+ const { reconnectBaseDelayMs, reconnectMaxAttempts } = config;
104
+ for (let attempt = 0; attempt < reconnectMaxAttempts; attempt++) {
105
+ if (signal.aborted)
106
+ return { kind: 'aborted' };
107
+ const succeeded = await detect(attempt);
108
+ if (succeeded)
109
+ return { kind: 'reconnected' };
110
+ if (attempt < reconnectMaxAttempts - 1) {
111
+ const delay = reconnectBaseDelayMs * Math.pow(2, attempt);
112
+ await sleep(delay);
113
+ if (signal.aborted)
114
+ return { kind: 'aborted' };
115
+ }
116
+ }
117
+ return { kind: 'exhausted' };
118
+ }
119
+ async function handleReconnectOutcome(outcome, reconnectingState, deps) {
120
+ switch (outcome.kind) {
121
+ case 'reconnected':
122
+ console.error('[Bridge] Reconnected to primary');
123
+ return;
124
+ case 'aborted':
125
+ return;
126
+ case 'exhausted':
127
+ if (reconnectingState.respawnBudget > 0) {
128
+ await deps.triggerSpawn();
129
+ deps.setConnectionState({
130
+ kind: 'reconnecting',
131
+ attempt: 0,
132
+ maxAttempts: deps.config.reconnectMaxAttempts,
133
+ respawnBudget: reconnectingState.respawnBudget - 1,
134
+ });
135
+ deps.startReconnectLoop();
136
+ }
137
+ else {
138
+ deps.setConnectionState({ kind: 'closed' });
139
+ deps.performShutdown('respawn budget exhausted — primary repeatedly unavailable');
140
+ }
141
+ return;
142
+ }
143
+ }
144
+ async function startBridgeServer(primaryPort, config = exports.DEFAULT_BRIDGE_CONFIG, deps = {}) {
145
+ console.error(`[Bridge] Forwarding stdio → http://localhost:${primaryPort}/mcp`);
146
+ const { StdioServerTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/server/stdio.js')));
147
+ const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
148
+ const { spawn: nodeSpawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
149
+ const spawnFn = deps.spawn ?? ((command, args, opts) => nodeSpawn(command, args, opts));
150
+ const shutdownController = new AbortController();
151
+ const { signal: shutdownSignal } = shutdownController;
152
+ const stdioTransport = new StdioServerTransport();
153
+ let connectionState = { kind: 'connecting' };
154
+ const setConnectionState = (next) => {
155
+ connectionState = next;
156
+ };
157
+ const performShutdown = (reason) => {
158
+ if (shutdownSignal.aborted)
159
+ return;
160
+ shutdownController.abort();
161
+ console.error(`[Bridge] Shutting down: ${reason}`);
162
+ const state = connectionState;
163
+ void (state.kind === 'connected' ? state.transport.close() : Promise.resolve()).finally(() => process.exit(0));
164
+ };
165
+ const buildConnectedTransport = async () => {
166
+ const t = new StreamableHTTPClientTransport(new URL(`http://localhost:${primaryPort}/mcp`));
167
+ t.onerror = (err) => console.error('[Bridge] HTTP transport error:', err);
168
+ t.onmessage = (msg) => {
169
+ void stdioTransport.send(msg).catch((err) => {
170
+ console.error('[Bridge] Forward to IDE failed:', err);
171
+ });
172
+ };
173
+ t.onclose = () => {
174
+ if (shutdownSignal.aborted)
175
+ return;
176
+ const current = connectionState;
177
+ if (current.kind === 'connecting' || current.kind === 'reconnecting')
178
+ return;
179
+ console.error('[Bridge] Primary connection lost — reconnecting');
180
+ setConnectionState({
181
+ kind: 'reconnecting',
182
+ attempt: 0,
183
+ maxAttempts: config.reconnectMaxAttempts,
184
+ respawnBudget: config.maxRespawnAttempts,
185
+ });
186
+ startReconnectLoop();
187
+ };
188
+ try {
189
+ await t.start();
190
+ const transport = { send: (msg) => t.send(msg), close: () => t.close() };
191
+ setConnectionState({ kind: 'connected', transport });
192
+ return transport;
193
+ }
194
+ catch {
195
+ return null;
196
+ }
197
+ };
198
+ const startReconnectLoop = () => {
199
+ const stateAtStart = connectionState;
200
+ if (stateAtStart.kind !== 'reconnecting')
201
+ return;
202
+ void reconnectWithBackoff({
203
+ signal: shutdownSignal,
204
+ config,
205
+ detect: async (attempt) => {
206
+ console.error(`[Bridge] Reconnect attempt ${attempt + 1}/${config.reconnectMaxAttempts}`);
207
+ const detected = await detectHealthyPrimary(primaryPort, { retries: 1, fetch: deps.fetch });
208
+ if (detected == null)
209
+ return false;
210
+ const transport = await buildConnectedTransport();
211
+ return transport != null;
212
+ },
213
+ })
214
+ .then((outcome) => {
215
+ const stateAtOutcome = connectionState;
216
+ if (stateAtOutcome.kind !== 'reconnecting')
217
+ return;
218
+ return handleReconnectOutcome(outcome, stateAtOutcome, {
219
+ setConnectionState,
220
+ performShutdown,
221
+ startReconnectLoop,
222
+ triggerSpawn: () => spawnPrimary(primaryPort, { spawn: spawnFn, fetch: deps.fetch }),
223
+ config,
224
+ });
225
+ })
226
+ .catch((err) => {
227
+ console.error('[Bridge] Unexpected error in reconnect loop:', err);
228
+ });
229
+ };
230
+ stdioTransport.onmessage = (msg) => {
231
+ const state = connectionState;
232
+ switch (state.kind) {
233
+ case 'connected': {
234
+ const timer = setTimeout(() => {
235
+ console.error('[Bridge] Warning: no response from primary after', config.forwardTimeoutMs, 'ms');
236
+ }, config.forwardTimeoutMs);
237
+ void state.transport
238
+ .send(msg)
239
+ .catch((err) => console.error('[Bridge] Forward to primary failed:', err))
240
+ .finally(() => clearTimeout(timer));
241
+ return;
242
+ }
243
+ case 'connecting':
244
+ case 'reconnecting':
245
+ sendUnavailableError(msg, (m) => stdioTransport.send(m));
246
+ return;
247
+ case 'closed':
248
+ return;
249
+ }
250
+ };
251
+ stdioTransport.onerror = (err) => console.error('[Bridge] Stdio error:', err);
252
+ const initialTransport = await buildConnectedTransport();
253
+ if (initialTransport == null) {
254
+ throw new Error(`[Bridge] Failed to connect to primary on port ${primaryPort}`);
255
+ }
256
+ console.error('[Bridge] Connected to primary');
257
+ process.stdout.on('error', (err) => {
258
+ const code = err.code;
259
+ const reason = code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED'
260
+ ? 'stdout pipe broken (client disconnected)'
261
+ : `stdout error: ${String(err)}`;
262
+ performShutdown(reason);
263
+ });
264
+ await stdioTransport.start();
265
+ console.error('[Bridge] WorkRail MCP bridge running on stdio');
266
+ process.stdin.once('end', () => performShutdown('stdin closed'));
267
+ process.once('SIGINT', () => performShutdown('SIGINT'));
268
+ process.once('SIGTERM', () => performShutdown('SIGTERM'));
269
+ process.once('SIGHUP', () => performShutdown('SIGHUP'));
270
+ }
271
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
272
+ function sendUnavailableError(msg, send) {
273
+ if (!('id' in msg) || msg.id == null)
274
+ return;
275
+ void send({
276
+ jsonrpc: '2.0',
277
+ id: msg.id,
278
+ error: {
279
+ code: -32603,
280
+ message: 'WorkRail primary server is temporarily unavailable — reconnecting. ' +
281
+ 'Wait a few seconds and retry your tool call. ' +
282
+ 'If this persists, tell the user: ' +
283
+ '"WorkRail disconnected. Check the terminal running workrail for the ' +
284
+ 'error message, then run /mcp in Claude to reconnect."',
285
+ },
286
+ }).catch(() => undefined);
287
+ }
@@ -56,6 +56,9 @@ async function startHttpServer(port) {
56
56
  listener.app.post('/mcp', (req, res) => transport.handleRequest(req, res, req.body));
57
57
  listener.app.get('/mcp', (req, res) => transport.handleRequest(req, res));
58
58
  listener.app.delete('/mcp', (req, res) => transport.handleRequest(req, res));
59
+ listener.app.get('/workrail-health', (_req, res) => {
60
+ res.json({ service: 'workrail' });
61
+ });
59
62
  await server.connect(transport);
60
63
  const boundPort = listener.getBoundPort();
61
64
  console.error('[Transport] WorkRail MCP Server running on HTTP');
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  export { startStdioServer } from './mcp/transports/stdio-entry.js';
3
3
  export { startHttpServer } from './mcp/transports/http-entry.js';
4
+ export { startBridgeServer, detectHealthyPrimary } from './mcp/transports/bridge-entry.js';
4
5
  export { composeServer } from './mcp/server.js';
@@ -1,31 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.composeServer = exports.startHttpServer = exports.startStdioServer = void 0;
4
+ exports.composeServer = exports.detectHealthyPrimary = exports.startBridgeServer = exports.startHttpServer = exports.startStdioServer = void 0;
5
5
  const transport_mode_js_1 = require("./mcp/transports/transport-mode.js");
6
6
  const stdio_entry_js_1 = require("./mcp/transports/stdio-entry.js");
7
7
  const http_entry_js_1 = require("./mcp/transports/http-entry.js");
8
+ const bridge_entry_js_1 = require("./mcp/transports/bridge-entry.js");
8
9
  const assert_never_js_1 = require("./runtime/assert-never.js");
9
10
  var stdio_entry_js_2 = require("./mcp/transports/stdio-entry.js");
10
11
  Object.defineProperty(exports, "startStdioServer", { enumerable: true, get: function () { return stdio_entry_js_2.startStdioServer; } });
11
12
  var http_entry_js_2 = require("./mcp/transports/http-entry.js");
12
13
  Object.defineProperty(exports, "startHttpServer", { enumerable: true, get: function () { return http_entry_js_2.startHttpServer; } });
14
+ var bridge_entry_js_2 = require("./mcp/transports/bridge-entry.js");
15
+ Object.defineProperty(exports, "startBridgeServer", { enumerable: true, get: function () { return bridge_entry_js_2.startBridgeServer; } });
16
+ Object.defineProperty(exports, "detectHealthyPrimary", { enumerable: true, get: function () { return bridge_entry_js_2.detectHealthyPrimary; } });
13
17
  var server_js_1 = require("./mcp/server.js");
14
18
  Object.defineProperty(exports, "composeServer", { enumerable: true, get: function () { return server_js_1.composeServer; } });
15
- const mode = (0, transport_mode_js_1.resolveTransportMode)(process.env);
16
- switch (mode.kind) {
17
- case 'stdio':
18
- (0, stdio_entry_js_1.startStdioServer)().catch((error) => {
19
- console.error('[stdio] Fatal error:', error);
20
- process.exit(1);
21
- });
22
- break;
23
- case 'http':
24
- (0, http_entry_js_1.startHttpServer)(mode.port).catch((error) => {
25
- console.error('[http] Fatal error:', error);
26
- process.exit(1);
27
- });
28
- break;
29
- default:
30
- (0, assert_never_js_1.assertNever)(mode);
19
+ const DEFAULT_MCP_PORT = 3100;
20
+ async function main() {
21
+ const mode = (0, transport_mode_js_1.resolveTransportMode)(process.env);
22
+ if (mode.kind === 'stdio') {
23
+ const primaryPort = await (0, bridge_entry_js_1.detectHealthyPrimary)(DEFAULT_MCP_PORT);
24
+ if (primaryPort != null) {
25
+ console.error(`[Startup] Primary detected on :${primaryPort} — starting in bridge mode`);
26
+ try {
27
+ await (0, bridge_entry_js_1.startBridgeServer)(primaryPort);
28
+ }
29
+ catch (error) {
30
+ console.error('[Bridge] Fatal error, falling back to full stdio server:', error);
31
+ await (0, stdio_entry_js_1.startStdioServer)();
32
+ }
33
+ return;
34
+ }
35
+ }
36
+ switch (mode.kind) {
37
+ case 'stdio':
38
+ await (0, stdio_entry_js_1.startStdioServer)();
39
+ break;
40
+ case 'http':
41
+ await (0, http_entry_js_1.startHttpServer)(mode.port);
42
+ break;
43
+ default:
44
+ (0, assert_never_js_1.assertNever)(mode);
45
+ }
31
46
  }
47
+ main().catch((error) => {
48
+ console.error('[Startup] Fatal error:', error);
49
+ process.exit(1);
50
+ });
@@ -8,7 +8,9 @@ export declare class LocalSessionLockV2 implements SessionLockPortV2 {
8
8
  private readonly dataDir;
9
9
  private readonly fs;
10
10
  private readonly clock;
11
- constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, clock: TimeClockPortV2);
11
+ private readonly workerId;
12
+ private readonly instanceId;
13
+ constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, clock: TimeClockPortV2, workerId?: string);
12
14
  private clearIfStaleLock;
13
15
  acquire(sessionId: SessionId): ResultAsync<SessionLockHandleV2, SessionLockError>;
14
16
  release(handle: SessionLockHandleV2): ResultAsync<void, SessionLockError>;
@@ -2,11 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalSessionLockV2 = void 0;
4
4
  const neverthrow_1 = require("neverthrow");
5
+ const node_crypto_1 = require("node:crypto");
5
6
  class LocalSessionLockV2 {
6
- constructor(dataDir, fs, clock) {
7
+ constructor(dataDir, fs, clock, workerId = 'default') {
7
8
  this.dataDir = dataDir;
8
9
  this.fs = fs;
9
10
  this.clock = clock;
11
+ this.workerId = workerId;
12
+ this.instanceId = (0, node_crypto_1.randomUUID)();
10
13
  }
11
14
  clearIfStaleLock(lockPath) {
12
15
  return this.fs
@@ -17,6 +20,18 @@ class LocalSessionLockV2 {
17
20
  const pid = typeof data.pid === 'number' ? data.pid : null;
18
21
  if (pid === null)
19
22
  return false;
23
+ const myPid = this.clock.getPid();
24
+ const lockWorkerId = typeof data.workerId === 'string' ? data.workerId : undefined;
25
+ if (pid === myPid && lockWorkerId !== undefined) {
26
+ if (lockWorkerId !== this.workerId) {
27
+ return false;
28
+ }
29
+ const lockInstanceId = typeof data.instanceId === 'string' ? data.instanceId : undefined;
30
+ if (lockInstanceId === this.instanceId) {
31
+ return false;
32
+ }
33
+ return true;
34
+ }
20
35
  try {
21
36
  process.kill(pid, 0);
22
37
  return false;
@@ -58,6 +73,8 @@ class LocalSessionLockV2 {
58
73
  v: 1,
59
74
  sessionId,
60
75
  pid: this.clock.getPid(),
76
+ workerId: this.workerId,
77
+ instanceId: this.instanceId,
61
78
  startedAtMs: this.clock.nowMs(),
62
79
  }))))
63
80
  .andThen(({ fd }) => this.fs.fsyncFile(fd).andThen(() => this.fs.closeFile(fd)))