@melaya/runner 1.0.13 → 1.0.15

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.
@@ -20,20 +20,38 @@ let _throttleQueue = [];
20
20
  function _flushThrottle(socket, verbose) {
21
21
  if (_throttleQueue.length === 0)
22
22
  return;
23
- // Send the LAST event per replyId (most recent content wins)
24
- const byReply = new Map();
23
+ // Coalescing rule:
24
+ // - `agent_message` is a streaming event type whose payload GROWS
25
+ // over time (each update replaces the previous with more text).
26
+ // Only the LAST per replyId matters — keep the last-write-wins
27
+ // dedup for these.
28
+ // - Everything else (reasoning / tool_call / tool_result /
29
+ // agent_reply / cost / session_status / compact / hook) is a
30
+ // discrete event — coalescing drops real information. Emit every
31
+ // one exactly once, preserving order.
32
+ const streamingByReply = new Map();
33
+ const discrete = [];
25
34
  for (const p of _throttleQueue) {
26
- const key = p.replyId || p.run_id || "default";
27
- byReply.set(key, p);
35
+ if (p.event_type === "agent_message") {
36
+ const key = p.replyId || p.run_id || "default";
37
+ streamingByReply.set(key, p);
38
+ }
39
+ else {
40
+ discrete.push(p);
41
+ }
28
42
  }
29
- for (const payload of byReply.values()) {
43
+ const emit = (payload) => {
30
44
  socket.emit("runner:event", payload);
31
45
  if (verbose) {
32
46
  const type = payload.event_type || "unknown";
33
- const agent = payload.replyName || "";
47
+ const agent = payload.replyName || payload.agent_name || "";
34
48
  process.stdout.write(` [relay] ${type}${agent ? ` (${agent})` : ""}\n`);
35
49
  }
36
- }
50
+ };
51
+ for (const payload of discrete)
52
+ emit(payload);
53
+ for (const payload of streamingByReply.values())
54
+ emit(payload);
37
55
  _throttleQueue = [];
38
56
  _throttleTimer = null;
39
57
  }
@@ -7,7 +7,7 @@
7
7
  * runtime from the server's /api/v1/agents/shared-bundle endpoint,
8
8
  * integrity-verified via SHA-256, and cached at ~/.melaya-runner/shared/.
9
9
  */
10
- import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
10
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "fs";
11
11
  import { join } from "path";
12
12
  import { homedir } from "os";
13
13
  import { createHash } from "crypto";
@@ -21,14 +21,38 @@ export function getSharedDir() {
21
21
  // and `from crews.X.main import ...` all resolve.
22
22
  return CACHE_DIR;
23
23
  }
24
+ /**
25
+ * Canary files that prove the current nested bundle layout has been written
26
+ * to disk. A cache where any of these is missing was produced by an older
27
+ * (flat-layout) bundler and must be refetched regardless of version match.
28
+ */
29
+ const CANARY_FILES = [
30
+ join(CACHE_DIR, "shared", "runtime", "agent_factory.py"),
31
+ join(CACHE_DIR, "shared", "runtime", "__init__.py"),
32
+ join(CACHE_DIR, "shared", "tools", "__init__.py"),
33
+ join(CACHE_DIR, "agentscope", "__init__.py"),
34
+ ];
35
+ function cacheLooksValid() {
36
+ return CANARY_FILES.every((p) => existsSync(p));
37
+ }
24
38
  export async function ensureSharedModules(serverUrl, expectedVersion) {
25
39
  mkdirSync(CACHE_DIR, { recursive: true });
26
- // Check cached version
40
+ // Check cached version. We require BOTH a version match AND a valid nested
41
+ // layout — older runner versions wrote flat filenames into shared/, and
42
+ // those caches must be purged rather than trusted.
27
43
  if (existsSync(VERSION_FILE)) {
28
44
  const cached = readFileSync(VERSION_FILE, "utf-8").trim();
29
- if (cached === expectedVersion && existsSync(join(CACHE_DIR, "shared", "events.py")) && existsSync(join(CACHE_DIR, "agentscope", "__init__.py"))) {
30
- return; // cache is up-to-date
45
+ if (cached === expectedVersion && cacheLooksValid()) {
46
+ return;
47
+ }
48
+ }
49
+ // Purge stale cache — flat-layout shared/ or missing agentscope dir.
50
+ // This is safe: we always refetch the full bundle below.
51
+ if (existsSync(SHARED_DIR) && !cacheLooksValid()) {
52
+ try {
53
+ rmSync(SHARED_DIR, { recursive: true, force: true });
31
54
  }
55
+ catch { /* best-effort */ }
32
56
  }
33
57
  // Fetch from server
34
58
  const httpUrl = serverUrl.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
@@ -44,6 +68,7 @@ export async function ensureSharedModules(serverUrl, expectedVersion) {
44
68
  throw new Error("Shared module integrity check failed — hash mismatch");
45
69
  }
46
70
  // Write files (sanitize filenames to prevent path traversal)
71
+ const writtenDirs = new Set();
47
72
  for (const [filename, content] of Object.entries(body.files)) {
48
73
  const safe = filename.replace(/\.\./g, "").replace(/^\//, "").replace(/\\/g, "/");
49
74
  if (safe.includes("..") || safe.startsWith("/"))
@@ -54,13 +79,27 @@ export async function ensureSharedModules(serverUrl, expectedVersion) {
54
79
  const dir = join(filepath, "..");
55
80
  mkdirSync(dir, { recursive: true });
56
81
  writeFileSync(filepath, content, "utf-8");
82
+ // Track every ancestor directory under CACHE_DIR so we can ensure each
83
+ // has an __init__.py for Python package resolution.
84
+ let cursor = dir;
85
+ while (cursor.startsWith(CACHE_DIR) && cursor !== CACHE_DIR) {
86
+ writtenDirs.add(cursor);
87
+ cursor = join(cursor, "..");
88
+ }
57
89
  }
58
- // Ensure __init__.py in shared/ and crews/ if missing
59
- for (const sub of ["shared", "crews"]) {
60
- const init = join(CACHE_DIR, sub, "__init__.py");
61
- if (existsSync(join(CACHE_DIR, sub)) && !existsSync(init)) {
90
+ // Ensure __init__.py in every nested directory of shared/ and crews/.
91
+ // agentscope ships its own __init__.py files as part of the bundle. This
92
+ // covers cases where the server didn't include an __init__.py for a
93
+ // particular subdir (e.g. accidentally removed) Python won't treat it
94
+ // as a package without one, breaking imports like `shared.runtime.*`.
95
+ for (const dir of writtenDirs) {
96
+ const rel = dir.slice(CACHE_DIR.length + 1).replace(/\\/g, "/");
97
+ const topLevel = rel.split("/")[0];
98
+ if (topLevel !== "shared" && topLevel !== "crews")
99
+ continue;
100
+ const init = join(dir, "__init__.py");
101
+ if (!existsSync(init))
62
102
  writeFileSync(init, "", "utf-8");
63
- }
64
103
  }
65
104
  writeFileSync(VERSION_FILE, expectedVersion, "utf-8");
66
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,