@melaya/runner 1.1.25 → 1.1.27

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.
@@ -426,6 +426,39 @@ def _log(msg: str) -> None:
426
426
  pass
427
427
 
428
428
 
429
+ # Failures the user can act on, told apart from the ones only we can. The runner
430
+ # already writes a full traceback to stderr; this builds the ONE line that reaches
431
+ # the panel, so it has to carry the exception type when the message is empty and
432
+ # stay readable when it is not.
433
+ _FRIENDLY_TURN_ERRORS = (
434
+ ("timeout", "The model did not respond in time. Try again, or pick a different model."),
435
+ ("timed out", "The model did not respond in time. Try again, or pick a different model."),
436
+ ("connection", "Could not reach the model provider from this machine. Check the network and try again."),
437
+ ("resolve", "Could not reach the model provider from this machine. Check the network and try again."),
438
+ ("refused", "The model provider refused the connection from this machine."),
439
+ ("ssl", "The connection to the model provider could not be secured on this machine."),
440
+ )
441
+
442
+
443
+ def _turn_error_message(exc: BaseException) -> str:
444
+ """One line for the panel. Falls back to the exception TYPE, never to nothing."""
445
+ raw = str(exc).strip()
446
+ name = type(exc).__name__
447
+ probe = (raw or name).lower()
448
+ for needle, friendly in _FRIENDLY_TURN_ERRORS:
449
+ if needle in probe:
450
+ detail = ": " + raw[:200] if raw else ""
451
+ return friendly + " (" + name + detail + ")"
452
+ if raw:
453
+ return raw[:500]
454
+ # Empty message: the type is all there is, and it is genuinely useful -
455
+ # TimeoutError and ConnectionResetError point at different fixes.
456
+ return (
457
+ "The assistant turn failed with " + name + " and no further detail. "
458
+ "The full traceback is in the runner log."
459
+ )
460
+
461
+
429
462
  def _extract_text(result) -> str:
430
463
  """Pull the final assistant text out of an agentscope reply Msg."""
431
464
  if result is None:
@@ -907,7 +940,14 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False) ->
907
940
  except Exception as exc:
908
941
  _log("turn error:\n" + traceback.format_exc())
909
942
  _stream["turnId"] = ""
910
- _emit(turn_id, "error", message=str(exc) or "assistant turn failed")
943
+ # NEVER surface a bare "assistant turn failed". That fallback fired only
944
+ # when str(exc) was EMPTY - exactly the case where the exception TYPE is
945
+ # the sole remaining clue, and it was being discarded. Several of the
946
+ # likeliest failures here stringify to nothing at all (asyncio.TimeoutError,
947
+ # httpx.ReadTimeout, ConnectionResetError), so the panel showed a message
948
+ # that named neither what broke nor where. Reported 2026-09-03: two turns
949
+ # in a row with no cause anywhere on screen.
950
+ _emit(turn_id, "error", message=_turn_error_message(exc))
911
951
  _emit(turn_id, "done")
912
952
  return
913
953
 
@@ -921,7 +961,10 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False) ->
921
961
  if result is _WALLCLOCK:
922
962
  _stream["turnId"] = ""
923
963
  _log("browser turn wall-clock budget exceeded")
924
- _emit(turn_id, "error", message="browser_turn_wallclock_exceeded")
964
+ _emit(turn_id, "error", message=(
965
+ "This browser task ran past its time budget and was stopped. "
966
+ "Ask again with a narrower step, or split it into two."
967
+ ))
925
968
  _emit(turn_id, "done")
926
969
  return
927
970
 
@@ -988,7 +1031,7 @@ def main() -> int:
988
1031
  agent = _build_agent()
989
1032
  except Exception as exc:
990
1033
  _log("boot failed:\n" + traceback.format_exc())
991
- _emit("", "error", message=f"assistant host failed to start: {exc}")
1034
+ _emit("", "error", message="The assistant could not start on this runner. " + _turn_error_message(exc))
992
1035
  return 1
993
1036
  # Capture the freshly-built BASE system prompt (before any static context is
994
1037
  # folded in) so each turn can deterministically rebuild base + persona.
@@ -143,10 +143,16 @@ export function loadPinnedPublicKeysFromEnv() {
143
143
  // Verifier
144
144
  // ---------------------------------------------------------------------
145
145
  export const DEFAULT_CLOCK_TOLERANCE_SECONDS = 30;
146
- /** Sanity ceiling: matches the server's MAX_TTL_SECONDS. A "valid"
147
- * token claiming a longer life than the issuer can mint is forged or
148
- * corrupt, so it is rejected regardless of signature. */
149
- const MAX_LIFETIME_SECONDS = 900;
146
+ /** Sanity ceiling: MUST match the server's MAX_TTL_SECONDS
147
+ * (server/src/services/browser/browserGrant.ts). A "valid" token claiming a
148
+ * longer life than the issuer can mint is forged or corrupt, so it is
149
+ * rejected regardless of signature.
150
+ *
151
+ * Raised 900s -> 4h alongside the server. Because this half ships in the
152
+ * runner, a server deployed ahead of a runner republish would mint grants
153
+ * this rejects with claims_invalid — publish the runner before, or at the
154
+ * same time as, the server change. */
155
+ const MAX_LIFETIME_SECONDS = 4 * 60 * 60;
150
156
  function b64urlJson(part, what) {
151
157
  let buf;
152
158
  try {
@@ -1478,7 +1478,7 @@ export async function connect(opts) {
1478
1478
  socket.on("runner:assistant_turn", async (payload) => {
1479
1479
  const s = activeAssistants.get(String(payload.sessionId || ""));
1480
1480
  if (!s) {
1481
- socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "session_not_found" });
1481
+ socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "The assistant session on your runner is gone (it most likely restarted). Send the message again to start a fresh one." });
1482
1482
  return;
1483
1483
  }
1484
1484
  // Generation gate (defense-in-depth): refuse to serve a turn stamped with a
@@ -1487,7 +1487,7 @@ export async function connect(opts) {
1487
1487
  // triggers the server's clean re-boot path. Backward-compatible: a turn with
1488
1488
  // no generation (older server) is served as before.
1489
1489
  if (payload.generation != null && Number(payload.generation) !== s.generation) {
1490
- socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "session_not_found" });
1490
+ socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "The assistant session on your runner is gone (it most likely restarted). Send the message again to start a fresh one." });
1491
1491
  return;
1492
1492
  }
1493
1493
  s.lastActivity = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,