Compare commits

...
Author SHA1 Message Date
alvinfeiandCursor a15e72178e fix(web): distinguish scanned and saved imports
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 20:54:54 +08:00
a04fcb50d5 docs: announce MemHub and CLI releases (#56)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:56:32 +08:00
a28ade2faf fix(release): hash Runtime symlinks consistently (#55)
Co-authored-by: alvinfei <alvinfei@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 19:56:07 +08:00
7 changed files with 133 additions and 15 deletions

No files matched your search

+2 -1
View File
@@ -32,7 +32,8 @@ English | [中文](README.zh.md)
## Latest News
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **Released** **Hy Memory 2.0 (`hy-memory==2.0.3`)** on [PyPI](https://pypi.org/project/hy-memory/2.0.3/) and open-sourced the **Hy-Memory** monorepo together with **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 📦 **Released [MemHub v1.0.4](https://github.com/Tencent-Hunyuan/Hy-Memory/releases/tag/v1.0.4)** and **[memhub-cli 1.0.5](https://www.npmjs.com/package/memhub-cli/v/1.0.5)**, with a signed macOS desktop app and verified macOS/Linux Runtimes!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **Released** **Hy Memory 2.0 (`hy-memory==2.0.4`)** on [PyPI](https://pypi.org/project/hy-memory/2.0.4/) and open-sourced the **Hy-Memory** monorepo together with **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **Open-sourced** **[Hy-Memory-30B-A3B](https://huggingface.co/tencent/Hy-Memory-30B-A3B)** on Hugging Face along with its [technical report](https://arxiv.org/abs/2610.22196) on attributed reinforcement learning for long-term memory writing!
* *<mark>[new]</mark>* 🔥🔥 **[2026-09]**: 📊 **Released** **[CMB: A Coding Memory Benchmark for Long-Horizon and Interleaved Coding Agents](docs/papers/CMB_A_Coding_Memory_Benchmark_for_Long_Horizon_and_Interleaved_Coding_Agents.pdf)** (submitted to **ICLR 2027**).
* *<mark>[new]</mark>* 🔥🔥 **[2026-08]**: 🎉 **Accepted** at **EMNLP 2026**: **[Memory Beyond Recall: A Dual-Process Cognitive Memory System for Self-Evolving LLM Agents](https://arxiv.org/abs/2606.09483)**!
+2 -1
View File
@@ -32,7 +32,8 @@
## 最新动态
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **正式发布** **Hy Memory 2.0 (`hy-memory==2.0.3`)**(已上线 [PyPI](https://pypi.org/project/hy-memory/2.0.3/)),并开源 **Hy-Memory** monorepo 与 **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 📦 **正式发布 [MemHub v1.0.4](https://github.com/Tencent-Hunyuan/Hy-Memory/releases/tag/v1.0.4)** 与 **[memhub-cli 1.0.5](https://www.npmjs.com/package/memhub-cli/v/1.0.5)**,提供已签名的 macOS 桌面应用及经过验证的 macOS/Linux Runtime!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **正式发布** **Hy Memory 2.0 (`hy-memory==2.0.4`)**(已上线 [PyPI](https://pypi.org/project/hy-memory/2.0.4/)),并开源 **Hy-Memory** monorepo 与 **[MemHub](memhub/README.md)**!
* *<mark>[new]</mark>* 🔥🔥🔥 **[2026-10]**: 🚀 **模型开源** 在 Hugging Face 发布 **[Hy-Memory-30B-A3B](https://huggingface.co/tencent/Hy-Memory-30B-A3B)** 长期记忆写入模型及[技术报告](https://arxiv.org/abs/2610.22196)!
* *<mark>[new]</mark>* 🔥🔥 **[2026-09]**: 📊 **基准与方法发布** 推出长周期与交错式编程记忆评测基准 **[CMB: A Coding Memory Benchmark for Long-Horizon and Interleaved Coding Agents](docs/papers/CMB_A_Coding_Memory_Benchmark_for_Long_Horizon_and_Interleaved_Coding_Agents.pdf)**(已投稿 **ICLR 2027**)。
* *<mark>[new]</mark>* 🔥🔥 **[2026-08]**: 🎉 **论文录用** 双系统认知记忆架构论文 **[Memory Beyond Recall: A Dual-Process Cognitive Memory System for Self-Evolving LLM Agents](https://arxiv.org/abs/2606.09483)** 被 **EMNLP 2026** 接收!
@@ -174,20 +174,20 @@ it("continues to step3 while sample GET is pending without calling completion AP
expect(screen.queryByText(/^已生成.*记忆/)).toBeNull();
});
it.each([
"pending",
"paused",
"interrupted",
"failed",
"cancelled",
"completed",
["pending", "已扫描 100 条,正在筛选"],
["paused", "已扫描 100 条,尚未保存"],
["interrupted", "已扫描 100 条,尚未保存"],
["failed", "已扫描 100 条,尚未保存"],
["cancelled", "已扫描 100 条,尚未保存"],
["completed", "已扫描 100 条,本次没有可保存的对话"],
] as const)(
"shows %s without mutating jobs or fetching missing content",
(status) => {
(status, expected) => {
setup({
jobs: [{ ...job, status, stored: 0 }],
sources: [{ ...source, imported_messages: 0 }],
});
expect(screen.getByText("已扫描 100 条,正在保存")).toBeTruthy();
expect(screen.getByText(expected)).toBeTruthy();
expect(api.receipt).not.toHaveBeenCalled();
},
);
@@ -38,6 +38,71 @@ it("uses the selected job's message count, never the source total or fake percen
expect(container.textContent).not.toContain("%");
expect(container.textContent).not.toContain("持续导入中");
});
it("does not describe scanned-but-skipped history as saved", () => {
const { container } = render(
<R6ImportProgress
sourceIds={["cursor"]}
sources={[source]}
jobs={[
{
...job,
status: "completed",
discovered: 12_375,
stored: 0,
skipped: 12_375,
history_after: "2026-10-15T00:00:00Z",
},
]}
/>,
);
expect(
screen.getByText("已扫描 12,375 条,本次没有符合时间范围的对话"),
).toBeTruthy();
expect(screen.getByText("导入完成")).toBeTruthy();
expect(container.textContent).not.toContain("正在保存");
expect(container.textContent).not.toContain("已保存 12,375");
});
it("calls an active bounded scan filtering rather than saving", () => {
render(
<R6ImportProgress
sourceIds={["cursor"]}
sources={[source]}
jobs={[
{
...job,
discovered: 3_522,
stored: 0,
history_after: "2026-10-15T00:00:00Z",
},
]}
/>,
);
expect(screen.getByText("已扫描 3,522 条,正在筛选最近 3 天")).toBeTruthy();
});
it("reports only stored rows as saved after a completed partial import", () => {
const { container } = render(
<R6ImportProgress
sourceIds={["cursor"]}
sources={[source]}
jobs={[
{
...job,
status: "completed",
discovered: 25_177,
stored: 20_826,
skipped: 4_351,
},
]}
/>,
);
expect(screen.getByText("已保存 20,826 条消息")).toBeTruthy();
expect(screen.getByText("导入完成")).toBeTruthy();
expect(container.textContent).not.toContain("25,177");
});
it.each(["paused", "interrupted", "failed", "completed", "cancelled"] as const)(
"does not animate %s as active work",
(status) => {
+19 -5
View File
@@ -3,6 +3,21 @@ import { useEffect, useLayoutEffect, useRef } from "react";
import { AppMark } from "./app-mark";
import "./r6-import-progress.css";
function progressText(job: ImportJob): string | null {
if (job.stored > 0) return `已保存 ${job.stored.toLocaleString()} 条消息`;
if (job.discovered <= 0) return null;
const scanned = `已扫描 ${job.discovered.toLocaleString()} 条`;
if (job.status === "completed")
return job.history_after
? `${scanned},本次没有符合时间范围的对话`
: `${scanned},本次没有可保存的对话`;
if (job.status === "pending" || job.status === "running")
return job.history_after
? `${scanned},正在筛选最近 3 天`
: `${scanned},正在筛选`;
return `${scanned},尚未保存`;
}
export function R6ImportProgress({
sourceIds,
sources,
@@ -86,8 +101,9 @@ export function R6ImportProgress({
interrupted: "已中断",
failed: "导入遇到问题",
cancelled: "已取消",
completed: "已保存",
completed: "导入完成",
};
const progress = job ? progressText(job) : null;
return (
<article
className="r6-import-progress-card"
@@ -102,10 +118,8 @@ export function R6ImportProgress({
</div>
</div>
<h2>{source?.display_name ?? id}</h2>
{job && job.stored > 0 ? (
<p>{`已保存 ${job.stored.toLocaleString()} 条消息`}</p>
) : job && job.discovered > 0 ? (
<p>{`已扫描 ${job.discovered.toLocaleString()} 条,正在保存`}</p>
{progress ? (
<p>{progress}</p>
) : noImportedHistory ? (
<p>没有可保存的历史对话</p>
) : null}
+7
View File
@@ -8,6 +8,7 @@ import {
mkdir,
readdir,
readFile,
readlink,
realpath,
rm,
stat,
@@ -320,6 +321,12 @@ async function distributionBytes(source) {
async function sha256File(file) {
const digest = createHash("sha256");
const info = await lstat(file);
if (info.isSymbolicLink()) {
digest.update("symlink\0");
digest.update(await readlink(file));
return digest.digest("hex");
}
for await (const chunk of createReadStream(file)) digest.update(chunk);
return digest.digest("hex");
}
@@ -6,6 +6,7 @@ import {
readdir,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import os from "node:os";
@@ -121,6 +122,35 @@ test("builds versioned Runtime archives, checksums and a downloader-compatible m
}
});
test("verifies Runtime symlinks with the same identity rule as the installer", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "memhub-release-assets-"));
t.after(() => rm(root, { recursive: true, force: true }));
const source = await distribution(root, "linux", "x64");
const link = "memhub";
await symlink(link, path.join(source, "bin", "node"));
const manifestPath = path.join(source, "manifest.json");
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
manifest.files["bin/node"] = sha256(`symlink\0${link}`);
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const release = await buildReleaseAssets({
output: path.join(root, "release"),
version: "0.1.0-internal.1",
channel: "prerelease",
assetBaseUrl: "https://git.example.test/release/downloads/",
distributions: [source],
});
assert.ok(
release.artifacts.some(
(artifact) =>
artifact.kind === "runtime" &&
artifact.platform === "linux" &&
artifact.arch === "x64",
),
);
});
test("refuses version, build and duplicate target drift", async (t) => {
const root = await mkdtemp(path.join(os.tmpdir(), "memhub-release-assets-"));
t.after(() => rm(root, { recursive: true, force: true }));