Repository: mastra-ai/mastra
File: workspaces/blaxel/src/sandbox/process-manager.ts (TypeScript)

Code before:
      // Merge default env with per-spawn env
      const mergedEnv = { ...this.env, ...options.env };
      const envs = Object.fromEntries(
        Object.entries(mergedEnv).filter((entry): entry is [string, string] => entry[1] !== undefined),
      );

      // Spawn as background process
      const result = await blaxel.process.exec({
        command,
        waitForCompletion: false,
        workingDir: options.cwd,
        ...(Object.keys(envs).length > 0 && { env: envs }),
        ...(options.timeout && { timeout: Math.ceil(options.timeout / 1000) }),
      });

      const identifier = result.pid;
      const pid = this._nextPid++;
      const handle = new BlaxelProcessHandle(pid, identifier, blaxel, Date.now(), options);

      // Start streaming logs — route to handle's emitters
      const streamControl = blaxel.process.streamLogs(identifier, {
        onStdout: (data: string) => handle.emitStdout(data),
        onStderr: (data: string) => handle.emitStderr(data),
        onError: () => {
          // Stream ends when process is killed or errors out — swallow
        },
      });

      handle.streamControl = streamControl;
      this._tracked.set(pid, handle);
      return handle;
    });
  }

  async list(): Promise<ProcessInfo[]> {
    const result: ProcessInfo[] = [];
    for (const [pid, handle] of this._tracked) {
      result.push({
        pid,
        command: handle.command,
        running: handle.exitCode === undefined,
        exitCode: handle.exitCode,
      });
    }
    return result;
  }

  async get(pid: number): Promise<ProcessHandle | undefined> {
    return this._tracked.get(pid);
  }
}

Review comment (suggestion):
I don't think we should swallow this error. Passing it in emitStderr is probably fine. The user and LLM would then see the error message like process was killed / timed out etc. If we swallow it then that will never appear and nobody will know whats going on.

Code after:
      const envs = Object.fromEntries(
        Object.entries(mergedEnv).filter((entry): entry is [string, string] => entry[1] !== undefined),
      );

      // Spawn as background process
      const result = await blaxel.process.exec({
        command,
        waitForCompletion: false,
        workingDir: options.cwd,
        ...(Object.keys(envs).length > 0 && { env: envs }),
        ...(options.timeout && { timeout: Math.ceil(options.timeout / 1000) }),
      });

      // Blaxel PIDs are numeric strings (e.g. "412") — parse to number for ProcessHandle.
      // Keep the original string as _identifier for SDK calls (streamLogs, get, kill).
      const identifier = result.pid;
      const pid = parseInt(identifier, 10);
      const handle = new BlaxelProcessHandle(pid, identifier, blaxel, Date.now(), options);

      // Start streaming logs — route to handle's emitters
      const streamControl = blaxel.process.streamLogs(identifier, {
        onStdout: (data: string) => handle.emitStdout(data),
        onStderr: (data: string) => handle.emitStderr(data),
        onError: (err: Error | string) => {
          const msg = err instanceof Error ? err.message : String(err);
          handle.emitStderr(msg);
        },
      });

      handle.streamControl = streamControl;
      this._tracked.set(pid, handle);
      return handle;
    });
  }

  async list(): Promise<ProcessInfo[]> {
    const result: ProcessInfo[] = [];
    for (const [pid, handle] of this._tracked) {
      result.push({
        pid,
        command: handle.command,
        running: handle.exitCode === undefined,
        exitCode: handle.exitCode,
      });
    }
    return result;
  }

  async get(pid: number): Promise<ProcessHandle | undefined> {
    return this._tracked.get(pid);
  }