meshy-node 0.3.8 → 0.4.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/main.cjs CHANGED
@@ -33297,6 +33297,7 @@ function createNodeMessage(kind, payload, options = {}) {
33297
33297
  }
33298
33298
 
33299
33299
  // ../../packages/core/src/messaging/node-message-client.ts
33300
+ var RETRYABLE_NODE_MESSAGE_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
33300
33301
  var NodeMessageClient = class {
33301
33302
  constructor(options = {}) {
33302
33303
  this.options = options;
@@ -33335,6 +33336,16 @@ var NodeMessageClient = class {
33335
33336
  throw error;
33336
33337
  }
33337
33338
  if (!response.ok) {
33339
+ if (RETRYABLE_NODE_MESSAGE_STATUSES.has(response.status) && this.options.heartbeat?.enqueueNodeMessage) {
33340
+ this.log?.warn("direct node message delivery returned retryable status; queued for keepalive", {
33341
+ nodeId: node.id,
33342
+ messageId: message.id,
33343
+ kind: message.kind,
33344
+ statusCode: response.status
33345
+ });
33346
+ this.options.heartbeat.enqueueNodeMessage(node.id, message);
33347
+ return { queued: true };
33348
+ }
33338
33349
  throw new Error(await formatHttpError(response));
33339
33350
  }
33340
33351
  return { queued: false };
@@ -34558,6 +34569,10 @@ var NODE_KIND_BY_LEGACY = {
34558
34569
  "node-workdir-tree": "node.workdir.tree",
34559
34570
  "node-workdir-branch-info": "node.workdir.branch-info",
34560
34571
  "node-workdir-branch-create": "node.workdir.branch-create",
34572
+ "node-terminal-session-start": "node.terminal.session.start",
34573
+ "node-terminal-session-list": "node.terminal.session.list",
34574
+ "node-terminal-session-get": "node.terminal.session.get",
34575
+ "node-terminal-session-stop": "node.terminal.session.stop",
34561
34576
  "node-sessions-list": "node.sessions.list",
34562
34577
  devtunnel: "node.transport.set",
34563
34578
  "node-agent-upgrade": "node.agent.upgrade",
@@ -43063,6 +43078,7 @@ var NativeSessionSummarySchema = external_exports.object({
43063
43078
  summary: external_exports.string(),
43064
43079
  updatedAt: external_exports.string().nullable()
43065
43080
  });
43081
+ var NodeTerminalSessionStatusSchema = external_exports.enum(["running", "exited", "failed", "stopped"]);
43066
43082
  var NodeListQuery = external_exports.object({
43067
43083
  status: external_exports.enum(["online", "busy", "offline"]).optional(),
43068
43084
  capability: external_exports.string().optional()
@@ -43096,7 +43112,7 @@ var NodeNativeSessionsQuery = external_exports.object({
43096
43112
  agent: external_exports.enum(["codex", "claudecode"]),
43097
43113
  limit: external_exports.coerce.number().int().min(1).max(100).default(50)
43098
43114
  });
43099
- var NodeTerminalExecuteBody = external_exports.object({
43115
+ var NodeTerminalSessionStartBody = external_exports.object({
43100
43116
  command: external_exports.string().trim().min(1),
43101
43117
  cwd: external_exports.string().trim().min(1).optional()
43102
43118
  });
@@ -43141,18 +43157,28 @@ var NodeNativeSessionsResponse = external_exports.object({
43141
43157
  agent: external_exports.enum(["codex", "claudecode"]),
43142
43158
  sessions: external_exports.array(NativeSessionSummarySchema)
43143
43159
  });
43144
- var NodeTerminalExecuteResponse = external_exports.object({
43160
+ var NodeTerminalSessionResponse = external_exports.object({
43161
+ id: external_exports.string(),
43145
43162
  nodeId: external_exports.string(),
43146
43163
  cwd: external_exports.string(),
43147
43164
  command: external_exports.string(),
43165
+ pid: external_exports.number().int().nullable(),
43166
+ status: NodeTerminalSessionStatusSchema,
43148
43167
  exitCode: external_exports.number().int().nullable(),
43168
+ signal: external_exports.string().nullable(),
43149
43169
  stdout: external_exports.string(),
43150
43170
  stderr: external_exports.string(),
43171
+ startedAt: external_exports.number(),
43172
+ updatedAt: external_exports.number(),
43173
+ completedAt: external_exports.number().optional(),
43151
43174
  durationMs: external_exports.number().int().min(0),
43152
- timedOut: external_exports.boolean(),
43153
43175
  stdoutTruncated: external_exports.boolean(),
43154
43176
  stderrTruncated: external_exports.boolean()
43155
43177
  });
43178
+ var NodeTerminalSessionListResponse = external_exports.object({
43179
+ nodeId: external_exports.string(),
43180
+ sessions: external_exports.array(NodeTerminalSessionResponse)
43181
+ });
43156
43182
  var UpdateNodeBody = external_exports.object({
43157
43183
  name: external_exports.string().min(1).optional(),
43158
43184
  capabilities: external_exports.array(external_exports.string()).optional()
@@ -45163,7 +45189,10 @@ var LEGACY_KIND_BY_NODE_MESSAGE = {
45163
45189
  "node.workdir.tree": "node-workdir-tree",
45164
45190
  "node.workdir.branch-info": "node-workdir-branch-info",
45165
45191
  "node.workdir.branch-create": "node-workdir-branch-create",
45166
- "node.terminal.execute": "node-terminal-execute",
45192
+ "node.terminal.session.start": "node-terminal-session-start",
45193
+ "node.terminal.session.list": "node-terminal-session-list",
45194
+ "node.terminal.session.get": "node-terminal-session-get",
45195
+ "node.terminal.session.stop": "node-terminal-session-stop",
45167
45196
  "node.sessions.list": "node-sessions-list",
45168
45197
  "node.transport.set": "devtunnel",
45169
45198
  "node.agent.upgrade": "node-agent-upgrade",
@@ -45179,9 +45208,13 @@ var LEGACY_KIND_BY_NODE_MESSAGE = {
45179
45208
  function canRequestNodeMessage(heartbeat) {
45180
45209
  return !!(heartbeat?.requestNodeMessage || heartbeat?.requestWorkerControl);
45181
45210
  }
45182
- function requestFallbackNodeMessage(heartbeat, nodeId, message) {
45183
- if (heartbeat.requestNodeMessage) return heartbeat.requestNodeMessage(nodeId, message);
45184
- if (heartbeat.requestWorkerControl) return heartbeat.requestWorkerControl(nodeId, toLegacyWorkerControl2(message));
45211
+ function requestFallbackNodeMessage(heartbeat, nodeId, message, timeoutMs) {
45212
+ if (heartbeat.requestNodeMessage) {
45213
+ return timeoutMs === void 0 ? heartbeat.requestNodeMessage(nodeId, message) : heartbeat.requestNodeMessage(nodeId, message, timeoutMs);
45214
+ }
45215
+ if (heartbeat.requestWorkerControl) {
45216
+ return timeoutMs === void 0 ? heartbeat.requestWorkerControl(nodeId, toLegacyWorkerControl2(message)) : heartbeat.requestWorkerControl(nodeId, toLegacyWorkerControl2(message), timeoutMs);
45217
+ }
45185
45218
  throw new Error("Node message fallback is not available");
45186
45219
  }
45187
45220
  function toLegacyWorkerControl2(message) {
@@ -45442,16 +45475,18 @@ async function maybeProxyReadToLeader(req, res, options = {}) {
45442
45475
  }
45443
45476
  }
45444
45477
 
45445
- // ../../packages/api/src/node/node-terminal-service.ts
45478
+ // ../../packages/api/src/node/node-terminal-session-service.ts
45446
45479
  var import_node_child_process10 = require("child_process");
45480
+ var import_node_crypto8 = require("crypto");
45481
+
45482
+ // ../../packages/api/src/node/node-terminal-service.ts
45447
45483
  var fs17 = __toESM(require("fs"), 1);
45448
45484
  var path18 = __toESM(require("path"), 1);
45449
- var DEFAULT_TIMEOUT_MS = 3e4;
45450
45485
  var DEFAULT_OUTPUT_LIMIT_BYTES = 64 * 1024;
45451
45486
  function isAbsolutePath2(value) {
45452
45487
  return path18.isAbsolute(value) || /^[A-Za-z]:[\/]/.test(value);
45453
45488
  }
45454
- function resolveCommandCwd(nodeId, rootPath, cwd) {
45489
+ function resolveNodeTerminalCwd(nodeId, rootPath, cwd) {
45455
45490
  if (!rootPath) {
45456
45491
  throw new MeshyError("VALIDATION_ERROR", `Node ${nodeId} does not expose a working directory`, 400);
45457
45492
  }
@@ -45462,74 +45497,170 @@ function resolveCommandCwd(nodeId, rootPath, cwd) {
45462
45497
  }
45463
45498
  return resolved;
45464
45499
  }
45465
- function captureLimited(chunks, chunk, currentBytes, limitBytes) {
45466
- if (currentBytes >= limitBytes) {
45467
- return currentBytes + chunk.length;
45500
+
45501
+ // ../../packages/api/src/node/node-terminal-session-service.ts
45502
+ var MAX_COMPLETED_SESSIONS = 10;
45503
+ function createOutputBuffer() {
45504
+ return { chunks: [], bytes: 0, observedBytes: 0 };
45505
+ }
45506
+ function appendTail(buffer, chunk, limitBytes) {
45507
+ buffer.observedBytes += chunk.length;
45508
+ const nextChunk = chunk.length > limitBytes ? chunk.subarray(chunk.length - limitBytes) : chunk;
45509
+ buffer.chunks.push(nextChunk);
45510
+ buffer.bytes += nextChunk.length;
45511
+ while (buffer.bytes > limitBytes && buffer.chunks.length > 0) {
45512
+ const first = buffer.chunks[0];
45513
+ if (!first) break;
45514
+ const overflow = buffer.bytes - limitBytes;
45515
+ if (first.length <= overflow) {
45516
+ buffer.chunks.shift();
45517
+ buffer.bytes -= first.length;
45518
+ continue;
45519
+ }
45520
+ buffer.chunks[0] = first.subarray(overflow);
45521
+ buffer.bytes -= overflow;
45468
45522
  }
45469
- const remaining = limitBytes - currentBytes;
45470
- chunks.push(chunk.length <= remaining ? chunk : chunk.subarray(0, remaining));
45471
- return currentBytes + chunk.length;
45472
45523
  }
45473
- function toCapturedOutput(chunks, observedBytes, limitBytes) {
45524
+ function readBuffer(buffer, limitBytes) {
45474
45525
  return {
45475
- text: Buffer.concat(chunks).toString("utf8"),
45476
- truncated: observedBytes > limitBytes
45526
+ text: Buffer.concat(buffer.chunks, buffer.bytes).toString("utf8"),
45527
+ truncated: buffer.observedBytes > limitBytes
45477
45528
  };
45478
45529
  }
45479
- function executeLocalNodeTerminalCommand(nodeId, rootPath, command, options = {}) {
45480
- const normalizedCommand = command.trim();
45481
- if (!normalizedCommand) {
45482
- throw new MeshyError("VALIDATION_ERROR", "Command must not be empty", 400);
45530
+ function cloneSession(session) {
45531
+ return { ...session };
45532
+ }
45533
+ function killSessionProcess(state3, signal) {
45534
+ const child = state3.process;
45535
+ if (!child) return;
45536
+ const pid = child.pid;
45537
+ if (process.platform !== "win32" && typeof pid === "number") {
45538
+ try {
45539
+ process.kill(-pid, signal);
45540
+ return;
45541
+ } catch {
45542
+ }
45483
45543
  }
45484
- const cwd = resolveCommandCwd(nodeId, rootPath, options.cwd);
45485
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
45486
- const outputLimitBytes = options.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
45487
- const startedAt = Date.now();
45488
- const stdoutChunks = [];
45489
- const stderrChunks = [];
45490
- let stdoutBytes = 0;
45491
- let stderrBytes = 0;
45492
- let timedOut = false;
45493
- return new Promise((resolve15, reject) => {
45544
+ try {
45545
+ child.kill(signal);
45546
+ } catch {
45547
+ }
45548
+ }
45549
+ var NodeTerminalSessionService = class {
45550
+ sessions = /* @__PURE__ */ new Map();
45551
+ nextSequence = 0;
45552
+ start(nodeId, rootPath, command, options = {}) {
45553
+ const normalizedCommand = command.trim();
45554
+ if (!normalizedCommand) {
45555
+ throw new MeshyError("VALIDATION_ERROR", "Command must not be empty", 400);
45556
+ }
45557
+ const cwd = resolveNodeTerminalCwd(nodeId, rootPath, options.cwd);
45558
+ const startedAt = Date.now();
45494
45559
  const child = (0, import_node_child_process10.spawn)(normalizedCommand, {
45495
45560
  cwd,
45496
45561
  shell: true,
45497
45562
  windowsHide: true,
45498
- stdio: ["ignore", "pipe", "pipe"]
45563
+ stdio: ["ignore", "pipe", "pipe"],
45564
+ detached: process.platform !== "win32"
45499
45565
  });
45500
- const timeout = setTimeout(() => {
45501
- timedOut = true;
45502
- child.kill("SIGTERM");
45503
- }, timeoutMs);
45566
+ const session = {
45567
+ id: (0, import_node_crypto8.randomUUID)(),
45568
+ nodeId,
45569
+ cwd,
45570
+ command: normalizedCommand,
45571
+ pid: child.pid ?? null,
45572
+ status: "running",
45573
+ exitCode: null,
45574
+ signal: null,
45575
+ stdout: "",
45576
+ stderr: "",
45577
+ startedAt,
45578
+ updatedAt: startedAt,
45579
+ durationMs: 0,
45580
+ stdoutTruncated: false,
45581
+ stderrTruncated: false
45582
+ };
45583
+ const state3 = {
45584
+ session,
45585
+ process: child,
45586
+ stdout: createOutputBuffer(),
45587
+ stderr: createOutputBuffer(),
45588
+ sequence: this.nextSequence++
45589
+ };
45590
+ const outputLimitBytes = options.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES;
45591
+ this.sessions.set(session.id, state3);
45504
45592
  child.stdout?.on("data", (chunk) => {
45505
- stdoutBytes = captureLimited(stdoutChunks, chunk, stdoutBytes, outputLimitBytes);
45593
+ appendTail(state3.stdout, chunk, outputLimitBytes);
45594
+ this.refreshOutput(state3, outputLimitBytes);
45506
45595
  });
45507
45596
  child.stderr?.on("data", (chunk) => {
45508
- stderrBytes = captureLimited(stderrChunks, chunk, stderrBytes, outputLimitBytes);
45597
+ if (state3.session.status === "stopped") return;
45598
+ appendTail(state3.stderr, chunk, outputLimitBytes);
45599
+ this.refreshOutput(state3, outputLimitBytes);
45509
45600
  });
45510
45601
  child.on("error", (err) => {
45511
- clearTimeout(timeout);
45512
- reject(new MeshyError("VALIDATION_ERROR", err.message, 400));
45513
- });
45514
- child.on("close", (code) => {
45515
- clearTimeout(timeout);
45516
- const stdout = toCapturedOutput(stdoutChunks, stdoutBytes, outputLimitBytes);
45517
- const stderr = toCapturedOutput(stderrChunks, stderrBytes, outputLimitBytes);
45518
- resolve15({
45519
- nodeId,
45520
- cwd,
45521
- command: normalizedCommand,
45522
- exitCode: code,
45523
- stdout: stdout.text,
45524
- stderr: stderr.text,
45525
- durationMs: Date.now() - startedAt,
45526
- timedOut,
45527
- stdoutTruncated: stdout.truncated,
45528
- stderrTruncated: stderr.truncated
45529
- });
45602
+ state3.session.status = "failed";
45603
+ state3.session.stderr = state3.session.stderr ? `${state3.session.stderr}
45604
+ ${err.message}` : err.message;
45605
+ this.complete(state3, null, null, outputLimitBytes);
45530
45606
  });
45531
- });
45532
- }
45607
+ child.on("close", (code, signal) => {
45608
+ if (state3.session.status !== "stopped") {
45609
+ state3.session.status = code === 0 ? "exited" : "failed";
45610
+ }
45611
+ this.complete(state3, code, signal, outputLimitBytes);
45612
+ });
45613
+ return cloneSession(session);
45614
+ }
45615
+ list() {
45616
+ return Array.from(this.sessions.values()).sort((a, b) => b.sequence - a.sequence).map((state3) => cloneSession(state3.session));
45617
+ }
45618
+ get(id) {
45619
+ const state3 = this.sessions.get(id);
45620
+ return state3 ? cloneSession(state3.session) : null;
45621
+ }
45622
+ stop(id) {
45623
+ const state3 = this.sessions.get(id);
45624
+ if (!state3) return null;
45625
+ if (state3.session.status === "running") {
45626
+ state3.session.status = "stopped";
45627
+ state3.session.updatedAt = Date.now();
45628
+ killSessionProcess(state3, "SIGTERM");
45629
+ setTimeout(() => {
45630
+ if (state3.session.status === "stopped" && !state3.session.completedAt) {
45631
+ killSessionProcess(state3, "SIGKILL");
45632
+ }
45633
+ }, 2e3).unref?.();
45634
+ }
45635
+ return cloneSession(state3.session);
45636
+ }
45637
+ refreshOutput(state3, outputLimitBytes) {
45638
+ const stdout = readBuffer(state3.stdout, outputLimitBytes);
45639
+ const stderr = readBuffer(state3.stderr, outputLimitBytes);
45640
+ state3.session.stdout = stdout.text;
45641
+ state3.session.stderr = stderr.text;
45642
+ state3.session.stdoutTruncated = stdout.truncated;
45643
+ state3.session.stderrTruncated = stderr.truncated;
45644
+ state3.session.updatedAt = Date.now();
45645
+ state3.session.durationMs = state3.session.updatedAt - state3.session.startedAt;
45646
+ }
45647
+ complete(state3, exitCode, signal, outputLimitBytes) {
45648
+ this.refreshOutput(state3, outputLimitBytes);
45649
+ state3.session.exitCode = exitCode;
45650
+ state3.session.signal = signal;
45651
+ state3.session.completedAt = Date.now();
45652
+ state3.session.updatedAt = state3.session.completedAt;
45653
+ state3.session.durationMs = state3.session.completedAt - state3.session.startedAt;
45654
+ state3.process = null;
45655
+ this.pruneCompletedSessions();
45656
+ }
45657
+ pruneCompletedSessions() {
45658
+ const completed = Array.from(this.sessions.values()).filter((state3) => state3.session.status !== "running").sort((a, b) => b.sequence - a.sequence);
45659
+ for (const state3 of completed.slice(MAX_COMPLETED_SESSIONS)) {
45660
+ this.sessions.delete(state3.session.id);
45661
+ }
45662
+ }
45663
+ };
45533
45664
 
45534
45665
  // ../../packages/api/src/node/node-native-session-service.ts
45535
45666
  function getLocalNodeNativeSessions(nodeId, agent, limit) {
@@ -46615,6 +46746,10 @@ function payloadValue(request, key, fallback) {
46615
46746
  const value = request.payload[key];
46616
46747
  return value === void 0 ? fallback : value;
46617
46748
  }
46749
+ function getTerminalSessionService(deps) {
46750
+ deps.terminalSessionService ??= new NodeTerminalSessionService();
46751
+ return deps.terminalSessionService;
46752
+ }
46618
46753
  function maybeImportTaskSnapshot(deps, request) {
46619
46754
  if (!request.kind.startsWith("task.output.") && request.kind !== "task.preview.create" && request.kind !== "task.logs") return;
46620
46755
  const task = payloadValue(request, "task", null);
@@ -46692,12 +46827,12 @@ async function executeWorkerControlRequest(deps, request) {
46692
46827
  );
46693
46828
  break;
46694
46829
  }
46695
- case "node.terminal.execute": {
46830
+ case "node.terminal.session.start": {
46696
46831
  const self2 = deps.nodeRegistry.getSelf();
46697
46832
  response = jsonResponse(
46698
46833
  request.id,
46699
- 200,
46700
- await executeLocalNodeTerminalCommand(
46834
+ 202,
46835
+ getTerminalSessionService(deps).start(
46701
46836
  self2.id,
46702
46837
  self2.workDir ?? deps.workDir,
46703
46838
  payloadValue(request, "command", ""),
@@ -46706,6 +46841,28 @@ async function executeWorkerControlRequest(deps, request) {
46706
46841
  );
46707
46842
  break;
46708
46843
  }
46844
+ case "node.terminal.session.list": {
46845
+ const self2 = deps.nodeRegistry.getSelf();
46846
+ response = jsonResponse(request.id, 200, {
46847
+ nodeId: self2.id,
46848
+ sessions: getTerminalSessionService(deps).list()
46849
+ });
46850
+ break;
46851
+ }
46852
+ case "node.terminal.session.get": {
46853
+ const sessionId = payloadValue(request, "sessionId", "");
46854
+ const session = getTerminalSessionService(deps).get(sessionId);
46855
+ if (!session) throw new MeshyError("NODE_NOT_FOUND", `Terminal session ${sessionId} not found`, 404);
46856
+ response = jsonResponse(request.id, 200, session);
46857
+ break;
46858
+ }
46859
+ case "node.terminal.session.stop": {
46860
+ const sessionId = payloadValue(request, "sessionId", "");
46861
+ const session = getTerminalSessionService(deps).stop(sessionId);
46862
+ if (!session) throw new MeshyError("NODE_NOT_FOUND", `Terminal session ${sessionId} not found`, 404);
46863
+ response = jsonResponse(request.id, 200, session);
46864
+ break;
46865
+ }
46709
46866
  case "node.sessions.list": {
46710
46867
  const self2 = deps.nodeRegistry.getSelf();
46711
46868
  response = jsonResponse(
@@ -46954,142 +47111,89 @@ async function sendNodeAgentUpgrade(req, res, nodeId, agentParam) {
46954
47111
  }
46955
47112
 
46956
47113
  // ../../packages/api/src/routes/node-terminal.ts
46957
- var NODE_TERMINAL_PROXY_TIMEOUT_MS = 35e3;
46958
- function describeProxyError2(error) {
46959
- if (error instanceof Error) {
46960
- const errorCategory = error.name === "AbortError" || /aborted/i.test(error.message) ? "abort" : /timeout/i.test(error.message) ? "timeout" : "network";
46961
- return {
46962
- errorName: error.name,
46963
- errorMessage: error.message,
46964
- errorCategory
46965
- };
46966
- }
46967
- return {
46968
- errorName: "UnknownError",
46969
- errorMessage: String(error),
46970
- errorCategory: "unknown"
46971
- };
47114
+ var TERMINAL_SESSION_PROXY_TIMEOUT_MS = 1e4;
47115
+ function getTerminalSessionService2(deps) {
47116
+ deps.terminalSessionService ??= new NodeTerminalSessionService();
47117
+ return deps.terminalSessionService;
46972
47118
  }
46973
- function createNodeTerminalProxyTrace(log2, nodeId, proxyPath) {
46974
- return {
46975
- onAttempt: ({ attempt, endpoint, timeoutMs, totalEndpoints }) => {
46976
- log2.debug("node terminal proxy attempt", {
46977
- nodeId,
46978
- proxyPath,
46979
- endpoint,
46980
- attempt,
46981
- totalEndpoints,
46982
- timeoutMs
46983
- });
46984
- },
46985
- onResponse: ({ attempt, endpoint, response, timeoutMs, totalEndpoints }) => {
46986
- log2.info("node terminal proxy response", {
46987
- nodeId,
46988
- proxyPath,
46989
- endpoint,
46990
- attempt,
46991
- totalEndpoints,
46992
- timeoutMs,
46993
- ok: response.ok,
46994
- statusCode: response.status
46995
- });
46996
- },
46997
- onError: ({ attempt, endpoint, error, timeoutMs, totalEndpoints }) => {
46998
- log2.warn("node terminal proxy attempt failed", {
46999
- nodeId,
47000
- proxyPath,
47001
- endpoint,
47002
- attempt,
47003
- totalEndpoints,
47004
- timeoutMs,
47005
- ...describeProxyError2(error)
47006
- });
47119
+ async function sendRemoteTerminalSessionRequest(req, res, nodeId, message) {
47120
+ const deps = req.app.locals.deps;
47121
+ const node = deps.nodeRegistry.getNode(nodeId);
47122
+ if (!node) throw new MeshyError("NODE_NOT_FOUND", `Node ${nodeId} not found`, 404);
47123
+ const heartbeat = deps.heartbeat;
47124
+ const canPushToNode = heartbeat.canPushToNode?.(nodeId) ?? true;
47125
+ let response;
47126
+ if (!canPushToNode && canRequestNodeMessage(heartbeat)) {
47127
+ response = await requestFallbackNodeMessage(heartbeat, nodeId, message, TERMINAL_SESSION_PROXY_TIMEOUT_MS);
47128
+ } else {
47129
+ try {
47130
+ response = await new NodeMessageClient({
47131
+ logger: deps.logger,
47132
+ timeoutMs: TERMINAL_SESSION_PROXY_TIMEOUT_MS
47133
+ }).request(node, message);
47134
+ } catch (err) {
47135
+ if (canRequestNodeMessage(heartbeat)) {
47136
+ response = await requestFallbackNodeMessage(heartbeat, nodeId, message, TERMINAL_SESSION_PROXY_TIMEOUT_MS);
47137
+ } else {
47138
+ throw new MeshyError("NODE_OFFLINE", `Cannot reach node ${nodeId} for terminal session control`, 502, {
47139
+ error: err instanceof Error ? err.message : String(err)
47140
+ });
47141
+ }
47007
47142
  }
47008
- };
47009
- }
47010
- async function maybeHandleRemoteNodeTerminalExecuteRequest(req, res, nodeId) {
47011
- const body = NodeTerminalExecuteBody.parse(req.body);
47012
- const { nodeRegistry, heartbeat, logger: rootLogger } = req.app.locals.deps;
47013
- const selfId = nodeRegistry.getSelf().id;
47014
- if (nodeId === selfId) {
47015
- return false;
47016
47143
  }
47017
- const node = nodeRegistry.getNode(nodeId);
47018
- if (!node) {
47019
- throw new MeshyError("NODE_NOT_FOUND", `Node ${nodeId} not found`, 404);
47144
+ sendWorkerControlResponse(res, response);
47145
+ }
47146
+ async function sendNodeTerminalSessionStart(req, res, nodeId) {
47147
+ const body = NodeTerminalSessionStartBody.parse(req.body);
47148
+ const deps = req.app.locals.deps;
47149
+ const self2 = deps.nodeRegistry.getSelf();
47150
+ if (nodeId !== self2.id) {
47151
+ await sendRemoteTerminalSessionRequest(req, res, nodeId, createNodeMessage("node.terminal.session.start", {
47152
+ command: body.command,
47153
+ cwd: body.cwd
47154
+ }, { expectsResponse: true }));
47155
+ return;
47020
47156
  }
47021
- const log2 = rootLogger.child("nodes/terminal");
47022
- const proxyPath = req.originalUrl ?? `/api/nodes/${nodeId}/terminal/execute`;
47023
- const fallbackRequest = createNodeMessage("node.terminal.execute", {
47024
- command: body.command,
47025
- cwd: body.cwd
47026
- }, { expectsResponse: true });
47027
- const canPushToNode = heartbeat?.canPushToNode?.(nodeId) ?? true;
47028
- if (!canPushToNode && canRequestNodeMessage(heartbeat)) {
47029
- log2.warn("node terminal request falling back to keepalive control", { nodeId, proxyPath });
47030
- const controlResponse = await requestFallbackNodeMessage(heartbeat, nodeId, fallbackRequest);
47031
- sendWorkerControlResponse(res, controlResponse);
47032
- return true;
47157
+ const session = getTerminalSessionService2(deps).start(self2.id, self2.workDir ?? deps.workDir, body.command, { cwd: body.cwd });
47158
+ res.status(202).json(session);
47159
+ }
47160
+ async function sendNodeTerminalSessionList(req, res, nodeId) {
47161
+ const deps = req.app.locals.deps;
47162
+ const self2 = deps.nodeRegistry.getSelf();
47163
+ if (nodeId !== self2.id) {
47164
+ await sendRemoteTerminalSessionRequest(req, res, nodeId, createNodeMessage("node.terminal.session.list", {}, { expectsResponse: true }));
47165
+ return;
47033
47166
  }
47034
- try {
47035
- const { endpoint, response } = await fetchNodeWithFallback(
47036
- node,
47037
- proxyPath,
47038
- {
47039
- method: "POST",
47040
- headers: { "Content-Type": "application/json" },
47041
- body: JSON.stringify(body)
47042
- },
47043
- NODE_TERMINAL_PROXY_TIMEOUT_MS,
47044
- createNodeTerminalProxyTrace(log2, nodeId, proxyPath),
47045
- { preferPublicEndpoint: true }
47046
- );
47047
- log2.debug("proxying node terminal request", { nodeId, endpoint, proxyPath });
47048
- await sendProxyResponse(res, response);
47049
- return true;
47050
- } catch (err) {
47051
- const errorDetails = describeProxyError2(err);
47052
- log2.warn("node terminal proxy error", {
47053
- nodeId,
47054
- proxyPath,
47055
- timeoutMs: NODE_TERMINAL_PROXY_TIMEOUT_MS,
47056
- ...errorDetails
47057
- });
47058
- if (canRequestNodeMessage(heartbeat)) {
47059
- log2.warn("node terminal proxy failed, falling back to keepalive control", {
47060
- nodeId,
47061
- proxyPath,
47062
- timeoutMs: NODE_TERMINAL_PROXY_TIMEOUT_MS,
47063
- ...errorDetails
47064
- });
47065
- const controlResponse = await requestFallbackNodeMessage(heartbeat, nodeId, fallbackRequest);
47066
- sendWorkerControlResponse(res, controlResponse);
47067
- return true;
47068
- }
47069
- throw new MeshyError("NODE_OFFLINE", `Cannot reach node ${nodeId} to execute a terminal command`, 502);
47167
+ res.json({ nodeId: self2.id, sessions: getTerminalSessionService2(deps).list() });
47168
+ }
47169
+ async function sendNodeTerminalSessionGet(req, res, nodeId, sessionId) {
47170
+ const deps = req.app.locals.deps;
47171
+ const self2 = deps.nodeRegistry.getSelf();
47172
+ if (nodeId !== self2.id) {
47173
+ await sendRemoteTerminalSessionRequest(req, res, nodeId, createNodeMessage("node.terminal.session.get", { sessionId }, { expectsResponse: true }));
47174
+ return;
47070
47175
  }
47176
+ const session = getTerminalSessionService2(deps).get(sessionId);
47177
+ if (!session) throw new MeshyError("NODE_NOT_FOUND", `Terminal session ${sessionId} not found`, 404);
47178
+ res.json(session);
47071
47179
  }
47072
- async function sendLocalNodeTerminalExecute(req, res, nodeId) {
47073
- const body = NodeTerminalExecuteBody.parse(req.body);
47074
- const { nodeRegistry, workDir } = req.app.locals.deps;
47075
- const self2 = nodeRegistry.getSelf();
47180
+ async function sendNodeTerminalSessionStop(req, res, nodeId, sessionId) {
47181
+ const deps = req.app.locals.deps;
47182
+ const self2 = deps.nodeRegistry.getSelf();
47076
47183
  if (nodeId !== self2.id) {
47077
- throw new MeshyError("NODE_NOT_FOUND", `Node ${nodeId} not found`, 404);
47184
+ await sendRemoteTerminalSessionRequest(req, res, nodeId, createNodeMessage("node.terminal.session.stop", { sessionId }, { expectsResponse: true }));
47185
+ return;
47078
47186
  }
47079
- const result = await executeLocalNodeTerminalCommand(
47080
- self2.id,
47081
- self2.workDir ?? workDir,
47082
- body.command,
47083
- { cwd: body.cwd }
47084
- );
47085
- res.json(result);
47187
+ const session = getTerminalSessionService2(deps).stop(sessionId);
47188
+ if (!session) throw new MeshyError("NODE_NOT_FOUND", `Terminal session ${sessionId} not found`, 404);
47189
+ res.json(session);
47086
47190
  }
47087
47191
 
47088
47192
  // ../../packages/api/src/routes/nodes.ts
47089
47193
  var NODE_WORKDIR_PROXY_TIMEOUT_MS = 1e4;
47090
47194
  var NODE_WORKDIR_BRANCH_PROXY_TIMEOUT_MS = 25e3;
47091
47195
  var NODE_NATIVE_SESSIONS_PROXY_TIMEOUT_MS = 1e4;
47092
- function describeProxyError3(error) {
47196
+ function describeProxyError2(error) {
47093
47197
  if (error instanceof Error) {
47094
47198
  const errorCategory = error.name === "AbortError" || /aborted/i.test(error.message) ? "abort" : /timeout/i.test(error.message) ? "timeout" : "network";
47095
47199
  return {
@@ -47136,7 +47240,7 @@ function createNodeWorkdirProxyTrace(log2, nodeId, proxyPath) {
47136
47240
  attempt,
47137
47241
  totalEndpoints,
47138
47242
  timeoutMs,
47139
- ...describeProxyError3(error)
47243
+ ...describeProxyError2(error)
47140
47244
  });
47141
47245
  }
47142
47246
  };
@@ -47191,7 +47295,7 @@ async function maybeHandleRemoteNodeWorkDirRequest(req, res, nodeId) {
47191
47295
  await sendProxyResponse(res, response);
47192
47296
  return true;
47193
47297
  } catch (err) {
47194
- const errorDetails = describeProxyError3(err);
47298
+ const errorDetails = describeProxyError2(err);
47195
47299
  log2.warn("node workdir proxy error", {
47196
47300
  nodeId,
47197
47301
  proxyPath,
@@ -47258,7 +47362,7 @@ async function maybeHandleRemoteNodeWorkDirBranchInfoRequest(req, res, nodeId) {
47258
47362
  await sendProxyResponse(res, response);
47259
47363
  return true;
47260
47364
  } catch (err) {
47261
- const errorDetails = describeProxyError3(err);
47365
+ const errorDetails = describeProxyError2(err);
47262
47366
  log2.warn("node workdir branch proxy error", {
47263
47367
  nodeId,
47264
47368
  proxyPath,
@@ -47323,7 +47427,7 @@ async function maybeHandleRemoteNodeNativeSessionsRequest(req, res, nodeId) {
47323
47427
  await sendProxyResponse(res, response);
47324
47428
  return true;
47325
47429
  } catch (err) {
47326
- const errorDetails = describeProxyError3(err);
47430
+ const errorDetails = describeProxyError2(err);
47327
47431
  log2.warn("node native sessions proxy error", {
47328
47432
  nodeId,
47329
47433
  proxyPath,
@@ -47410,13 +47514,17 @@ function createNodeRoutes() {
47410
47514
  }
47411
47515
  sendLocalNodeWorkDirBranchInfo(req, res, nodeId);
47412
47516
  }));
47413
- router.post("/:id/terminal/execute", asyncHandler3(async (req, res) => {
47414
- const nodeId = req.params.id;
47415
- const handled = await maybeHandleRemoteNodeTerminalExecuteRequest(req, res, nodeId);
47416
- if (handled) {
47417
- return;
47418
- }
47419
- await sendLocalNodeTerminalExecute(req, res, nodeId);
47517
+ router.get("/:id/terminal/sessions", asyncHandler3(async (req, res) => {
47518
+ await sendNodeTerminalSessionList(req, res, req.params.id);
47519
+ }));
47520
+ router.post("/:id/terminal/sessions", asyncHandler3(async (req, res) => {
47521
+ await sendNodeTerminalSessionStart(req, res, req.params.id);
47522
+ }));
47523
+ router.get("/:id/terminal/sessions/:sessionId", asyncHandler3(async (req, res) => {
47524
+ await sendNodeTerminalSessionGet(req, res, req.params.id, req.params.sessionId);
47525
+ }));
47526
+ router.post("/:id/terminal/sessions/:sessionId/stop", asyncHandler3(async (req, res) => {
47527
+ await sendNodeTerminalSessionStop(req, res, req.params.id, req.params.sessionId);
47420
47528
  }));
47421
47529
  router.post("/:id/workdir/branch", asyncHandler3(async (req, res) => {
47422
47530
  const nodeId = req.params.id;
@@ -47837,7 +47945,7 @@ function getTaskLogsProxyRequestMetadata(req) {
47837
47945
  const sourceNodeId = req.get(TASK_LOG_PROXY_SOURCE_HEADER) ?? void 0;
47838
47946
  return { isProxy: purpose === TASK_LOG_PROXY_PURPOSE, sourceNodeId, task: decodeTaskSnapshot(req.get(TASK_LOG_PROXY_TASK_HEADER)) };
47839
47947
  }
47840
- function describeProxyError4(error) {
47948
+ function describeProxyError3(error) {
47841
47949
  if (error instanceof Error) {
47842
47950
  const errorCategory = error.name === "AbortError" || /aborted/i.test(error.message) ? "abort" : /timeout/i.test(error.message) ? "timeout" : "network";
47843
47951
  return {
@@ -47879,7 +47987,7 @@ function createTaskLogsProxyTrace(log2, taskId, nodeId, proxyPath) {
47879
47987
  attempt,
47880
47988
  totalEndpoints,
47881
47989
  timeoutMs,
47882
- ...describeProxyError4(error)
47990
+ ...describeProxyError3(error)
47883
47991
  });
47884
47992
  }
47885
47993
  };
@@ -47957,7 +48065,7 @@ async function sendTaskLogsResponse(req, res, taskId) {
47957
48065
  log2.warn("failed to seed task snapshot before task logs proxy", {
47958
48066
  taskId,
47959
48067
  assignedTo,
47960
- ...describeProxyError4(err)
48068
+ ...describeProxyError3(err)
47961
48069
  });
47962
48070
  return [];
47963
48071
  });
@@ -47990,7 +48098,7 @@ async function sendTaskLogsResponse(req, res, taskId) {
47990
48098
  taskId,
47991
48099
  assignedTo,
47992
48100
  timeoutMs: TASK_LOG_PROXY_TIMEOUT_MS,
47993
- ...describeProxyError4(err)
48101
+ ...describeProxyError3(err)
47994
48102
  });
47995
48103
  const fallback = await requestTaskLogsOverKeepalive(heartbeat, assignedTo, task, after);
47996
48104
  if (fallback) {
@@ -48067,7 +48175,7 @@ function resolveRequestOrigin(req) {
48067
48175
 
48068
48176
  // ../../packages/api/src/routes/task-output.ts
48069
48177
  var TASK_OUTPUT_PROXY_TIMEOUT_MS = 1e4;
48070
- function describeProxyError5(error) {
48178
+ function describeProxyError4(error) {
48071
48179
  if (error instanceof Error) {
48072
48180
  const errorCategory = error.name === "AbortError" || /aborted/i.test(error.message) ? "abort" : /timeout/i.test(error.message) ? "timeout" : "network";
48073
48181
  return {
@@ -48120,7 +48228,7 @@ function createTaskOutputProxyTrace(log2, taskId, assignedTo, kind, proxyPath) {
48120
48228
  attempt,
48121
48229
  totalEndpoints,
48122
48230
  timeoutMs,
48123
- ...describeProxyError5(error)
48231
+ ...describeProxyError4(error)
48124
48232
  });
48125
48233
  }
48126
48234
  };
@@ -48238,7 +48346,7 @@ async function maybeHandleRemoteTaskOutputRequest(req, res, taskId, subPath, ini
48238
48346
  assignedTo: assignedNodeId,
48239
48347
  kind: hydratedFallbackRequest.kind,
48240
48348
  timeoutMs: TASK_OUTPUT_PROXY_TIMEOUT_MS,
48241
- ...describeProxyError5(err)
48349
+ ...describeProxyError4(err)
48242
48350
  });
48243
48351
  const controlResponse = await requestFallbackNodeMessage(heartbeat, assignedNodeId, hydratedFallbackRequest);
48244
48352
  if (hydratedFallbackRequest.kind === "task.preview.create" && controlResponse.bodyEncoding === "json" && isPreviewSessionPayload(controlResponse.body)) {
@@ -48255,7 +48363,7 @@ async function maybeHandleRemoteTaskOutputRequest(req, res, taskId, subPath, ini
48255
48363
  assignedTo: assignedNodeId,
48256
48364
  kind: messageKind,
48257
48365
  timeoutMs: TASK_OUTPUT_PROXY_TIMEOUT_MS,
48258
- ...describeProxyError5(err)
48366
+ ...describeProxyError4(err)
48259
48367
  });
48260
48368
  throw new MeshyError("NODE_OFFLINE", "Cannot reach worker for task output", 502);
48261
48369
  }