@melaya/runner 1.0.19 → 1.0.20

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.
@@ -42,7 +42,7 @@ export async function connect(opts) {
42
42
  socket.emit("runner:models", opts.models);
43
43
  // Start local event relay
44
44
  if (!relay) {
45
- relay = await startLocalRelay(socket, opts.verbose);
45
+ relay = await startLocalRelay(socket, opts.verbose, opts.serverUrl);
46
46
  if (opts.verbose) {
47
47
  console.log(chalk.gray(` [relay] Listening on 127.0.0.1:${relay.port} (nonce: ${relay.nonce})`));
48
48
  }
@@ -16,4 +16,4 @@ export interface LocalRelay {
16
16
  close: () => void;
17
17
  }
18
18
  export declare function setActiveProject(project: string): void;
19
- export declare function startLocalRelay(socket: Socket, verbose: boolean): Promise<LocalRelay>;
19
+ export declare function startLocalRelay(socket: Socket, verbose: boolean, serverUrl?: string): Promise<LocalRelay>;
@@ -55,11 +55,56 @@ function _flushThrottle(socket, verbose) {
55
55
  _throttleQueue = [];
56
56
  _throttleTimer = null;
57
57
  }
58
- export function startLocalRelay(socket, verbose) {
58
+ export function startLocalRelay(socket, verbose, serverUrl) {
59
59
  const nonce = crypto.randomUUID().replace(/-/g, "");
60
60
  return new Promise((resolve, reject) => {
61
61
  const expectedPath = `/events/relay/${nonce}`;
62
+ // Strip trailing slash and convert ws:// → http:// for the upstream
63
+ // poll proxy below. The cloud Node API is the source of truth for
64
+ // approval decisions; the runner just forwards.
65
+ const upstream = (serverUrl || "https://api.melaya.org")
66
+ .replace(/^wss:\/\//, "https://")
67
+ .replace(/^ws:\/\//, "http://")
68
+ .replace(/\/+$/, "");
62
69
  const server = http.createServer((req, res) => {
70
+ // ── HITL approval-poll proxy ────────────────────────────────────
71
+ // The python middleware on the agent subprocess polls
72
+ // `GET ${MEL_BUILDER_URL}/pipelines/<name>/approvals/<request_id>`
73
+ // to learn when the operator approved a tool call. MEL_BUILDER_URL
74
+ // is wired to this local relay (so per-token POSTs land here). We
75
+ // forward the GET to the cloud Node API's `hitl.pollApproval` tRPC
76
+ // endpoint (event-secret-gated upstream — we attach the secret
77
+ // server-side). Without this proxy the GET 404s here, the python
78
+ // poll loop never breaks, the run stays stuck after approval, and
79
+ // the operator sees "approved but nothing happened" — exactly the
80
+ // bug observed on run 1fd397d67db24b9a (May 2026).
81
+ const m = req.method === "GET" && req.url
82
+ ? req.url.match(/^\/pipelines\/[^/]+\/approvals\/([A-Za-z0-9_-]{1,200})\/?$/)
83
+ : null;
84
+ if (m) {
85
+ const requestId = m[1];
86
+ // Forward via socket.io rather than direct HTTP — the runner is
87
+ // already authenticated to the cloud server, so we use it as
88
+ // the trust anchor. Server-side this is `runner:pollApproval`,
89
+ // which calls _validateEventSecret-equivalent before the DB read.
90
+ const replyOnce = (status, body) => {
91
+ if (res.headersSent)
92
+ return;
93
+ res.writeHead(status, { "Content-Type": "application/json" });
94
+ res.end(JSON.stringify(body));
95
+ };
96
+ const t = setTimeout(() => replyOnce(504, { status: "pending", error: "upstream_timeout" }), 6000);
97
+ socket.emit("runner:pollApproval", { requestId }, (resp) => {
98
+ clearTimeout(t);
99
+ if (resp && typeof resp === "object") {
100
+ replyOnce(200, resp);
101
+ }
102
+ else {
103
+ replyOnce(502, { status: "pending", error: "no_response" });
104
+ }
105
+ });
106
+ return;
107
+ }
63
108
  if (req.method !== "POST" || req.url !== expectedPath) {
64
109
  res.writeHead(404);
65
110
  res.end("Not found");
@@ -94,6 +139,10 @@ export function startLocalRelay(socket, verbose) {
94
139
  res.end("Invalid JSON");
95
140
  }
96
141
  });
142
+ // Suppress: `serverUrl` only consumed once at relay start to build
143
+ // the upstream URL — keep the variable referenced so TS doesn't
144
+ // flag it as unused if the GET proxy is removed in the future.
145
+ void upstream;
97
146
  });
98
147
  server.listen(0, "127.0.0.1", () => {
99
148
  const addr = server.address();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,