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
17 changes: 17 additions & 0 deletions .github/release-notes/v1.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Memmy v1.2.0

## Highlights

- Updated the DeepSeek Harness integration to mark recalled context with the current `memmy-memory` source and consume completed `assistant/message` events.
- Synchronized Memmy Desktop and Agent release metadata to version `1.2.0`.

## Agent and Desktop improvements

- Kept DeepSeek Harness recall and completed-turn capture aligned with the current event and message schemas.
- Added regression coverage for the generated DeepSeek Harness template and release version metadata.

## Upgrade notes

- Memmy Desktop and Agent are version `1.2.0`.
- The bundled Memory service, viewer, and CLI remain independently versioned.
- Memory protocol version remains `1`.
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" } }
});
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 @@ -361,7 +361,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
Expand Up @@ -76,7 +76,7 @@ export function apply(ctx, config = {}) {
pendingL3.delete(String(payload.agent.session.id));
if (!markdown && !l3) return decision;
const memory = createUserMessage({
source: { kind: "plugin", plugin: name, form: "recall" },
source: { kind: "memmy-memory", form: "recall" },
content: [{ type: "text", text: [l3, markdown ? renderMemoryPacket(markdown, "turn_start", query) : ""].filter(Boolean).join("\n\n") }]
});
return { ...decision, messages: insertAfterUserMessage(decision.messages, memory) };
Expand Down Expand Up @@ -653,6 +653,7 @@ export const DEEPSEEK_HARNESS_PLUGIN_CLIENT = String.raw`window.__ModuleLoader__
export function createDeepseekHarnessPluginPackageManifest(): Record<string, unknown> {
return {
name: "@memmy/memmy-memory",
version: "1.0.0",
private: true,
type: "module",
exports: {
Expand Down
2 changes: 1 addition & 1 deletion App/backend/src/project-version.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/** Generated from the root package.json by scripts/sync-project-version.mjs. */
export const MEMMY_VERSION = "1.1.9";
export const MEMMY_VERSION = "1.2.0";
2 changes: 1 addition & 1 deletion App/backend/src/tests/project-version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe("project version", () => {
);

expect(MEMMY_VERSION).toBe(rootManifest.version);
expect(MEMMY_VERSION).toBe("1.1.9");
expect(MEMMY_VERSION).toBe("1.2.0");
expect(MEMMY_VERSION).toMatch(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/);
expect(readFileSync(resolve(repoRoot, "App/backend/src/project-version.ts"), "utf8"))
.toContain(`MEMMY_VERSION = ${JSON.stringify(rootManifest.version)}`);
Expand Down
4 changes: 2 additions & 2 deletions App/memmy-agent/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion App/memmy-agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "memmy-agent",
"version": "1.1.9",
"version": "1.2.0",
"description": "TypeScript refactor of memmy's agent runtime.",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion App/shell/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@memmy/desktop",
"version": "1.1.9",
"version": "1.2.0",
"private": true,
"type": "module",
"description": "Memmy desktop client.",
Expand Down
6 changes: 3 additions & 3 deletions Memory/adapters/dsh/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,17 @@ export async function apply(ctx, config) {
if (query) {
const started = await request("/turns/start", profileId, { sessionId, query }, config.recallTimeoutMs);
turns.set(String(agent?.id || "default"), { sessionId, query, turnId: started.turnId });
if (started.injectedContext && Array.isArray(payload?.messages)) payload.messages.push({ role: "user", content: [{ type: "text", text: started.injectedContext }], source: { kind: "plugin", plugin: name, form: "recall" } });
if (started.injectedContext && Array.isArray(payload?.messages)) payload.messages.push({ role: "user", content: [{ type: "text", text: started.injectedContext }], source: { kind: "memmy-memory", form: "recall" } });
}
} catch (error) { ctx.logger.warn(`memmy-memory recall unavailable: ${String(error)}`); }
return next();
}));
disposers.push(ctx.on("session/event", (session, event) => {
if (!config.captureEnabled || event?.type !== "assistant") return;
if (!config.captureEnabled || event?.type !== "assistant/message") return;
const active = turns.get(String(session?.id || "default"));
if (!active) return;
turns.delete(String(session?.id || "default"));
const answer = String(event?.message?.content?.map?.((part) => part.text || "").join("\n") || event?.content || "");
const answer = String(event?.data?.message?.content?.map?.((part) => part.text || "").join("\n") || "");
void request(`/turns/${encodeURIComponent(active.turnId)}/complete`, profileId, { sessionId: active.sessionId, query: active.query, answer, status: "succeeded" }, 10000).catch(() => undefined);
}));
disposers.push(ctx.on("session/disposed", (session) => { const key = String(session?.id || "default"); const id = sessions.get(key); sessions.delete(key); if (id) void request(`/sessions/${encodeURIComponent(id)}/close`, profileId, {}).catch(() => undefined); }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function apply(ctx, config = {}) {
pendingL3.delete(String(payload.agent.session.id));
if (!markdown && !l3) return decision;
const memory = createUserMessage({
source: { kind: "plugin", plugin: name, form: "recall" },
source: { kind: "memmy-memory", form: "recall" },
content: [{ type: "text", text: [l3, markdown ? renderMemoryPacket(markdown, "turn_start", query) : ""].filter(Boolean).join("\n\n") }]
});
return { ...decision, messages: insertAfterUserMessage(decision.messages, memory) };
Expand Down Expand Up @@ -653,6 +653,7 @@ export const DEEPSEEK_HARNESS_PLUGIN_CLIENT = String.raw`window.__ModuleLoader__
export function createDeepseekHarnessPluginPackageManifest(): Record<string, unknown> {
return {
name: "@memmy/memmy-memory",
version: "1.0.0",
private: true,
type: "module",
exports: {
Expand Down
13 changes: 13 additions & 0 deletions Memory/tests/deepseek-harness-template.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { DEEPSEEK_HARNESS_PLUGIN_INDEX } from "../src/agent-source/integration/templates/memmy-deepseek-harness-plugin.js";

describe("DeepSeek Harness plugin template", () => {
it("marks recalled context as Memmy memory", () => {
expect(DEEPSEEK_HARNESS_PLUGIN_INDEX).toContain(
'source: { kind: "memmy-memory", form: "recall" }'
);
expect(DEEPSEEK_HARNESS_PLUGIN_INDEX).not.toContain(
'source: { kind: "plugin", plugin: name, form: "recall" }'
);
});
});
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "memmy-agent",
"version": "1.1.9",
"version": "1.2.0",
"private": true,
"type": "module",
"description": "Local-first agent memory substrate with desktop and CLI surfaces.",
Expand Down
42 changes: 41 additions & 1 deletion tests/smoke/memory-layer-smoke-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
DEFAULT_MEMMY_CONFIG,
MemoryDb,
MemoryService,
type Embedder
type Embedder,
type LlmClient
} from "../../Memory/src/index.js";

const tempRoots: string[] = [];
Expand Down Expand Up @@ -45,6 +46,7 @@ describe("memory layer smoke plan", () => {
db,
mode: "dev",
config: DEFAULT_MEMMY_CONFIG,
llm: createSmokeSummaryLlm(),
embedder: createSmokeEmbedder(),
fetchAppMemoryBudget: async () => null
});
Expand Down Expand Up @@ -200,6 +202,44 @@ function createSmokeEmbedder(): Embedder {
};
}

function createSmokeSummaryLlm(): LlmClient {
return {
config: {
...DEFAULT_MEMMY_CONFIG.summary,
provider: "host",
endpoint: "http://127.0.0.1/summary",
model: "smoke-summary"
},
isConfigured: () => true,
async complete() {
return JSON.stringify({ title: "Smoke turn", summary: "Verified smoke summary" });
},
async completeJson<T extends Record<string, unknown>>(messages) {
const system = messages
.filter((message) => message.role === "system")
.map((message) => message.content)
.join("\n");
if (system.includes("Judge L1 and User Memory independently")) {
return {
l1: {
title: "Release verification",
summary: "Verified release contracts and attachments.",
evidence: [{ quote: "release workflow", role: "user", kind: "task_request" }]
},
user: null
} as unknown as T;
}
return { title: "Smoke turn", summary: "Verified smoke summary" } as unknown as T;
},
status: () => ({
provider: "host",
model: "smoke-summary",
configured: true,
remote: true
})
};
}

function captureFetch(requests: CapturedRequest[], responseBody: unknown): typeof fetch {
return (async (input: string | URL | Request, init?: RequestInit) => {
const rawUrl = input instanceof Request ? input.url : String(input);
Expand Down
Loading