@melaya/runner 1.0.13 → 1.0.14

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.
@@ -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.14",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,