@melaya/runner 1.1.47 → 1.1.48

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.
@@ -230,21 +230,47 @@ class _RedactingStdout:
230
230
  def _scrub(self, s: str) -> str:
231
231
  return self._B64.sub(lambda m: f"<redacted base64 {len(m.group(0))} bytes>", s)
232
232
 
233
+ def _flush_transcript(self):
234
+ # Send any buffered agentscope transcript to STDERR (redacted). Keeping it
235
+ # off stdout means the runner's stdout channel carries ONLY `MELASSIST `
236
+ # protocol frames: a transcript print can never glue onto a frame and be
237
+ # dropped (the missing-delta / garbled-fragment bug), and frame parsing on
238
+ # the runner stays cheap — no multi-KB base64 transcript lines flooding its
239
+ # event loop (which was spiking lag and dropping the socket heartbeat).
240
+ if self._buf:
241
+ try:
242
+ sys.stderr.write(self._scrub(self._buf))
243
+ except Exception:
244
+ pass
245
+ self._buf = ""
246
+
233
247
  def write(self, s):
234
248
  try:
249
+ # A protocol frame is emitted in ONE write() call, whole and
250
+ # newline-terminated (see _emit). Route it to the REAL stdout byte for
251
+ # byte (no scrub — frames are short structured JSON), after flushing any
252
+ # pending partial transcript so nothing can glue onto it.
253
+ if s.startswith(_EVENT_PREFIX):
254
+ self._flush_transcript()
255
+ self._real.write(s)
256
+ self._real.flush()
257
+ return len(s)
258
+ # Everything else = agentscope's verbatim message transcript → stderr,
259
+ # line-buffered + base64-redacted, so it never pollutes the protocol.
235
260
  self._buf += s
236
261
  while "\n" in self._buf:
237
262
  line, self._buf = self._buf.split("\n", 1)
238
- self._real.write(self._scrub(line) + "\n")
263
+ try:
264
+ sys.stderr.write(self._scrub(line) + "\n")
265
+ except Exception:
266
+ pass
239
267
  return len(s)
240
268
  except Exception:
241
269
  return self._real.write(s)
242
270
 
243
271
  def flush(self):
244
272
  try:
245
- if self._buf:
246
- self._real.write(self._scrub(self._buf))
247
- self._buf = ""
273
+ self._flush_transcript()
248
274
  self._real.flush()
249
275
  except Exception:
250
276
  pass
@@ -55,6 +55,11 @@ try {
55
55
  }
56
56
  catch { /* dev tree layout differs — non-fatal */ }
57
57
  const activeAssistants = new Map();
58
+ // Line prefix the assistant host uses to frame protocol events on stdout
59
+ // (mirror of assistantHost.py `_EVENT_PREFIX`). Everything else on stdout is
60
+ // raw agentscope transcript (now routed to stderr host-side, but we still parse
61
+ // defensively in case a frame arrives glued to a raw print).
62
+ const _EVENT_PREFIX = "MELASSIST ";
58
63
  // Warm pool (size 1): a fully-booted host held in reserve, ready to be ADOPTED
59
64
  // by the next new session whose config+generation match — skipping the ~7s cold
60
65
  // boot (import + build_agent). Pure optimization: any mismatch or unhealthy
@@ -1325,9 +1330,17 @@ export async function connect(opts) {
1325
1330
  const line = session.stdoutBuf.slice(0, idx);
1326
1331
  session.stdoutBuf = session.stdoutBuf.slice(idx + 1);
1327
1332
  const t = line.trim();
1328
- if (t.startsWith("MELASSIST ")) {
1333
+ // A protocol frame can arrive GLUED to a preceding raw print that had no
1334
+ // trailing newline (agentscope prints the agent transcript to the same
1335
+ // stdout), e.g. `Assistant: GotMELASSIST {"kind":"delta",…}`. Find the
1336
+ // prefix ANYWHERE in the line, not just at the start, or that delta is
1337
+ // dropped as raw output (the missing-deltas / garbled-fragment bug).
1338
+ const mi = t.indexOf(_EVENT_PREFIX);
1339
+ if (mi >= 0) {
1340
+ if (mi > 0 && opts.verbose)
1341
+ console.log(chalk.gray(` [assistant-stdout] ${t.slice(0, mi)}`));
1329
1342
  try {
1330
- const parsed = JSON.parse(t.slice("MELASSIST ".length));
1343
+ const parsed = JSON.parse(t.slice(mi + _EVENT_PREFIX.length));
1331
1344
  if (typeof parsed?.memoryWatermark === "number")
1332
1345
  session.memoryWatermark = parsed.memoryWatermark;
1333
1346
  if (parsed?.kind === "ready") {
@@ -1343,7 +1356,12 @@ export async function connect(opts) {
1343
1356
  if (session.forward)
1344
1357
  socket.emit("runner:assistant_event", { sessionId: session.sid, ...parsed });
1345
1358
  }
1346
- catch { /* skip malformed */ }
1359
+ catch {
1360
+ // The literal "MELASSIST " appeared inside raw text (not a real frame)
1361
+ // — the JSON tail won't parse; treat the whole line as raw output.
1362
+ if (t && opts.verbose)
1363
+ console.log(chalk.gray(` [assistant-stdout] ${t}`));
1364
+ }
1347
1365
  }
1348
1366
  else if (t && opts.verbose) {
1349
1367
  console.log(chalk.gray(` [assistant-stdout] ${t}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.47",
3
+ "version": "1.1.48",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,