@melaya/runner 1.0.100 → 1.0.102

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.
Files changed (2) hide show
  1. package/dist/connection.js +20 -2
  2. package/package.json +1 -1
@@ -889,8 +889,12 @@ export async function connect(opts) {
889
889
  // watermark so the server doesn't needlessly re-offer a rehydrate (a bare
890
890
  // ready would read as watermark 0). No second spawn.
891
891
  const live = activeAssistants.get(sid);
892
+ // Report the live host's ACTUAL boot generation (not just its watermark) so a
893
+ // server that restarted (its in-memory session map wiped) can DETECT a host
894
+ // that booted under an older generation and reboot it instead of serving the
895
+ // next turn from stale memory/config. A bare ready hid this.
892
896
  if (live) {
893
- emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0 });
897
+ emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0, generation: live.generation });
894
898
  return;
895
899
  }
896
900
  // A boot is already in flight for this sid (concurrent start / server restart
@@ -1030,7 +1034,12 @@ export async function connect(opts) {
1030
1034
  }
1031
1035
  });
1032
1036
  proc.on("exit", (code) => {
1033
- activeAssistants.delete(sid);
1037
+ // Only evict if WE are still the registered host. A config-drift reboot
1038
+ // (e.g. the user added a connector) kills this host and spawns a REPLACEMENT
1039
+ // under the same sid; if this late exit deleted the map entry blindly it
1040
+ // would orphan the newcomer and make the next turn "session_not_found".
1041
+ if (activeAssistants.get(sid) === session)
1042
+ activeAssistants.delete(sid);
1034
1043
  const detail = code !== 0 && stderrTail.length ? stderrTail.slice(-12).join("\n") : "";
1035
1044
  socket.emit("runner:assistant_event", { sessionId: sid, turnId: "", kind: "session_closed", code, detail });
1036
1045
  console.log(chalk.gray(` ■ Assistant session ${sid.slice(0, 10)}… closed (exit ${code})`));
@@ -1080,6 +1089,15 @@ export async function connect(opts) {
1080
1089
  socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "session_not_found" });
1081
1090
  return;
1082
1091
  }
1092
+ // Generation gate (defense-in-depth): refuse to serve a turn stamped with a
1093
+ // generation that doesn't match this host's boot generation — the server has
1094
+ // moved on (restart / takeover) and this host is stale. session_not_found
1095
+ // triggers the server's clean re-boot path. Backward-compatible: a turn with
1096
+ // no generation (older server) is served as before.
1097
+ if (payload.generation != null && Number(payload.generation) !== s.generation) {
1098
+ socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "session_not_found" });
1099
+ return;
1100
+ }
1083
1101
  s.lastActivity = Date.now();
1084
1102
  // Per-turn hitl_mode: the host applies it to os.environ["MEL_ASSISTANT_HITL_MODE"]
1085
1103
  // (mirrored to MEL_HITL_MODE) BEFORE running the turn, so a mid-session flip
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.100",
3
+ "version": "1.0.102",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,