Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dabe01c
fix(documents): support continued reading of long documents
Orsted290 Sep 23, 2026
8114ccc
fix(agent): keep read_file line counts aligned with apply_patch
Orsted290 Sep 24, 2026
c639753
chore(documents): drop debug scripts and investigation notes
Orsted290 Sep 28, 2026
1aa0614
Merge pull request #568 from MemTensor/main
syzsunshine219 Sep 29, 2026
32c3437
fix: refresh installed agent hooks when the bundled revision changes
Orsted290 Sep 30, 2026
478ae78
fix: deprioritize startup summary repairs
Sep 30, 2026
b5b9174
Merge upstream/v1.2.1 into fix-hook-auto-update
Orsted290 Sep 30, 2026
fb65ef0
fix(dsh): use the current recall message source and assistant event
Wang-Daoji Sep 30, 2026
8bb6d0d
fix(dsh): give the memory plugin a package version
Wang-Daoji Sep 30, 2026
7e9ec79
fix(dsh): align standalone adapter with supported lifecycle
hijzy Oct 8, 2026
7580c48
fix(dsh): align client manifest with supported tags
hijzy Oct 8, 2026
dcdecf6
Merge pull request #579 from hijzy/main
hijzy Oct 8, 2026
ceb5fa3
fix: remove legacy memory embedding config
hijzy Oct 8, 2026
fcf55fd
Merge pull request #581 from hijzy/main
hijzy Oct 8, 2026
9da8ea8
fix: retry a hook install whose revision was written before the files
Orsted290 Oct 8, 2026
fdb3617
fix(analytics): report each newly stored memory once
Oct 8, 2026
3c549a2
Merge pull request #574 from Orsted290/fix-hook-auto-update
origami-chen Oct 8, 2026
d424259
Merge pull request #582 from antalike/fix/tracking
syzsunshine219 Oct 8, 2026
cc09607
Merge pull request #559 from Orsted290/fix-long-document-reading
Wang-Daoji Oct 9, 2026
9cae0f4
Merge pull request #567 from Orsted290/fix-memory-patch-line-count
Wang-Daoji Oct 9, 2026
023430e
chore: merge v1.2.1 updates
Oct 9, 2026
fa12bd0
Merge pull request #577 from jia030928/fix-placeholder-summary-repair…
origami-chen Oct 9, 2026
8f9c428
fix(chat): keep copy actions on completed historical replies
Oct 9, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,9 @@ function createFakeAgentSourceService(): AgentSourceService {
async uninstallPlugin() {
return undefined;
},
async isInstalledHookCurrent() {
return true;
},
async detectMemoryPluginConflicts() {
return [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { createNodeHookCommand } from "../hook-command.js";
import { isInstalledMemmyHookCurrent, memmyHookRevisionField } from "../hook-revision.js";
import { readMemmyMemoryServiceConfig } from "../memmy-runtime-config.js";
import { removeMemmySkillDirectory, replaceMemmySkillDirectory } from "../skill-directory.js";
import { renderMemmyPluginSkillManifest } from "../templates/memmy-plugin.js";
Expand Down Expand Up @@ -85,6 +86,19 @@ export function createClaudeCodeSkillTarget(deps: CreateClaudeCodeSkillTargetDep
return (await readTextFile(join(root, TARGET_FILE_NAME))).includes(START_MARKER);
},

async isInstalledHookCurrent() {
const root = await this.resolveRootDirectory();
if (!root) return false;
const hookDirectory = join(root, HOOK_DIRECTORY_NAME);
return isInstalledMemmyHookCurrent({
source: CLAUDE_CODE_TARGET_ID,
mode: "claude-code",
hookScriptPath: join(hookDirectory, HOOK_SCRIPT_FILE_NAME),
bridgePath: join(hookDirectory, WORKSPACE_BRIDGE_FILE_NAME),
configPath: join(hookDirectory, HOOK_CONFIG_FILE_NAME)
});
},

async installPlugin(_targetId) {
const root = await this.resolveRootDirectory();
if (!root) {
Expand All @@ -93,11 +107,13 @@ export function createClaudeCodeSkillTarget(deps: CreateClaudeCodeSkillTargetDep

const hookDirectory = join(root, HOOK_DIRECTORY_NAME);
const hookScriptPath = join(hookDirectory, HOOK_SCRIPT_FILE_NAME);
const hookConfigPath = join(hookDirectory, HOOK_CONFIG_FILE_NAME);
const hookConfig = {
memmy_config_path: memmyConfigPath,
...(await readMemmyMemoryServiceConfig(memmyConfigPath))
};
await mkdir(hookDirectory, { recursive: true });
await writeFileAtomically(
join(hookDirectory, HOOK_CONFIG_FILE_NAME),
`${JSON.stringify({ memmy_config_path: memmyConfigPath, ...(await readMemmyMemoryServiceConfig(memmyConfigPath)) }, null, 2)}\n`
);
await writeFileAtomically(hookConfigPath, `${JSON.stringify(hookConfig, null, 2)}\n`);
await writeFileAtomically(
hookScriptPath,
renderMemmyResumeHookScript({ source: CLAUDE_CODE_TARGET_ID, mode: "claude-code" })
Expand All @@ -118,6 +134,10 @@ export function createClaudeCodeSkillTarget(deps: CreateClaudeCodeSkillTargetDep
upsertMarkerBlock(await readTextFile(filePath), renderMemmySkillBootstrapManifest(manifest))
);
await replaceMemmySkillDirectory(root, manifest);
await writeFileAtomically(
hookConfigPath,
`${JSON.stringify({ ...hookConfig, ...(await memmyHookRevisionField(CLAUDE_CODE_TARGET_ID, "claude-code")) }, null, 2)}\n`
);
},

async uninstallPlugin(_targetId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ describe("claude code skill target", () => {
}
});

it("marks a hook without a stored revision as outdated and refreshes it in place", async () => {
const { rootDirectory, memmyConfigPath } = createFixture();
const target = createClaudeCodeSkillTarget({ rootDirectory, memmyConfigPath });
const configPath = join(rootDirectory, "hooks", "memmy-memory-config.json");

await target.installPlugin?.("claude_code");
const installed = JSON.parse(readFileSync(configPath, "utf8")) as { hook_revision?: string };
expect(installed.hook_revision).toEqual(expect.stringMatching(/^[a-f0-9]{64}$/));
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(true);

writeFileSync(configPath, `${JSON.stringify({ ...installed, hook_revision: "stale" }, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

writeFileSync(join(rootDirectory, "hooks", "memmy-resume-hook.mjs"), "old hook\n", "utf8");
writeFileSync(configPath, `${JSON.stringify(installed, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);
rmSync(join(rootDirectory, "hooks", "memmy-resume-hook.mjs"));
writeFileSync(configPath, `${JSON.stringify(installed, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

await target.installPlugin?.("claude_code");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(true);
});

it("installs a UserPromptSubmit hook that blocks resume commands with top L1 candidates", async () => {
const { rootDirectory, memmyConfigPath } = createFixture();
let requestBody: Record<string, unknown> | undefined;
Expand Down
28 changes: 24 additions & 4 deletions App/backend/src/adapters/outbound/skill-writer/codex/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { resolveCodexHomeDirectory } from "../../agent-paths.js";
import { createNodeHookCommand } from "../hook-command.js";
import { isInstalledMemmyHookCurrent, memmyHookRevisionField } from "../hook-revision.js";
import { readMemmyMemoryServiceConfig } from "../memmy-runtime-config.js";
import {
removeMemmyResumeSkillDirectory,
Expand Down Expand Up @@ -91,6 +92,19 @@ export function createCodexSkillTarget(deps: CreateCodexSkillTargetDeps = {}): S
return (await readTextFile(join(root, TARGET_FILE_NAME))).includes(START_MARKER);
},

async isInstalledHookCurrent() {
const root = await this.resolveRootDirectory();
if (!root) return false;
const hookDirectory = join(root, HOOK_DIRECTORY_NAME);
return isInstalledMemmyHookCurrent({
source: CODEX_TARGET_ID,
mode: "codex",
hookScriptPath: join(hookDirectory, HOOK_SCRIPT_FILE_NAME),
bridgePath: join(hookDirectory, WORKSPACE_BRIDGE_FILE_NAME),
configPath: join(hookDirectory, HOOK_CONFIG_FILE_NAME)
});
},

async installPlugin(_targetId) {
const root = await this.resolveRootDirectory();
if (!root) {
Expand All @@ -99,11 +113,13 @@ export function createCodexSkillTarget(deps: CreateCodexSkillTargetDeps = {}): S

const hookDirectory = join(root, HOOK_DIRECTORY_NAME);
const hookScriptPath = join(hookDirectory, HOOK_SCRIPT_FILE_NAME);
const hookConfigPath = join(hookDirectory, HOOK_CONFIG_FILE_NAME);
const hookConfig = {
memmy_config_path: memmyConfigPath,
...(await readMemmyMemoryServiceConfig(memmyConfigPath))
};
await mkdir(hookDirectory, { recursive: true });
await writeFileAtomically(
join(hookDirectory, HOOK_CONFIG_FILE_NAME),
`${JSON.stringify({ memmy_config_path: memmyConfigPath, ...(await readMemmyMemoryServiceConfig(memmyConfigPath)) }, null, 2)}\n`
);
await writeFileAtomically(hookConfigPath, `${JSON.stringify(hookConfig, null, 2)}\n`);
await writeFileAtomically(hookScriptPath, renderMemmyResumeHookScript({ source: CODEX_TARGET_ID, mode: "codex" }));
await writeFileAtomically(
join(hookDirectory, WORKSPACE_BRIDGE_FILE_NAME),
Expand All @@ -127,6 +143,10 @@ export function createCodexSkillTarget(deps: CreateCodexSkillTargetDeps = {}): S
hooksFilePath,
hookCommand
});
await writeFileAtomically(
hookConfigPath,
`${JSON.stringify({ ...hookConfig, ...(await memmyHookRevisionField(CODEX_TARGET_ID, "codex")) }, null, 2)}\n`
);
},

async uninstallPlugin(_targetId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ describe("codex skill target", () => {
}
});

it("marks a hook without a stored revision as outdated and refreshes it in place", async () => {
const { rootDirectory, memmyConfigPath } = createFixture();
const target = createCodexSkillTarget({ rootDirectory, memmyConfigPath, trustHooks: noOpTrustHooks });
const configPath = join(rootDirectory, "hooks", "memmy-memory-config.json");

await target.installPlugin?.("codex");
const installed = JSON.parse(readFileSync(configPath, "utf8")) as { hook_revision?: string };
expect(installed.hook_revision).toEqual(expect.stringMatching(/^[a-f0-9]{64}$/));
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(true);

writeFileSync(configPath, `${JSON.stringify({ ...installed, hook_revision: "stale" }, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

writeFileSync(join(rootDirectory, "hooks", "memmy-resume-hook.mjs"), "old hook\n", "utf8");
writeFileSync(configPath, `${JSON.stringify(installed, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

await target.installPlugin?.("codex");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(true);
});

it("persists trust for the installed user-level hooks before installation completes", async () => {
const { rootDirectory, memmyConfigPath } = createFixture();
let trustOptions: TrustMemmyCodexHooksOptions | undefined;
Expand Down
26 changes: 22 additions & 4 deletions App/backend/src/adapters/outbound/skill-writer/cursor/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { createNodeHookCommand } from "../hook-command.js";
import { isInstalledMemmyHookCurrent, memmyHookRevisionField } from "../hook-revision.js";
import { readMemmyMemoryServiceConfig } from "../memmy-runtime-config.js";
import {
removeMemmyResumeSkillDirectory,
Expand Down Expand Up @@ -59,16 +60,29 @@ export function createCursorSkillTarget(deps: CreateCursorSkillTargetDeps = {}):
return (await readTextFile(join(cursorRootDirectory, "skills", "memmy-memory", "SKILL.md"))).includes("name: memmy-memory");
},

async isInstalledHookCurrent() {
const hookDirectory = join(cursorRootDirectory, HOOK_DIRECTORY_NAME);
return isInstalledMemmyHookCurrent({
source: CURSOR_TARGET_ID,
mode: "cursor",
hookScriptPath: join(hookDirectory, HOOK_SCRIPT_FILE_NAME),
bridgePath: join(hookDirectory, WORKSPACE_BRIDGE_FILE_NAME),
configPath: join(hookDirectory, HOOK_CONFIG_FILE_NAME)
});
},

async installPlugin(_targetId) {
await mkdir(cursorRootDirectory, { recursive: true });

const hookDirectory = join(cursorRootDirectory, HOOK_DIRECTORY_NAME);
const hookScriptPath = join(hookDirectory, HOOK_SCRIPT_FILE_NAME);
const hookConfigPath = join(hookDirectory, HOOK_CONFIG_FILE_NAME);
const hookConfig = {
memmy_config_path: memmyConfigPath,
...(await readMemmyMemoryServiceConfig(memmyConfigPath))
};
await mkdir(hookDirectory, { recursive: true });
await writeFileAtomically(
join(hookDirectory, HOOK_CONFIG_FILE_NAME),
`${JSON.stringify({ memmy_config_path: memmyConfigPath, ...(await readMemmyMemoryServiceConfig(memmyConfigPath)) }, null, 2)}\n`
);
await writeFileAtomically(hookConfigPath, `${JSON.stringify(hookConfig, null, 2)}\n`);
await writeFileAtomically(
hookScriptPath,
renderMemmyResumeHookScript({ source: CURSOR_TARGET_ID, mode: "cursor" })
Expand All @@ -83,6 +97,10 @@ export function createCursorSkillTarget(deps: CreateCursorSkillTargetDeps = {}):
const manifest = renderMemmyPluginSkillManifest(_targetId);
await replaceMemmySkillDirectory(cursorRootDirectory, manifest);
await replaceMemmyResumeSkillDirectory(cursorRootDirectory, CURSOR_TARGET_ID);
await writeFileAtomically(
hookConfigPath,
`${JSON.stringify({ ...hookConfig, ...(await memmyHookRevisionField(CURSOR_TARGET_ID, "cursor")) }, null, 2)}\n`
);
},

async uninstallPlugin(_targetId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,31 @@ describe("cursor skill target", () => {
}
});

it("marks a hook without a stored revision as outdated and refreshes it in place", async () => {
const { rootDirectory, memmyConfigPath } = createFixture();
const target = createCursorSkillTarget({ rootDirectory, memmyConfigPath });
const configPath = join(rootDirectory, "hooks", "memmy-memory-config.json");

await target.installPlugin?.("cursor");
const installed = JSON.parse(readFileSync(configPath, "utf8")) as { hook_revision?: string };
expect(installed.hook_revision).toEqual(expect.stringMatching(/^[a-f0-9]{64}$/));
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(true);

writeFileSync(configPath, `${JSON.stringify({ ...installed, hook_revision: "stale" }, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

writeFileSync(join(rootDirectory, "hooks", "memmy-resume-hook.mjs"), "old hook\n", "utf8");
writeFileSync(configPath, `${JSON.stringify(installed, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

delete installed.hook_revision;
writeFileSync(configPath, `${JSON.stringify(installed, null, 2)}\n`, "utf8");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(false);

await target.installPlugin?.("cursor");
await expect(target.isInstalledHookCurrent?.()).resolves.toBe(true);
});

it("installs a resume Skill and reads the current memmyMemory storage instead of legacy storage", async () => {
const { rootDirectory, memmyConfigPath } = createFixture();
let requestBody: Record<string, unknown> | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ describe("DeepSeek Harness skill target", () => {
const packageManifest = JSON.parse(readFileSync(packagePath, "utf8")) as Record<string, unknown>;
expect(packageManifest).toMatchObject({
name: "@memmy/memmy-memory",
version: "1.0.0",
type: "module",
exports: {
".": "./index.mjs",
"./client": "./client.js"
},
dsh: { client: { platform: "web" } }
dsh: { client: { platform: "web", inject: ["@deepseek-ai/dsh-client-ui-conversation"] } }
});
expect(packageManifest).not.toHaveProperty("version");
expect(readFileSync(skillPath, "utf8")).toContain('memmy-memory search "query text" --source deepseek_harness');
expect(readFileSync(resumeSkillPath, "utf8")).toContain("--source deepseek_harness");
expect(patch).toContain("id: user-plugin");
Expand Down Expand Up @@ -227,6 +227,29 @@ describe("DeepSeek Harness skill target", () => {
expect(definition?.kind).toBe("memmy-optimistic-user");
});

it("supports the transitional conversation.events registry", async () => {
const rootDirectory = createRoot();
const target = createDeepseekHarnessSkillTarget({ rootDirectory });
await target.installPlugin?.("deepseek_harness");
const clientPath = join(installedPluginDirectory(rootDirectory), "client.js");
let handoff: { id: string; factory(): Record<string, any> } | undefined;
runInNewContext(readFileSync(clientPath, "utf8"), {
window: { __ModuleLoader__: { load: (value: typeof handoff) => { handoff = value; } } }
});

let registered = 0;
const client = handoff?.factory();
client?.apply({
get(name: string) {
return name === "conversation"
? { events: { register: () => { registered += 1; } } }
: undefined;
}
});

expect(registered).toBe(1);
});

it("fails clearly when neither conversation event API is available", async () => {
const rootDirectory = createRoot();
const target = createDeepseekHarnessSkillTarget({ rootDirectory });
Expand Down Expand Up @@ -361,7 +384,7 @@ describe("DeepSeek Harness skill target", () => {
) as { messages: Array<{ source: { kind: string }; content: Array<{ text: string }> }> };

expect(decision.messages[0]).toBe(userMessage);
expect(decision.messages[1]?.source.kind).toBe("plugin");
expect(decision.messages[1]?.source.kind).toBe("memmy-memory");
expect(decision.messages[1]?.content[0]?.text).toContain("User prefers concise answers.");
expect(decision.messages[1]?.content[0]?.text).toContain("<current_user_request>\n检查 README");
expect(decision.messages[2]).toBe(runtimeContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** Hook revision tests. */
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { currentMemmyHookRevision, hookRevision, isInstalledMemmyHookCurrent } from "./hook-revision.js";

let tempDir: string | undefined;

afterEach(() => {
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = undefined;
}
});

describe("hook revision", () => {
it("changes when the hook script or workspace bridge changes", () => {
const current = hookRevision("hook script", "workspace bridge");

expect(hookRevision("hook script", "workspace bridge")).toBe(current);
expect(hookRevision("updated hook script", "workspace bridge")).not.toBe(current);
expect(hookRevision("hook script", "updated workspace bridge")).not.toBe(current);
});

it("treats a current fingerprint with leftover hook files as outdated", async () => {
tempDir = mkdtempSync(join(tmpdir(), "memmy-hook-revision-"));
const revision = await currentMemmyHookRevision("cursor", "cursor");
const hookScriptPath = join(tempDir, "memmy-resume-hook.mjs");
const bridgePath = join(tempDir, "memmy-workspace-bridge.mjs");
const configPath = join(tempDir, "memmy-memory-config.json");
writeFileSync(hookScriptPath, "old hook\n", "utf8");
writeFileSync(bridgePath, "old bridge\n", "utf8");
writeFileSync(configPath, `${JSON.stringify({ hook_revision: revision }, null, 2)}\n`, "utf8");

await expect(isInstalledMemmyHookCurrent({
source: "cursor",
mode: "cursor",
hookScriptPath,
bridgePath,
configPath
})).resolves.toBe(false);
});
});
Loading
Loading