Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions App/shell/desktop/electron-builder.unsigned.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ extraResources:
to: memory-runtime
filter:
- "**/*"
# electron-builder excludes a matcher-root node_modules directory. Copy it
# through its own matcher so the standalone Memory runtime stays complete.
- from: dist/runtime/memory/node_modules
to: memory-runtime/node_modules
filter:
- "**/*"
- from: dist/runtime/bin
to: cli
filter:
Expand Down
6 changes: 6 additions & 0 deletions App/shell/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ extraResources:
to: memory-runtime
filter:
- "**/*"
# electron-builder excludes a matcher-root node_modules directory. Copy it
# through its own matcher so the standalone Memory runtime stays complete.
- from: dist/runtime/memory/node_modules
to: memory-runtime/node_modules
filter:
- "**/*"
- from: dist/runtime/bin
to: cli
filter:
Expand Down
5 changes: 4 additions & 1 deletion App/shell/desktop/src/main/runtime-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,9 @@ export function resolveRuntimeEntryPaths(options: StartManagedRuntimeServicesOpt
return { ...options.runtimeEntries };
}
return {
memoryEntry: join(options.appPath, "dist/runtime/memory/dist/src/server/index.js"),
memoryEntry: options.offlineMemoryRuntimeDirectory
? join(options.offlineMemoryRuntimeDirectory, "dist/src/server/index.js")
: join(options.appPath, "dist/runtime/memory/dist/src/server/index.js"),
agentEntry: join(options.appPath, "dist/runtime/memmy-agent/dist/main.js")
};
}
Expand Down Expand Up @@ -1547,6 +1549,7 @@ function isPackagedMemoryServiceProcess(pid: number, memoryEntry: string): boole
const normalizedCommand = command.replaceAll("\\", "/");
const normalizedEntry = resolve(memoryEntry).replaceAll("\\", "/");
return normalizedCommand.includes(normalizedEntry)
|| normalizedCommand.includes("/memory-runtime/dist/src/server/index.js")
|| normalizedCommand.includes("/dist/runtime/memory/src/server/index.js");
} catch {
return false;
Expand Down
33 changes: 33 additions & 0 deletions App/shell/desktop/tests/packaged-runtime-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,39 @@ describe("desktop packaged runtime boundaries", () => {
}
});

it("ships the standalone Memory runtime with production dependencies on macOS", () => {
for (const configPath of [electronBuilderPath, unsignedElectronBuilderPath]) {
const config = parseYaml(readFileSync(configPath, "utf8")) as {
extraResources?: Array<{ from?: string; to?: string; filter?: string[] }>;
};
expect(config.extraResources).toContainEqual({
from: "dist/runtime/memory",
to: "memory-runtime",
filter: ["**/*"]
});
expect(config.extraResources).toContainEqual({
from: "dist/runtime/memory/node_modules",
to: "memory-runtime/node_modules",
filter: ["**/*"]
});
}

const macSource = readFileSync(packageMacDmgPath, "utf8");
expect(macSource).toContain(
'packaged_memory_runtime="$app_path/Contents/Resources/memory-runtime"'
);
expect(macSource).toContain(
'$packaged_memory_runtime/node_modules/onnxruntime-node/bin/napi-v3/darwin/$target_cpu/libonnxruntime*.dylib'
);
expect(macSource).not.toContain(
'$unpacked_runtime/memory/node_modules/onnxruntime-node/bin/napi-v3/darwin/$target_cpu/libonnxruntime*.dylib'
);
const asarGuardSource = readFileSync(verifyPackagedAsarPath, "utf8");
expect(asarGuardSource).toContain(
'if (platform === "win32") {\n requiredFiles.push(\n "dist/runtime/memory/package.json"'
);
});

it("excludes dependency root tests and docs from every desktop app archive", () => {
for (const configPath of [
electronBuilderPath,
Expand Down
14 changes: 12 additions & 2 deletions Memory/src/cli/load-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function loadCloudServiceEnv(options: {
const moduleDir = options.moduleDir ?? dirname(fileURLToPath(import.meta.url));
const packagedRuntime = isPackagedRuntimeModule(moduleDir);
if (options.manifestPath !== undefined || packagedRuntime) {
const manifestPath = options.manifestPath ?? resolve(moduleDir, "../../../../main/desktop-edition.json");
const manifestPath = options.manifestPath ?? packagedManifestPath(moduleDir);
if (!existsSync(manifestPath)) {
throw new Error("Packaged desktop runtime manifest is missing");
}
Expand All @@ -77,7 +77,17 @@ export function loadCloudServiceEnv(options: {
}

function isPackagedRuntimeModule(moduleDir: string): boolean {
return resolve(moduleDir).replace(/\\/g, "/").endsWith("/dist/runtime/memory/src/cli");
const normalized = resolve(moduleDir).replace(/\\/g, "/");
return normalized.endsWith("/dist/runtime/memory/src/cli")
|| normalized.endsWith("/memory-runtime/dist/src/cli");
}

function packagedManifestPath(moduleDir: string): string {
const normalized = resolve(moduleDir).replace(/\\/g, "/");
if (normalized.endsWith("/memory-runtime/dist/src/cli")) {
return resolve(moduleDir, "../../../../app.asar/dist/main/desktop-edition.json");
}
return resolve(moduleDir, "../../../../main/desktop-edition.json");
}

loadCloudServiceEnv();
4 changes: 4 additions & 0 deletions Memory/src/model/agent-region.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface ResolveMemoryAgentRegionOptions {
export function packagedDesktopEditionManifestPath(
modelDirectory = import.meta.dirname
): string {
const normalized = resolve(modelDirectory).replace(/\\/g, "/");
if (normalized.includes("/memory-runtime/dist/")) {
return resolve(modelDirectory, "../../../../app.asar/dist/main/desktop-edition.json");
}
return resolve(modelDirectory, "../../../../main/desktop-edition.json");
}

Expand Down
8 changes: 8 additions & 0 deletions Memory/tests/llm-agent-region.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ describe("memory account model agent region", () => {
);
});

it("locates the desktop manifest beside the app ASAR for standalone Memory", () => {
const modelDirectory = resolve("app-root/Resources/memory-runtime/dist/src/model");

expect(packagedDesktopEditionManifestPath(modelDirectory)).toBe(
resolve("app-root/Resources/app.asar/dist/main/desktop-edition.json")
);
});

it.each([
[{ accountChannel: "email" }, "intl"],
[{ accountChannel: "phone" }, "cn"],
Expand Down
13 changes: 13 additions & 0 deletions Memory/tests/load-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ describe("packaged memmy-memory cloud-service loading", () => {
expect(env.MEMMY_CLOUD_SERVICE).toBe("https://dev.example.test");
});

it("loads the desktop manifest from the app ASAR for the standalone runtime", () => {
const root = fixtureRoot();
const moduleDir = join(root, "Resources", "memory-runtime", "dist", "src", "cli");
const manifestPath = join(root, "Resources", "app.asar", "dist", "main", "desktop-edition.json");
mkdirSync(moduleDir, { recursive: true });
mkdirSync(join(root, "Resources", "app.asar", "dist", "main"), { recursive: true });
writeFileSync(manifestPath, JSON.stringify({ cloudService: "https://standalone.example.test" }));

const env: NodeJS.ProcessEnv = {};
expect(loadCloudServiceEnv({ cwd: root, moduleDir, env })).toBe(manifestPath);
expect(env.MEMMY_CLOUD_SERVICE).toBe("https://standalone.example.test");
});

it("fails closed for an invalid packaged manifest", () => {
const root = fixtureRoot();
const manifestPath = join(root, "desktop-edition.json");
Expand Down
60 changes: 55 additions & 5 deletions scripts/internal/mac/build-dmg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,22 @@ resolve_microphone_usage_description() {

create_cli_launcher() {
local output_path="$1"
local asar_entry="$2"
local entry_path="$2"
local entry_kind="${3:-asar}"
local entry_expression

case "$entry_kind" in
asar)
entry_expression='"$RESOURCES_DIR/app.asar/'"$entry_path"'"'
;;
resource)
entry_expression='"$RESOURCES_DIR/memory-runtime/'"$entry_path"'"'
;;
*)
echo "Unsupported CLI launcher entry kind: $entry_kind" >&2
exit 1
;;
esac

cat > "$output_path" <<EOF
#!/usr/bin/env bash
Expand Down Expand Up @@ -168,7 +183,7 @@ fi

export ELECTRON_RUN_AS_NODE=1
export NODE_ENV="\${NODE_ENV:-production}"
exec "\$APP_EXEC" "\$RESOURCES_DIR/app.asar/$asar_entry" "\$@"
exec "\$APP_EXEC" $entry_expression "\$@"
EOF

chmod 755 "$output_path"
Expand Down Expand Up @@ -621,12 +636,23 @@ verify_packaged_mac_unpacked_artifacts() {
local app_path
app_path="$(resolve_packaged_mac_app_path "$target_cpu")"
local unpacked_runtime="$app_path/Contents/Resources/app.asar.unpacked/dist/runtime"
local packaged_memory_runtime="$app_path/Contents/Resources/memory-runtime"
local packaged_embedding_model="$app_path/Contents/Resources/embedding-models/$EMBEDDING_MODEL_ID"

require_packaged_runtime_file "$app_path/Contents/Resources/app.asar"
verify_packaged_runtime_config_boundary "$app_path/Contents/Resources"
require_packaged_runtime_glob "$unpacked_runtime/memory/node_modules/onnxruntime-node/bin/napi-v3/darwin/$target_cpu/libonnxruntime*.dylib"
require_packaged_runtime_glob "$unpacked_runtime/memory/node_modules/@img/sharp-libvips-darwin-$target_cpu/lib/libvips*.dylib"
require_packaged_runtime_file "$packaged_memory_runtime/package.json"
require_packaged_runtime_file "$packaged_memory_runtime/package-lock.json"
require_packaged_runtime_file "$packaged_memory_runtime/memory-runtime.json"
require_packaged_runtime_file "$packaged_memory_runtime/dist/src/server/index.js"
require_packaged_runtime_file "$packaged_memory_runtime/dist/src/cli/index.js"
require_packaged_runtime_file "$packaged_memory_runtime/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
require_packaged_runtime_glob "$packaged_memory_runtime/node_modules/sqlite-vec-darwin-$target_cpu/vec0.*"
require_packaged_runtime_file "$packaged_memory_runtime/node_modules/onnxruntime-node/bin/napi-v3/darwin/$target_cpu/onnxruntime_binding.node"
require_packaged_runtime_glob "$packaged_memory_runtime/node_modules/onnxruntime-node/bin/napi-v3/darwin/$target_cpu/libonnxruntime*.dylib"
require_packaged_runtime_file "$packaged_memory_runtime/node_modules/@img/sharp-darwin-$target_cpu/lib/sharp-darwin-$target_cpu.node"
require_packaged_runtime_glob "$packaged_memory_runtime/node_modules/@img/sharp-libvips-darwin-$target_cpu/lib/libvips*.dylib"
verify_packaged_memory_runtime_manifest "$packaged_memory_runtime" "$target_cpu"
require_packaged_runtime_file "$unpacked_runtime/memmy-agent/node_modules/@memmy/migrations/dist/index.js"
require_packaged_runtime_file "$packaged_embedding_model/config.json"
require_packaged_runtime_file "$packaged_embedding_model/tokenizer.json"
Expand All @@ -638,6 +664,30 @@ verify_packaged_mac_unpacked_artifacts() {
require_packaged_runtime_file "$unpacked_runtime/memmy-agent/node_modules/openclaw/node_modules/@lydell/node-pty-darwin-$target_cpu/prebuilds/darwin-$target_cpu/spawn-helper"
}

verify_packaged_memory_runtime_manifest() {
local packaged_memory_runtime="$1"
local target_cpu="$2"

node - "$MEMORY_DIR/package.json" "$packaged_memory_runtime/package.json" "$packaged_memory_runtime/package-lock.json" "$packaged_memory_runtime/memory-runtime.json" "$target_cpu" <<'NODE'
const fs = require("node:fs");
const [sourcePath, packagePath, lockPath, manifestPath, targetCpu] = process.argv.slice(2);
const source = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
const packaged = JSON.parse(fs.readFileSync(packagePath, "utf8"));
const lock = JSON.parse(fs.readFileSync(lockPath, "utf8"));
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
if (packaged.version !== source.version) throw new Error("Packaged Memory version does not match source");
if (lock.version !== source.version || lock.packages?.[""]?.version !== source.version) {
throw new Error("Packaged Memory lock version does not match source");
}
if (manifest.version !== source.version || manifest.target !== `darwin-${targetCpu}`) {
throw new Error("Packaged Memory runtime manifest does not match source or architecture");
}
if (manifest.entrypoint !== "dist/src/server/index.js" || manifest.viewer !== "dist/viewer/index.html") {
throw new Error("Packaged Memory runtime manifest entrypoints are invalid");
}
NODE
}

verify_packaged_runtime_config_boundary() {
local resources_root="$1"
local asar_file="$resources_root/app.asar"
Expand Down Expand Up @@ -850,7 +900,7 @@ node_modules/.bin/electron-rebuild \
-w better-sqlite3 \
-m "$RUNTIME_DIR/memmy-agent"
package_step_start "Create CLI launchers"
create_cli_launcher "$CLI_BIN_DIR/memmy-memory" "dist/runtime/memory/dist/src/cli/index.js"
create_cli_launcher "$CLI_BIN_DIR/memmy-memory" "dist/src/cli/index.js" resource
create_cli_launcher "$CLI_BIN_DIR/memmy" "dist/runtime/memmy-agent/dist/main.js"
create_cli_installer "$CLI_BIN_DIR/install-cli"
create_dmg_cli_installer_command "$DMG_HELPER_DIR/Install CLI.command"
Expand Down
15 changes: 10 additions & 5 deletions scripts/internal/shared/verify-packaged-asar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ if (entries.some((entry) => /(^|\/)\.env(?:$|\.)/u.test(entry))) {
const requiredFiles = [
"dist/main/desktop-edition.json",
"package.json",
"dist/runtime/memory/package.json",
"dist/runtime/memmy-agent/package.json",
"dist/runtime/memmy-agent/node_modules/@memmy/local-api-contracts/dist/index.js",
"node_modules/@memmy/backend/dist/src/adapters/outbound/skill-writer/workspace-bridge/memmy-workspace-bridge.mjs",
];
if (platform === "win32") {
requiredFiles.push(
"dist/runtime/memory/package.json",
`dist/runtime/memory/node_modules/onnxruntime-node/bin/napi-v3/${platform}/${arch}/onnxruntime_binding.node`,
`dist/runtime/memory/node_modules/onnxruntime-node/bin/napi-v3/${platform}/${arch}/onnxruntime.dll`,
"dist/runtime/memmy-agent/dist/main.js.map",
Expand Down Expand Up @@ -55,13 +55,18 @@ if (platform === "win32") {
}
}

for (const [file, lock] of [
const versionedFiles = [
["package.json", false],
["dist/runtime/memory/package.json", false],
["dist/runtime/memory/package-lock.json", true],
["dist/runtime/memmy-agent/package.json", false],
["dist/runtime/memmy-agent/package-lock.json", true],
]) {
];
if (platform === "win32") {
versionedFiles.splice(1, 0,
["dist/runtime/memory/package.json", false],
["dist/runtime/memory/package-lock.json", true],
);
}
for (const [file, lock] of versionedFiles) {
// electron-builder excludes npm lockfiles by default. The staged-runtime
// version guard validates them before packaging; re-check any that are kept.
if (lock && !entrySet.has(file)) continue;
Expand Down
Loading