@exaudeus/workrail 3.32.0 → 3.34.0
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/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +488 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-C1JXnwZS.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +27 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +63 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +285 -46
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +136 -104
- package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
- package/dist/mcp/handlers/v2-error-mapping.js +2 -0
- package/dist/mcp/handlers/v2-execution/advance.js +25 -0
- package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +0 -5
- package/dist/mcp/transports/stdio-entry.js +24 -12
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +1 -42
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
- package/dist/v2/durable-core/domain/observation-builder.js +2 -2
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
- package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +185 -10
- package/dist/v2/usecases/console-types.d.ts +8 -0
- package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
- package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
- package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/discovery/design-candidates.md +180 -0
- package/docs/discovery/design-review-findings.md +110 -0
- package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
- package/docs/ideas/backlog.md +627 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +59 -8
- package/dist/mcp/transports/bridge-entry.d.ts +0 -102
- package/dist/mcp/transports/bridge-entry.js +0 -454
- package/dist/mcp/transports/bridge-events.d.ts +0 -51
- package/dist/mcp/transports/bridge-events.js +0 -24
- package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
- package/dist/mcp/transports/primary-tombstone.js +0 -51
- /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
readonly spawnLockStaleMs: number;
|
|
8
|
-
readonly waitForPrimaryPollMs: number;
|
|
9
|
-
}
|
|
10
|
-
export declare const DEFAULT_BRIDGE_CONFIG: BridgeConfig;
|
|
11
|
-
type HttpBridgeTransport = {
|
|
12
|
-
readonly send: (msg: JSONRPCMessage) => Promise<void>;
|
|
13
|
-
readonly close: () => Promise<void>;
|
|
14
|
-
};
|
|
15
|
-
export type ConnectionState = {
|
|
16
|
-
readonly kind: 'connecting';
|
|
17
|
-
} | {
|
|
18
|
-
readonly kind: 'connected';
|
|
19
|
-
readonly transport: HttpBridgeTransport;
|
|
20
|
-
} | {
|
|
21
|
-
readonly kind: 'reconnecting';
|
|
22
|
-
readonly attempt: number;
|
|
23
|
-
readonly maxAttempts: number;
|
|
24
|
-
readonly respawnBudget: number;
|
|
25
|
-
} | {
|
|
26
|
-
readonly kind: 'waiting_for_primary';
|
|
27
|
-
} | {
|
|
28
|
-
readonly kind: 'closed';
|
|
29
|
-
};
|
|
30
|
-
export type ReconnectOutcome = {
|
|
31
|
-
readonly kind: 'reconnected';
|
|
32
|
-
} | {
|
|
33
|
-
readonly kind: 'exhausted';
|
|
34
|
-
} | {
|
|
35
|
-
readonly kind: 'aborted';
|
|
36
|
-
};
|
|
37
|
-
export type SpawnLockResult = {
|
|
38
|
-
readonly kind: 'acquired';
|
|
39
|
-
} | {
|
|
40
|
-
readonly kind: 'skipped';
|
|
41
|
-
readonly reason: string;
|
|
42
|
-
};
|
|
43
|
-
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
44
|
-
export type SpawnLike = (command: string, args: ReadonlyArray<string>, opts: {
|
|
45
|
-
readonly env: NodeJS.ProcessEnv;
|
|
46
|
-
readonly detached: boolean;
|
|
47
|
-
readonly stdio: 'ignore';
|
|
48
|
-
}) => {
|
|
49
|
-
unref: () => void;
|
|
50
|
-
};
|
|
51
|
-
export type WriteFileSyncLike = (path: string, content: string, opts: {
|
|
52
|
-
flag: 'wx';
|
|
53
|
-
}) => void;
|
|
54
|
-
export type StatSyncLike = (path: string) => {
|
|
55
|
-
mtimeMs: number;
|
|
56
|
-
};
|
|
57
|
-
export type UnlinkSyncLike = (path: string) => void;
|
|
58
|
-
export type HealthResponse = {
|
|
59
|
-
readonly port: number;
|
|
60
|
-
readonly pid: number;
|
|
61
|
-
};
|
|
62
|
-
export declare function detectHealthyPrimary(port: number, opts?: {
|
|
63
|
-
retries?: number;
|
|
64
|
-
baseDelayMs?: number;
|
|
65
|
-
fetch?: FetchLike;
|
|
66
|
-
}): Promise<HealthResponse | null>;
|
|
67
|
-
export declare function spawnLockPath(port: number): string;
|
|
68
|
-
export declare function acquireSpawnLock(port: number, staleMs: number, deps?: {
|
|
69
|
-
readonly writeFileSync?: WriteFileSyncLike;
|
|
70
|
-
readonly statSync?: StatSyncLike;
|
|
71
|
-
readonly unlinkSync?: UnlinkSyncLike;
|
|
72
|
-
}): SpawnLockResult;
|
|
73
|
-
export declare function releaseSpawnLock(port: number, deps?: {
|
|
74
|
-
readonly unlinkSync?: UnlinkSyncLike;
|
|
75
|
-
}): void;
|
|
76
|
-
export declare function spawnPrimary(port: number, deps: {
|
|
77
|
-
spawn: SpawnLike;
|
|
78
|
-
fetch?: FetchLike;
|
|
79
|
-
}): Promise<void>;
|
|
80
|
-
type ReconnectDeps = {
|
|
81
|
-
readonly detect: (attempt: number) => Promise<boolean>;
|
|
82
|
-
readonly config: Pick<BridgeConfig, 'reconnectBaseDelayMs' | 'reconnectMaxAttempts'>;
|
|
83
|
-
readonly signal: AbortSignal;
|
|
84
|
-
};
|
|
85
|
-
export declare function reconnectWithBackoff(deps: ReconnectDeps): Promise<ReconnectOutcome>;
|
|
86
|
-
type OutcomeHandlerDeps = {
|
|
87
|
-
readonly setConnectionState: (state: ConnectionState) => void;
|
|
88
|
-
readonly performShutdown: (reason: string) => void;
|
|
89
|
-
readonly startReconnectLoop: () => void;
|
|
90
|
-
readonly startWaitLoop: () => void;
|
|
91
|
-
readonly triggerSpawn: () => Promise<void>;
|
|
92
|
-
readonly config: Pick<BridgeConfig, 'reconnectMaxAttempts'>;
|
|
93
|
-
};
|
|
94
|
-
export declare function handleReconnectOutcome(outcome: ReconnectOutcome, reconnectingState: Extract<ConnectionState, {
|
|
95
|
-
kind: 'reconnecting';
|
|
96
|
-
}>, deps: OutcomeHandlerDeps): Promise<void>;
|
|
97
|
-
export declare function startBridgeServer(primaryPort: number, config?: BridgeConfig, deps?: {
|
|
98
|
-
spawn?: SpawnLike;
|
|
99
|
-
fetch?: FetchLike;
|
|
100
|
-
originalPrimaryPid?: number;
|
|
101
|
-
}): Promise<void>;
|
|
102
|
-
export {};
|
|
@@ -1,454 +0,0 @@
|
|
|
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.spawnLockPath = spawnLockPath;
|
|
39
|
-
exports.acquireSpawnLock = acquireSpawnLock;
|
|
40
|
-
exports.releaseSpawnLock = releaseSpawnLock;
|
|
41
|
-
exports.spawnPrimary = spawnPrimary;
|
|
42
|
-
exports.reconnectWithBackoff = reconnectWithBackoff;
|
|
43
|
-
exports.handleReconnectOutcome = handleReconnectOutcome;
|
|
44
|
-
exports.startBridgeServer = startBridgeServer;
|
|
45
|
-
const fatal_exit_js_1 = require("./fatal-exit.js");
|
|
46
|
-
const bridge_events_js_1 = require("./bridge-events.js");
|
|
47
|
-
const primary_tombstone_js_1 = require("./primary-tombstone.js");
|
|
48
|
-
const fs_1 = require("fs");
|
|
49
|
-
const os_1 = require("os");
|
|
50
|
-
const path_1 = require("path");
|
|
51
|
-
exports.DEFAULT_BRIDGE_CONFIG = {
|
|
52
|
-
reconnectBaseDelayMs: 250,
|
|
53
|
-
reconnectMaxAttempts: 8,
|
|
54
|
-
forwardTimeoutMs: 30000,
|
|
55
|
-
maxRespawnAttempts: 3,
|
|
56
|
-
spawnLockStaleMs: 30000,
|
|
57
|
-
waitForPrimaryPollMs: 5000,
|
|
58
|
-
};
|
|
59
|
-
async function detectHealthyPrimary(port, opts = {}) {
|
|
60
|
-
const retries = opts.retries ?? 3;
|
|
61
|
-
const baseDelayMs = opts.baseDelayMs ?? 200;
|
|
62
|
-
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
63
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetchFn(`http://localhost:${port}/workrail-health`, {
|
|
66
|
-
method: 'GET',
|
|
67
|
-
signal: AbortSignal.timeout(500),
|
|
68
|
-
});
|
|
69
|
-
if (response.ok) {
|
|
70
|
-
const body = (await response.json().catch(() => null));
|
|
71
|
-
if (body?.service === 'workrail') {
|
|
72
|
-
const pid = typeof body.pid === 'number' ? body.pid : 0;
|
|
73
|
-
return { port, pid };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
}
|
|
79
|
-
if (attempt < retries - 1) {
|
|
80
|
-
await sleep(baseDelayMs * (attempt + 1));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
function spawnLockPath(port) {
|
|
86
|
-
return (0, path_1.join)((0, os_1.homedir)(), '.workrail', `spawn-coordinator-${port}.lock`);
|
|
87
|
-
}
|
|
88
|
-
function acquireSpawnLock(port, staleMs, deps = {}) {
|
|
89
|
-
const lockPath = spawnLockPath(port);
|
|
90
|
-
const writeFn = deps.writeFileSync ?? fs_1.writeFileSync;
|
|
91
|
-
const statFn = deps.statSync ?? require('fs').statSync;
|
|
92
|
-
const unlinkFn = deps.unlinkSync ?? require('fs').unlinkSync;
|
|
93
|
-
try {
|
|
94
|
-
(0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.workrail'), { recursive: true });
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return { kind: 'skipped', reason: 'mkdirSync failed' };
|
|
98
|
-
}
|
|
99
|
-
try {
|
|
100
|
-
writeFn(lockPath, String(process.pid), { flag: 'wx' });
|
|
101
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_lock_acquired', port });
|
|
102
|
-
return { kind: 'acquired' };
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
const code = err.code;
|
|
106
|
-
if (code === 'EEXIST') {
|
|
107
|
-
try {
|
|
108
|
-
const stat = statFn(lockPath);
|
|
109
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
110
|
-
if (ageMs > staleMs) {
|
|
111
|
-
try {
|
|
112
|
-
unlinkFn(lockPath);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
writeFn(lockPath, String(process.pid), { flag: 'wx' });
|
|
118
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_lock_acquired', port });
|
|
119
|
-
return { kind: 'acquired' };
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_lock_skipped', reason: 'lost race after stale reclaim' });
|
|
123
|
-
return { kind: 'skipped', reason: 'lost race after stale reclaim' };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_lock_skipped', reason: 'lock held by another bridge' });
|
|
127
|
-
return { kind: 'skipped', reason: 'lock held by another bridge' };
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_lock_skipped', reason: 'lock contested' });
|
|
131
|
-
return { kind: 'skipped', reason: 'lock contested' };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_lock_skipped', reason: `unexpected error: ${code ?? String(err)}` });
|
|
135
|
-
return { kind: 'skipped', reason: `unexpected error: ${code ?? String(err)}` };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function releaseSpawnLock(port, deps = {}) {
|
|
139
|
-
const unlinkFn = deps.unlinkSync ?? require('fs').unlinkSync;
|
|
140
|
-
try {
|
|
141
|
-
unlinkFn(spawnLockPath(port));
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function spawnPrimary(port, deps) {
|
|
147
|
-
await sleep(Math.random() * 2000);
|
|
148
|
-
const primaryDetected = await detectHealthyPrimary(port, { retries: 3, baseDelayMs: 500, fetch: deps.fetch });
|
|
149
|
-
if (primaryDetected != null) {
|
|
150
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_skipped', reason: 'primary already up after jitter' });
|
|
151
|
-
console.error('[Bridge] Primary already available after jitter — skipping spawn');
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const scriptPath = process.argv[1];
|
|
155
|
-
if (scriptPath == null) {
|
|
156
|
-
console.error('[Bridge] Cannot spawn primary: process.argv[1] is undefined');
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'spawn_primary', port });
|
|
160
|
-
console.error('[Bridge] Spawning new WorkRail primary process');
|
|
161
|
-
try {
|
|
162
|
-
const child = deps.spawn(process.execPath, [scriptPath], {
|
|
163
|
-
env: {
|
|
164
|
-
...process.env,
|
|
165
|
-
WORKRAIL_TRANSPORT: 'http',
|
|
166
|
-
WORKRAIL_HTTP_PORT: String(port),
|
|
167
|
-
},
|
|
168
|
-
detached: true,
|
|
169
|
-
stdio: 'ignore',
|
|
170
|
-
});
|
|
171
|
-
child.unref();
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
console.error('[Bridge] Failed to spawn primary:', err);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async function reconnectWithBackoff(deps) {
|
|
178
|
-
const { detect, config, signal } = deps;
|
|
179
|
-
const { reconnectBaseDelayMs, reconnectMaxAttempts } = config;
|
|
180
|
-
for (let attempt = 0; attempt < reconnectMaxAttempts; attempt++) {
|
|
181
|
-
if (signal.aborted)
|
|
182
|
-
return { kind: 'aborted' };
|
|
183
|
-
const succeeded = await detect(attempt);
|
|
184
|
-
if (succeeded)
|
|
185
|
-
return { kind: 'reconnected' };
|
|
186
|
-
if (attempt < reconnectMaxAttempts - 1) {
|
|
187
|
-
const delay = reconnectBaseDelayMs * Math.pow(2, attempt);
|
|
188
|
-
await sleep(delay);
|
|
189
|
-
if (signal.aborted)
|
|
190
|
-
return { kind: 'aborted' };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return { kind: 'exhausted' };
|
|
194
|
-
}
|
|
195
|
-
async function handleReconnectOutcome(outcome, reconnectingState, deps) {
|
|
196
|
-
switch (outcome.kind) {
|
|
197
|
-
case 'reconnected':
|
|
198
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'reconnected', attempt: reconnectingState.attempt });
|
|
199
|
-
console.error('[Bridge] Reconnected to primary');
|
|
200
|
-
return;
|
|
201
|
-
case 'aborted':
|
|
202
|
-
return;
|
|
203
|
-
case 'exhausted':
|
|
204
|
-
if (reconnectingState.respawnBudget > 0) {
|
|
205
|
-
await deps.triggerSpawn();
|
|
206
|
-
deps.setConnectionState({
|
|
207
|
-
kind: 'reconnecting',
|
|
208
|
-
attempt: 0,
|
|
209
|
-
maxAttempts: deps.config.reconnectMaxAttempts,
|
|
210
|
-
respawnBudget: reconnectingState.respawnBudget - 1,
|
|
211
|
-
});
|
|
212
|
-
deps.startReconnectLoop();
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
(0, bridge_events_js_1.logBridgeEvent)({
|
|
216
|
-
kind: 'budget_exhausted',
|
|
217
|
-
budgetUsed: reconnectingState.maxAttempts,
|
|
218
|
-
respawnBudget: reconnectingState.respawnBudget,
|
|
219
|
-
});
|
|
220
|
-
console.error('[Bridge] Spawn budget exhausted — entering slow-poll mode (5s interval)');
|
|
221
|
-
deps.setConnectionState({ kind: 'waiting_for_primary' });
|
|
222
|
-
deps.startWaitLoop();
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function startBridgeServer(primaryPort, config = exports.DEFAULT_BRIDGE_CONFIG, deps = {}) {
|
|
228
|
-
(0, fatal_exit_js_1.registerFatalHandlers)('bridge');
|
|
229
|
-
(0, fatal_exit_js_1.logStartup)('bridge', { primaryPort });
|
|
230
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'started', primaryPort, ppid: process.ppid });
|
|
231
|
-
const { StdioServerTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/server/stdio.js')));
|
|
232
|
-
const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
|
|
233
|
-
const { spawn: nodeSpawn } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
234
|
-
const spawnFn = deps.spawn ?? ((command, args, opts) => nodeSpawn(command, args, opts));
|
|
235
|
-
const shutdownController = new AbortController();
|
|
236
|
-
const { signal: shutdownSignal } = shutdownController;
|
|
237
|
-
const stdioTransport = new StdioServerTransport();
|
|
238
|
-
let connectionState = { kind: 'connecting' };
|
|
239
|
-
const setConnectionState = (next) => {
|
|
240
|
-
connectionState = next;
|
|
241
|
-
};
|
|
242
|
-
const performShutdown = (reason) => {
|
|
243
|
-
if (shutdownSignal.aborted)
|
|
244
|
-
return;
|
|
245
|
-
shutdownController.abort();
|
|
246
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'shutdown', reason });
|
|
247
|
-
process.stderr.write(`[Bridge] Shutdown pid=${process.pid} reason="${reason}" ts=${new Date().toISOString()}\n`);
|
|
248
|
-
const state = connectionState;
|
|
249
|
-
void (state.kind === 'connected' ? state.transport.close() : Promise.resolve()).finally(() => process.exit(0));
|
|
250
|
-
};
|
|
251
|
-
const buildConnectedTransport = async () => {
|
|
252
|
-
const t = new StreamableHTTPClientTransport(new URL(`http://localhost:${primaryPort}/mcp`));
|
|
253
|
-
t.onerror = (err) => console.error('[Bridge] HTTP transport error:', err);
|
|
254
|
-
t.onmessage = (msg) => {
|
|
255
|
-
void stdioTransport.send(msg).catch((err) => {
|
|
256
|
-
console.error('[Bridge] Forward to IDE failed:', err);
|
|
257
|
-
});
|
|
258
|
-
};
|
|
259
|
-
t.onclose = () => {
|
|
260
|
-
if (shutdownSignal.aborted)
|
|
261
|
-
return;
|
|
262
|
-
const current = connectionState;
|
|
263
|
-
if (current.kind === 'connecting' ||
|
|
264
|
-
current.kind === 'reconnecting' ||
|
|
265
|
-
current.kind === 'waiting_for_primary')
|
|
266
|
-
return;
|
|
267
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'disconnected' });
|
|
268
|
-
console.error('[Bridge] Primary connection lost — reconnecting');
|
|
269
|
-
const connectedPrimaryPid = deps.originalPrimaryPid ?? 0;
|
|
270
|
-
if (connectedPrimaryPid > 0) {
|
|
271
|
-
const tombstone = (0, primary_tombstone_js_1.readTombstone)();
|
|
272
|
-
if (tombstone?.pid === connectedPrimaryPid) {
|
|
273
|
-
console.error('[Bridge] Tombstone detected — primary died cleanly, entering slow-poll mode');
|
|
274
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'waiting_for_primary', port: primaryPort });
|
|
275
|
-
setConnectionState({ kind: 'waiting_for_primary' });
|
|
276
|
-
startWaitLoop();
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
setConnectionState({
|
|
281
|
-
kind: 'reconnecting',
|
|
282
|
-
attempt: 0,
|
|
283
|
-
maxAttempts: config.reconnectMaxAttempts,
|
|
284
|
-
respawnBudget: config.maxRespawnAttempts,
|
|
285
|
-
});
|
|
286
|
-
startReconnectLoop();
|
|
287
|
-
};
|
|
288
|
-
try {
|
|
289
|
-
await t.start();
|
|
290
|
-
const transport = { send: (msg) => t.send(msg), close: () => t.close() };
|
|
291
|
-
setConnectionState({ kind: 'connected', transport });
|
|
292
|
-
return transport;
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
const startReconnectLoop = () => {
|
|
299
|
-
const stateAtStart = connectionState;
|
|
300
|
-
if (stateAtStart.kind !== 'reconnecting')
|
|
301
|
-
return;
|
|
302
|
-
void reconnectWithBackoff({
|
|
303
|
-
signal: shutdownSignal,
|
|
304
|
-
config,
|
|
305
|
-
detect: async (attempt) => {
|
|
306
|
-
const connectedPrimaryPid = deps.originalPrimaryPid ?? 0;
|
|
307
|
-
if (connectedPrimaryPid > 0) {
|
|
308
|
-
const tombstone = (0, primary_tombstone_js_1.readTombstone)();
|
|
309
|
-
if (tombstone?.pid === connectedPrimaryPid) {
|
|
310
|
-
console.error('[Bridge] Tombstone detected during reconnect — entering slow-poll mode');
|
|
311
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'waiting_for_primary', port: primaryPort });
|
|
312
|
-
setConnectionState({ kind: 'waiting_for_primary' });
|
|
313
|
-
startWaitLoop();
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'reconnect_attempt', attempt: attempt + 1, maxAttempts: config.reconnectMaxAttempts });
|
|
318
|
-
console.error(`[Bridge] Reconnect attempt ${attempt + 1}/${config.reconnectMaxAttempts}`);
|
|
319
|
-
const detected = await detectHealthyPrimary(primaryPort, { retries: 1, fetch: deps.fetch });
|
|
320
|
-
if (detected == null)
|
|
321
|
-
return false;
|
|
322
|
-
const transport = await buildConnectedTransport();
|
|
323
|
-
return transport != null;
|
|
324
|
-
},
|
|
325
|
-
})
|
|
326
|
-
.then((outcome) => {
|
|
327
|
-
const stateAtOutcome = connectionState;
|
|
328
|
-
if (stateAtOutcome.kind !== 'reconnecting')
|
|
329
|
-
return;
|
|
330
|
-
return handleReconnectOutcome(outcome, stateAtOutcome, {
|
|
331
|
-
setConnectionState,
|
|
332
|
-
performShutdown,
|
|
333
|
-
startReconnectLoop,
|
|
334
|
-
startWaitLoop,
|
|
335
|
-
triggerSpawn: async () => {
|
|
336
|
-
const lockResult = acquireSpawnLock(primaryPort, config.spawnLockStaleMs);
|
|
337
|
-
if (lockResult.kind === 'skipped') {
|
|
338
|
-
console.error(`[Bridge] Spawn skipped (coordinator lock): ${lockResult.reason}`);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
try {
|
|
342
|
-
await spawnPrimary(primaryPort, { spawn: spawnFn, fetch: deps.fetch });
|
|
343
|
-
}
|
|
344
|
-
finally {
|
|
345
|
-
releaseSpawnLock(primaryPort);
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
config,
|
|
349
|
-
});
|
|
350
|
-
})
|
|
351
|
-
.catch((err) => {
|
|
352
|
-
const errObj = err instanceof Error ? err : new Error(String(err));
|
|
353
|
-
(0, bridge_events_js_1.logBridgeEvent)({
|
|
354
|
-
kind: 'reconnect_loop_error',
|
|
355
|
-
message: errObj.message,
|
|
356
|
-
stack: errObj.stack ?? null,
|
|
357
|
-
});
|
|
358
|
-
console.error('[Bridge] Unexpected error in reconnect loop:', err);
|
|
359
|
-
});
|
|
360
|
-
};
|
|
361
|
-
const startWaitLoop = () => {
|
|
362
|
-
const stateAtStart = connectionState;
|
|
363
|
-
if (stateAtStart.kind !== 'waiting_for_primary')
|
|
364
|
-
return;
|
|
365
|
-
void (async () => {
|
|
366
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'waiting_for_primary', port: primaryPort });
|
|
367
|
-
while (!shutdownSignal.aborted) {
|
|
368
|
-
await sleep(config.waitForPrimaryPollMs);
|
|
369
|
-
if (shutdownSignal.aborted)
|
|
370
|
-
return;
|
|
371
|
-
const detected = await detectHealthyPrimary(primaryPort, { retries: 1, fetch: deps.fetch });
|
|
372
|
-
if (detected != null) {
|
|
373
|
-
console.error('[Bridge] Primary found after waiting — resuming normal operation');
|
|
374
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'primary_found_after_wait', port: primaryPort });
|
|
375
|
-
setConnectionState({
|
|
376
|
-
kind: 'reconnecting',
|
|
377
|
-
attempt: 0,
|
|
378
|
-
maxAttempts: config.reconnectMaxAttempts,
|
|
379
|
-
respawnBudget: config.maxRespawnAttempts,
|
|
380
|
-
});
|
|
381
|
-
startReconnectLoop();
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
})().catch((err) => {
|
|
386
|
-
const errObj = err instanceof Error ? err : new Error(String(err));
|
|
387
|
-
(0, bridge_events_js_1.logBridgeEvent)({
|
|
388
|
-
kind: 'reconnect_loop_error',
|
|
389
|
-
message: `wait loop error: ${errObj.message}`,
|
|
390
|
-
stack: errObj.stack ?? null,
|
|
391
|
-
});
|
|
392
|
-
console.error('[Bridge] Unexpected error in wait loop:', err);
|
|
393
|
-
});
|
|
394
|
-
};
|
|
395
|
-
stdioTransport.onmessage = (msg) => {
|
|
396
|
-
const state = connectionState;
|
|
397
|
-
switch (state.kind) {
|
|
398
|
-
case 'connected': {
|
|
399
|
-
const timer = setTimeout(() => {
|
|
400
|
-
console.error('[Bridge] Warning: no response from primary after', config.forwardTimeoutMs, 'ms');
|
|
401
|
-
}, config.forwardTimeoutMs);
|
|
402
|
-
void state.transport
|
|
403
|
-
.send(msg)
|
|
404
|
-
.catch((err) => console.error('[Bridge] Forward to primary failed:', err))
|
|
405
|
-
.finally(() => clearTimeout(timer));
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
case 'connecting':
|
|
409
|
-
case 'reconnecting':
|
|
410
|
-
case 'waiting_for_primary':
|
|
411
|
-
sendUnavailableError(msg, (m) => stdioTransport.send(m));
|
|
412
|
-
return;
|
|
413
|
-
case 'closed':
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
stdioTransport.onerror = (err) => console.error('[Bridge] Stdio error:', err);
|
|
418
|
-
const initialTransport = await buildConnectedTransport();
|
|
419
|
-
if (initialTransport == null) {
|
|
420
|
-
throw new Error(`[Bridge] Failed to connect to primary on port ${primaryPort}`);
|
|
421
|
-
}
|
|
422
|
-
(0, bridge_events_js_1.logBridgeEvent)({ kind: 'connected', primaryPort });
|
|
423
|
-
console.error('[Bridge] Connected to primary');
|
|
424
|
-
process.stdout.on('error', (err) => {
|
|
425
|
-
const code = err.code;
|
|
426
|
-
const reason = code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED'
|
|
427
|
-
? 'stdout pipe broken (client disconnected)'
|
|
428
|
-
: `stdout error: ${String(err)}`;
|
|
429
|
-
performShutdown(reason);
|
|
430
|
-
});
|
|
431
|
-
await stdioTransport.start();
|
|
432
|
-
console.error('[Bridge] WorkRail MCP bridge running on stdio');
|
|
433
|
-
process.stdin.once('end', () => performShutdown('stdin closed'));
|
|
434
|
-
process.once('SIGINT', () => performShutdown('SIGINT'));
|
|
435
|
-
process.once('SIGTERM', () => performShutdown('SIGTERM'));
|
|
436
|
-
process.once('SIGHUP', () => performShutdown('SIGHUP'));
|
|
437
|
-
}
|
|
438
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
439
|
-
function sendUnavailableError(msg, send) {
|
|
440
|
-
if (!('id' in msg) || msg.id == null)
|
|
441
|
-
return;
|
|
442
|
-
void send({
|
|
443
|
-
jsonrpc: '2.0',
|
|
444
|
-
id: msg.id,
|
|
445
|
-
error: {
|
|
446
|
-
code: -32603,
|
|
447
|
-
message: 'WorkRail primary server is temporarily unavailable — reconnecting. ' +
|
|
448
|
-
'Wait a few seconds and retry your tool call. ' +
|
|
449
|
-
'If this persists, tell the user: ' +
|
|
450
|
-
'"WorkRail disconnected. Check the terminal running workrail for the ' +
|
|
451
|
-
'error message, then run /mcp in Claude to reconnect."',
|
|
452
|
-
},
|
|
453
|
-
}).catch(() => undefined);
|
|
454
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export type BridgeEvent = {
|
|
2
|
-
readonly kind: 'started';
|
|
3
|
-
readonly primaryPort: number;
|
|
4
|
-
readonly ppid: number;
|
|
5
|
-
} | {
|
|
6
|
-
readonly kind: 'connected';
|
|
7
|
-
readonly primaryPort: number;
|
|
8
|
-
} | {
|
|
9
|
-
readonly kind: 'disconnected';
|
|
10
|
-
} | {
|
|
11
|
-
readonly kind: 'reconnect_attempt';
|
|
12
|
-
readonly attempt: number;
|
|
13
|
-
readonly maxAttempts: number;
|
|
14
|
-
} | {
|
|
15
|
-
readonly kind: 'reconnected';
|
|
16
|
-
readonly attempt: number;
|
|
17
|
-
} | {
|
|
18
|
-
readonly kind: 'spawn_primary';
|
|
19
|
-
readonly port: number;
|
|
20
|
-
} | {
|
|
21
|
-
readonly kind: 'spawn_skipped';
|
|
22
|
-
readonly reason: string;
|
|
23
|
-
} | {
|
|
24
|
-
readonly kind: 'spawn_lock_acquired';
|
|
25
|
-
readonly port: number;
|
|
26
|
-
} | {
|
|
27
|
-
readonly kind: 'spawn_lock_skipped';
|
|
28
|
-
readonly reason: string;
|
|
29
|
-
} | {
|
|
30
|
-
readonly kind: 'budget_exhausted';
|
|
31
|
-
readonly budgetUsed: number;
|
|
32
|
-
readonly respawnBudget: number;
|
|
33
|
-
} | {
|
|
34
|
-
readonly kind: 'waiting_for_primary';
|
|
35
|
-
readonly port: number;
|
|
36
|
-
} | {
|
|
37
|
-
readonly kind: 'primary_found_after_wait';
|
|
38
|
-
readonly port: number;
|
|
39
|
-
} | {
|
|
40
|
-
readonly kind: 'reconnect_loop_error';
|
|
41
|
-
readonly message: string;
|
|
42
|
-
readonly stack: string | null;
|
|
43
|
-
} | {
|
|
44
|
-
readonly kind: 'shutdown';
|
|
45
|
-
readonly reason: string;
|
|
46
|
-
} | {
|
|
47
|
-
readonly kind: 'orphaned';
|
|
48
|
-
readonly expectedPid: number;
|
|
49
|
-
readonly actualPid: number;
|
|
50
|
-
};
|
|
51
|
-
export declare function logBridgeEvent(event: BridgeEvent): void;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.logBridgeEvent = logBridgeEvent;
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
|
-
const os_1 = require("os");
|
|
6
|
-
const path_1 = require("path");
|
|
7
|
-
const BRIDGE_LOG_PATH = (0, path_1.join)((0, os_1.homedir)(), '.workrail', 'bridge.log');
|
|
8
|
-
const BRIDGE_LOG_MAX_BYTES = 512 * 1024;
|
|
9
|
-
function logBridgeEvent(event) {
|
|
10
|
-
try {
|
|
11
|
-
(0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.workrail'), { recursive: true });
|
|
12
|
-
try {
|
|
13
|
-
const { statSync } = require('fs');
|
|
14
|
-
if (statSync(BRIDGE_LOG_PATH).size > BRIDGE_LOG_MAX_BYTES) {
|
|
15
|
-
const { writeFileSync } = require('fs');
|
|
16
|
-
writeFileSync(BRIDGE_LOG_PATH, '');
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
catch { }
|
|
20
|
-
const entry = { ts: new Date().toISOString(), pid: process.pid, ...event };
|
|
21
|
-
(0, fs_1.appendFileSync)(BRIDGE_LOG_PATH, JSON.stringify(entry) + '\n');
|
|
22
|
-
}
|
|
23
|
-
catch { }
|
|
24
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export interface PrimaryTombstone {
|
|
2
|
-
readonly pid: number;
|
|
3
|
-
readonly port: number;
|
|
4
|
-
readonly diedAt: string;
|
|
5
|
-
}
|
|
6
|
-
export type WriteSyncLike = (path: string, content: string) => void;
|
|
7
|
-
export type ReadSyncLike = (path: string, encoding: 'utf-8') => string;
|
|
8
|
-
export type UnlinkSyncLike = (path: string) => void;
|
|
9
|
-
export type MkdirSyncLike = (path: string, opts: {
|
|
10
|
-
recursive: true;
|
|
11
|
-
}) => void;
|
|
12
|
-
export interface TombstoneDeps {
|
|
13
|
-
readonly writeSync?: WriteSyncLike;
|
|
14
|
-
readonly readSync?: ReadSyncLike;
|
|
15
|
-
readonly unlinkSync?: UnlinkSyncLike;
|
|
16
|
-
readonly mkdirSync?: MkdirSyncLike;
|
|
17
|
-
}
|
|
18
|
-
export declare function tombstonePath(): string;
|
|
19
|
-
export declare function writeTombstone(port: number, pid: number, deps?: TombstoneDeps): void;
|
|
20
|
-
export declare function readTombstone(deps?: TombstoneDeps): PrimaryTombstone | null;
|
|
21
|
-
export declare function clearTombstone(deps?: TombstoneDeps): void;
|