Compare commits

..
Author SHA1 Message Date
f79770cdb0 release: prepare MemHub 1.0.2 (#19)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 01:54:14 +08:00
9ba602ffd2 ci: upload tagged Linux runtimes to draft releases (#16)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 01:51:58 +08:00
842fbe6044 fix(memhub): relaunch safely with existing runtimes (#18)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 01:50:23 +08:00
25 changed files with 142 additions and 32 deletions

No files matched your search

+11 -2
View File
@@ -16,11 +16,20 @@ Hy Memory and MemHub are versioned and released independently.
Because PyPI release metadata is immutable, the next SDK patch release must Because PyPI release metadata is immutable, the next SDK patch release must
carry the updated license metadata; `2.0.1` retains its published metadata. carry the updated license metadata; `2.0.1` retains its published metadata.
## MemHub 1.0.1 — Unreleased ## MemHub 1.0.2 — Unreleased
- Updated the source version to `1.0.1` (build 5). - Updated the source version to `1.0.2` (build 6).
- Pinned the public SDK to `hy-memory==2.0.2`. - Pinned the public SDK to `hy-memory==2.0.2`.
- Pinned `onnxruntime==1.23.2` to retain macOS x64 packaging support. - Pinned `onnxruntime==1.23.2` to retain macOS x64 packaging support.
- Fixed relaunch and upgrade failures when a prior managed Runtime exists.
- Embedded canonical build identity when an external packaging pipeline omits
release environment fields.
- Added automatic Linux Runtime upload to tagged Draft Releases.
## MemHub 1.0.1 — Superseded before release
- The candidate tag was not published as a GitHub Release; `1.0.2` replaces it
after the managed-Runtime relaunch fix.
## MemHub 1.0.0 ## MemHub 1.0.0
+3 -3
View File
@@ -6,13 +6,13 @@ SDK 和记忆引擎。One Memory / `one-memory` 是历史模块、数据目录
## 发行与平台合同 ## 发行与平台合同
公共 Python SDK 为 `hy-memory==2.0.2`。MemHub 源码版本为 `1.0.1` 公共 Python SDK 为 `hy-memory==2.0.2`。MemHub 源码版本为 `1.0.2`
(build 5),GitHub 仓库为 (build 6),GitHub 仓库为
[`Tencent-Hunyuan/Hy-Memory`](https://github.com/Tencent-Hunyuan/Hy-Memory),npm 包名为 [`Tencent-Hunyuan/Hy-Memory`](https://github.com/Tencent-Hunyuan/Hy-Memory),npm 包名为
`memhub-cli`。当前代码架构见 `memhub-cli`。当前代码架构见
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。
`v1.0.1` 的发行目标是 Linux x64/arm64 Runtime 与 macOS arm64/x64 App 和 Runtime。 `v1.0.2` 的发行目标是 Linux x64/arm64 Runtime 与 macOS arm64/x64 App 和 Runtime。
Linux 制品由 GitHub Actions 在原生架构 runner 上构建并完成生命周期验收;macOS Linux 制品由 GitHub Actions 在原生架构 runner 上构建并完成生命周期验收;macOS
制品由发行流水线完成 Developer ID 签名、公证和验收后手动上传。Windows 不支持。 制品由发行流水线完成 Developer ID 签名、公证和验收后手动上传。Windows 不支持。
只有同时出现在 GitHub Release 与 `release-manifest.json` 中、且通过公开校验的制品才属于 只有同时出现在 GitHub Release 与 `release-manifest.json` 中、且通过公开校验的制品才属于
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/cli", "name": "@one-memory/cli",
"version": "1.0.1", "version": "1.0.2",
"description": "MemHub bootstrap CLI for installing and managing the local-first shared memory runtime", "description": "MemHub bootstrap CLI for installing and managing the local-first shared memory runtime",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "uv_build"
[project] [project]
name = "one-memory" name = "one-memory"
version = "1.0.1" version = "1.0.2"
description = "MemHub local-first memory core" description = "MemHub local-first memory core"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -1801,7 +1801,7 @@ wheels = [
[[package]] [[package]]
name = "one-memory" name = "one-memory"
version = "1.0.1" version = "1.0.2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiomysql" }, { name = "aiomysql" },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/desktop", "name": "@one-memory/desktop",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
@@ -0,0 +1,27 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { expect, it } from "vitest";
it("bakes fallback release identity when the packaging pipeline omits it", async () => {
const config = await readFile(
path.resolve(__dirname, "../tsup.config.ts"),
"utf8",
);
expect(config).toContain("resolveBuildNumber");
expect(config).toContain(
"const releaseBuildNumber = resolveBuildNumber(process.env)",
);
expect(config).toMatch(
/__ONE_MEMORY_BUILD_NUMBER__: JSON\.stringify\(\s*String\(releaseBuildNumber\)/,
);
expect(config).toMatch(
/process\.env\.ONE_MEMORY_COMMIT\?\.trim\(\) \|\| readSourceCommit\(\)/,
);
expect(config).toMatch(
/execFileSync\("git", \["rev-parse", "HEAD"\],[\s\S]*?encoding: "utf8"/,
);
expect(config).toContain(
"__ONE_MEMORY_COMMIT__: JSON.stringify(releaseCommit)",
);
});
@@ -234,7 +234,9 @@ describe("desktop lifecycle failure paths", () => {
await f.flush(); await f.flush();
expect(f.createWindow).not.toHaveBeenCalled(); expect(f.createWindow).not.toHaveBeenCalled();
expect(f.quit().preventDefault).toHaveBeenCalled(); expect(f.quit().preventDefault).toHaveBeenCalled();
f.stopping.resolve(); // Teardown can fail after the startup error (for example, while writing a
// diagnostic). It must not replace the useful error with a second dialog.
f.stopping.reject(new Error("diagnostic_detail_key_invalid"));
await f.flush(); await f.flush();
// Whatever did come up before the failure still has to be torn down. // Whatever did come up before the failure still has to be torn down.
expect(f.stop).toHaveBeenCalledTimes(1); expect(f.stop).toHaveBeenCalledTimes(1);
+3 -1
View File
@@ -108,7 +108,9 @@ export function startDesktopLifecycle(options: LifecycleOptions): void {
shutdown = startup shutdown = startup
.catch(() => {}) .catch(() => {})
.then(options.stop) .then(options.stop)
.catch(options.onError) // Quit-time failures are logged by stop itself. A second modal here can
// only obscure the startup error that initiated this shutdown.
.catch(report)
.finally(() => { .finally(() => {
stopped = true; stopped = true;
app.quit(); app.quit();
+4 -3
View File
@@ -124,9 +124,10 @@ export function hourlyRuntimeInstanceId(): string {
* `build_number` is the only ordering authority the installer compares, so a * `build_number` is the only ordering authority the installer compares, so a
* package that omits it defaults to 0 and can never outrank an earlier install: * package that omits it defaults to 0 and can never outrank an earlier install:
* R->R.1 would be refused as a downgrade. The value is baked into the bundle's * R->R.1 would be refused as a downgrade. The value is baked into the bundle's
* `LSEnvironment` at pack time (`apps/desktop/scripts/after-pack.cjs`); this * `LSEnvironment` at pack time (`apps/desktop/scripts/after-pack.cjs`) and is
* reads it back and never invents one, so a source checkout that did not pass a * also compiled from the canonical root release contract. This reader accepts
* build number stays undefined rather than fabricating order. * either source so Finder launches still carry ordering even when an external
* packaging pipeline omitted the plist field.
*/ */
export function resolvePackagedBuildNumber( export function resolvePackagedBuildNumber(
environmentValue: unknown, environmentValue: unknown,
+1 -1
View File
@@ -1404,7 +1404,7 @@ if (desktopVersion !== null) {
component: "desktop.lifecycle", component: "desktop.lifecycle",
event: "desktop.runtime_stop", event: "desktop.runtime_stop",
outcome: event.outcome, outcome: event.outcome,
details: { runtime: event.detail }, details: { runtime_state: event.detail },
}); });
}, },
}); });
+20 -7
View File
@@ -1,12 +1,29 @@
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { defineConfig } from "tsup"; import { defineConfig } from "tsup";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const { resolveBuildVersion } = const { resolveBuildNumber, resolveBuildVersion } =
require("../../scripts/release-version.cjs") as { require("../../scripts/release-version.cjs") as {
resolveBuildNumber(environment?: NodeJS.ProcessEnv, root?: string): number;
resolveBuildVersion(environment?: NodeJS.ProcessEnv, root?: string): string; resolveBuildVersion(environment?: NodeJS.ProcessEnv, root?: string): string;
}; };
const releaseVersion = resolveBuildVersion(process.env); const releaseVersion = resolveBuildVersion(process.env);
const releaseBuildNumber = resolveBuildNumber(process.env);
const releaseCommit =
process.env.ONE_MEMORY_COMMIT?.trim() || readSourceCommit();
function readSourceCommit(): string {
try {
return execFileSync("git", ["rev-parse", "HEAD"], {
cwd: fileURLToPath(new URL("../..", import.meta.url)),
encoding: "utf8",
}).trim();
} catch {
return "unknown";
}
}
export default defineConfig({ export default defineConfig({
entry: ["src/main.ts", "src/preload.ts"], entry: ["src/main.ts", "src/preload.ts"],
@@ -27,11 +44,7 @@ export default defineConfig({
__ONE_MEMORY_TRIAL_PROFILE__: JSON.stringify( __ONE_MEMORY_TRIAL_PROFILE__: JSON.stringify(
process.env.ONE_MEMORY_BUILD_TRIAL_PROFILE ?? null, process.env.ONE_MEMORY_BUILD_TRIAL_PROFILE ?? null,
), ),
__ONE_MEMORY_BUILD_NUMBER__: JSON.stringify( __ONE_MEMORY_BUILD_NUMBER__: JSON.stringify(String(releaseBuildNumber)),
process.env.ONE_MEMORY_BUILD_NUMBER ?? "unknown", __ONE_MEMORY_COMMIT__: JSON.stringify(releaseCommit),
),
__ONE_MEMORY_COMMIT__: JSON.stringify(
process.env.ONE_MEMORY_COMMIT ?? "unknown",
),
}, },
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/site", "name": "@one-memory/site",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/web", "name": "@one-memory/web",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
# MemHub 安装与发行 # MemHub 安装与发行
MemHub `v1.0.1`(build 5)的发行目标如下: MemHub `v1.0.2`(build 6)的发行目标如下:
| 平台 | 发行物 | 构建与验收 | | 平台 | 发行物 | 构建与验收 |
| --- | --- | --- | | --- | --- | --- |
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "one-memory", "name": "one-memory",
"version": "1.0.1", "version": "1.0.2",
"release": { "release": {
"build_number": 5, "build_number": 6,
"macos_min_version": "13.0", "macos_min_version": "13.0",
"app_update": "manual", "app_update": "manual",
"runtime_manifest_schema": 1, "runtime_manifest_schema": 1,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/contracts", "name": "@one-memory/contracts",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"types": "./src/index.ts", "types": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/host-bridge", "name": "@one-memory/host-bridge",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/operations", "name": "@one-memory/operations",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"types": "./src/index.ts", "types": "./src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@one-memory/runtime-control", "name": "@one-memory/runtime-control",
"version": "1.0.1", "version": "1.0.2",
"private": true, "private": true,
"type": "module", "type": "module",
"bin": { "bin": {
@@ -217,6 +217,50 @@ describe("local Runtime installation", () => {
}, },
); );
it("ignores Finder metadata without weakening payload verification", async () => {
const value = await distribution();
const plan = await planLocalInstall({
source: value.source,
installRoot: value.installRoot,
platform: process.platform,
arch: process.arch,
});
const first = await applyLocalInstall(plan);
await writeFile(path.join(plan.version_directory, ".DS_Store"), "finder");
await expect(
verifyInstalledDistribution(
plan.version_directory,
process.platform,
process.arch,
),
).resolves.toMatchObject({ version: plan.version });
const repeated = await applyLocalInstall(plan);
expect(first.created_version).toBe(true);
expect(repeated.created_version).toBe(false);
await writeFile(
path.join(plan.version_directory, "unexpected-runtime-file"),
"not metadata",
);
await expect(
verifyInstalledDistribution(
plan.version_directory,
process.platform,
process.arch,
),
).rejects.toThrow("distribution_manifest_incomplete");
});
it("does not let a non-file masquerade as Finder metadata", async () => {
const value = await distribution();
await symlink("../outside", path.join(value.source, ".DS_Store"));
await expect(
verifyInstalledDistribution(value.source, process.platform, process.arch),
).rejects.toThrow("distribution_path_invalid");
});
it.each(["extra", "hash"])( it.each(["extra", "hash"])(
"same-version reuse still rejects %s changes to installed canonical trees", "same-version reuse still rejects %s changes to installed canonical trees",
async (change) => { async (change) => {
@@ -622,6 +622,12 @@ async function treeFiles(root: string, current = root): Promise<string[]> {
entries.sort((left, right) => left.name.localeCompare(right.name)); entries.sort((left, right) => left.name.localeCompare(right.name));
const result: string[] = []; const result: string[] = [];
for (const entry of entries) { for (const entry of entries) {
// Finder may add this metadata file after installation. It is not Runtime
// payload and must not make an otherwise verified install unreusable.
if (entry.name === ".DS_Store") {
if (!entry.isFile()) throw new Error("distribution_path_invalid");
continue;
}
const target = path.join(current, entry.name); const target = path.join(current, entry.name);
const relative = path.relative(root, target); const relative = path.relative(root, target);
if (!safeRelative(relative)) throw new Error("distribution_path_invalid"); if (!safeRelative(relative)) throw new Error("distribution_path_invalid");
@@ -256,6 +256,7 @@ describe("installPackagedRuntime", () => {
}); });
const selectedBefore = await readlink(current); const selectedBefore = await readlink(current);
const inodeBefore = (await lstat(current)).ino; const inodeBefore = (await lstat(current)).ino;
await writeFile(path.join(current, ".DS_Store"), "finder metadata");
await expect( await expect(
installPackagedRuntime({ installPackagedRuntime({
resourcesPath: resources, resourcesPath: resources,
@@ -429,6 +429,11 @@ async function treeFiles(root: string, current = root): Promise<string[]> {
entries.sort((left, right) => left.name.localeCompare(right.name)); entries.sort((left, right) => left.name.localeCompare(right.name));
const result: string[] = []; const result: string[] = [];
for (const entry of entries) { for (const entry of entries) {
// Finder metadata is not part of the packaged Runtime identity.
if (entry.name === ".DS_Store") {
if (!entry.isFile()) throw new Error("distribution_path_invalid");
continue;
}
const target = path.join(current, entry.name); const target = path.join(current, entry.name);
const relative = path.relative(root, target); const relative = path.relative(root, target);
if (entry.isSymbolicLink()) { if (entry.isSymbolicLink()) {
+1 -1
View File
@@ -13,7 +13,7 @@ test("installation docs disclose publication and lifecycle boundaries", async ()
); );
assert.match(readme, /公共 Python SDK 为 `hy-memory==2\.0\.2`/); assert.match(readme, /公共 Python SDK 为 `hy-memory==2\.0\.2`/);
assert.match(readme, /`v1\.0\.1` 的发行目标是 Linux x64\/arm64 Runtime/); assert.match(readme, /`v1\.0\.2` 的发行目标是 Linux x64\/arm64 Runtime/);
assert.match(readme, /Linux 制品由 GitHub Actions/); assert.match(readme, /Linux 制品由 GitHub Actions/);
assert.match(readme, /macOS.*Developer ID 签名、公证/s); assert.match(readme, /macOS.*Developer ID 签名、公证/s);
assert.match(readme, /release-manifest\.json/); assert.match(readme, /release-manifest\.json/);