diff --git a/next.config.mjs b/next.config.mjs
index b1e835a4ae..79348ec773 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -67,6 +67,7 @@ const nextConfig = {
"tls-client-node",
"koffi",
"tough-cookie",
+ "@ngrok/ngrok",
"child_process",
"fs",
"path",
@@ -100,6 +101,12 @@ const nextConfig = {
contextRegExp: /thread-stream/,
})
);
+
+ // Mark @ngrok/ngrok as external to prevent webpack from trying to bundle its .node binaries
+ config.externals = config.externals || [];
+ config.externals.push({
+ "@ngrok/ngrok": "commonjs @ngrok/ngrok",
+ });
// ── Turbopack / Next.js 16 module-hash patch (#394, #396, #398) ────────
//
// Next.js 16 (with or without Turbopack) compiles the instrumentation hook
diff --git a/open-sse/services/antigravityHeaders.ts b/open-sse/services/antigravityHeaders.ts
index 3e6b632337..4b38e90cf2 100644
--- a/open-sse/services/antigravityHeaders.ts
+++ b/open-sse/services/antigravityHeaders.ts
@@ -20,7 +20,8 @@ export const GEMINI_CLI_VERSION = "0.39.1";
export const GEMINI_SDK_VERSION = "1.30.0";
export const NODE_VERSION = "v22.21.1";
export const ANTIGRAVITY_LOAD_CODE_ASSIST_USER_AGENT = "google-api-nodejs-client/10.3.0";
-export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1";
+export const ANTIGRAVITY_LOAD_CODE_ASSIST_API_CLIENT =
+ "google-cloud-sdk vscode_cloudshelleditor/0.1";
const LOAD_CODE_ASSIST_METADATA = Object.freeze({
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
@@ -133,6 +134,4 @@ export function googApiClientHeader(): string {
return `google-genai-sdk/${GEMINI_SDK_VERSION} gl-node/${NODE_VERSION}`;
}
-export {
- ANTIGRAVITY_VERSION,
-};
+export { ANTIGRAVITY_VERSION };
diff --git a/package-lock.json b/package-lock.json
index a22ea08b6e..17ae67cb5c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2447,9 +2447,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2466,9 +2463,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2485,9 +2479,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2504,9 +2495,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
diff --git a/src/app/(dashboard)/dashboard/memory/page.tsx b/src/app/(dashboard)/dashboard/memory/page.tsx
index 526cc9debe..885727721a 100644
--- a/src/app/(dashboard)/dashboard/memory/page.tsx
+++ b/src/app/(dashboard)/dashboard/memory/page.tsx
@@ -205,17 +205,21 @@ export default function MemoryPage() {
{health !== null && (
)}
{health === null && !checkingHealth && (
)}
@@ -321,7 +325,7 @@ export default function MemoryPage() {
- Page {page} of {totalPages} ({total} total)
+ {t("pageInfo", { page, totalPages, total })}
@@ -356,47 +360,47 @@ export default function MemoryPage() {
onClick={() => setAddDialogOpen(false)}
disabled={isSubmitting}
>
- Cancel
+ {t("cancel")}
>
}
>
-
+
-
+
setNewMemory({ ...newMemory, key: e.target.value })}
- placeholder="e.g., user_preference_theme"
+ placeholder={t("keyPlaceholder")}
className="w-full"
/>
-
+
setNewMemory({ ...newMemory, content: e.target.value })}
- placeholder="e.g., Prefers dark mode"
+ placeholder={t("contentPlaceholder")}
className="w-full"
/>
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index 3f7f512e60..43ffa88793 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -1335,13 +1335,17 @@
}
}
},
- "autoConfiguredTab": "Auto Configured Tab",
- "toolCategoriesDesc": "Tool Categories Desc",
- "allToolsTab": "All Tools Tab",
- "guidedClientsTab": "Guided Clients Tab",
- "mitmClientsTab": "Mitm Clients Tab",
- "toolCategories": "Tool Categories",
- "visibleToolsCount": "Visible Tools Count"
+ "autoConfiguredTab": "自动配置",
+ "toolCategoriesDesc": "配置 AI 编程助手通过 OmniRoute 路由",
+ "allToolsTab": "所有工具",
+ "guidedClientsTab": "引导客户端",
+ "mitmClientsTab": "MITM 客户端",
+ "customCliTab": "自定义 CLI",
+ "toolCategories": "工具分类",
+ "visibleToolsCount": "{count} 个工具可用",
+ "installationGuide": "安装指南",
+ "whenToUseLabel": "何时使用",
+ "openToolDocs": "打开工具文档"
},
"combos": {
"title": "组合",
@@ -4324,7 +4328,9 @@
"addMemory": "添加记忆",
"type": "类型",
"key": "键",
+ "keyPlaceholder": "例如:user_preference_theme",
"content": "内容",
+ "contentPlaceholder": "例如:偏好深色模式",
"created": "创建时间",
"actions": "操作",
"delete": "删除",
@@ -4332,7 +4338,16 @@
"episodic": "情景型",
"procedural": "程序型",
"semantic": "语义型",
- "a": "A"
+ "previous": "上一页",
+ "next": "下一页",
+ "pageInfo": "第 {page} 页,共 {totalPages} 页(共 {total} 条)",
+ "checkingHealth": "检查中...",
+ "checkHealth": "检查健康",
+ "pipelineOk": "Pipeline 正常 ({latencyMs}ms)",
+ "pipelineError": "Pipeline 错误",
+ "healthUnknown": "健康状态未知",
+ "cancel": "取消",
+ "save": "保存"
},
"skills": {
"title": "技能",
diff --git a/src/lib/memory/store.ts b/src/lib/memory/store.ts
index df9f554f55..e724a0f2cf 100644
--- a/src/lib/memory/store.ts
+++ b/src/lib/memory/store.ts
@@ -77,15 +77,78 @@ function rowToMemory(row: MemoryRow): Memory {
}
/**
- * Create a new memory entry
+ * Find existing memory by apiKeyId and key (for UPSERT logic)
+ */
+function findExistingMemory(
+ db: ReturnType
,
+ apiKeyId: string,
+ key: string
+): MemoryRow | undefined {
+ if (!key) return undefined;
+ const stmt = db.prepare(
+ "SELECT * FROM memories WHERE api_key_id = ? AND key = ? ORDER BY created_at DESC LIMIT 1"
+ );
+ return stmt.get(apiKeyId, key) as MemoryRow | undefined;
+}
+
+/**
+ * Create a new memory entry (UPSERT: updates existing if same apiKeyId + key)
*/
export async function createMemory(
memory: Omit
): Promise {
const db = getDbInstance();
- const id = crypto.randomUUID();
const now = new Date().toISOString();
+ // Check for existing memory with same apiKeyId + key (UPSERT logic)
+ const existing = memory.key ? findExistingMemory(db, memory.apiKeyId, memory.key) : undefined;
+
+ if (existing) {
+ // UPDATE existing record
+ const updatedMetadata = { ...parseJSON(existing.metadata), ...memory.metadata };
+ const stmt = db.prepare(
+ "UPDATE memories SET content = ?, metadata = ?, updated_at = ?, session_id = ?, type = ?, expires_at = ? WHERE id = ?"
+ );
+ stmt.run(
+ memory.content,
+ JSON.stringify(updatedMetadata),
+ now,
+ memory.sessionId,
+ memory.type,
+ memory.expiresAt ?? null,
+ existing.id
+ );
+
+ const updatedMemory: Memory = {
+ id: String(existing.id),
+ apiKeyId: memory.apiKeyId,
+ sessionId: memory.sessionId,
+ type: memory.type,
+ key: memory.key,
+ content: memory.content,
+ metadata: updatedMetadata,
+ createdAt: new Date(String(existing.created_at)),
+ updatedAt: new Date(now),
+ expiresAt: memory.expiresAt ?? null,
+ };
+
+ // Invalidate and update cache
+ invalidateMemoryCache(existing.id);
+ evictIfNeeded(_memoryCache);
+ _memoryCache.set(existing.id, { value: updatedMemory, timestamp: Date.now() });
+
+ log.info("memory.updated", {
+ apiKeyId: memory.apiKeyId,
+ type: memory.type,
+ id: existing.id,
+ key: memory.key,
+ });
+
+ return updatedMemory;
+ }
+
+ // INSERT new record if not exists
+ const id = crypto.randomUUID();
const stmt = db.prepare(
"INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
diff --git a/tests/unit/memory-store.test.ts b/tests/unit/memory-store.test.ts
index de7009b5be..c86de71d37 100644
--- a/tests/unit/memory-store.test.ts
+++ b/tests/unit/memory-store.test.ts
@@ -177,18 +177,21 @@ test("listMemories filters by api key, type and session while preserving newest-
test("listMemories supports limit and offset pagination even when only offset is provided", async () => {
insertMemoryRow({
id: "page-1",
+ key: "pagination:1",
content: "oldest",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
});
insertMemoryRow({
id: "page-2",
+ key: "pagination:2",
content: "middle",
createdAt: "2026-04-02T00:00:00.000Z",
updatedAt: "2026-04-02T00:00:00.000Z",
});
insertMemoryRow({
id: "page-3",
+ key: "pagination:3",
content: "newest",
createdAt: "2026-04-03T00:00:00.000Z",
updatedAt: "2026-04-03T00:00:00.000Z",
@@ -251,18 +254,21 @@ test("listMemories applies query filtering before pagination and type stats", as
test("listMemories supports page-based pagination (page 1)", async () => {
insertMemoryRow({
id: "pg-1",
+ key: "page:test:1",
content: "first",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
});
insertMemoryRow({
id: "pg-2",
+ key: "page:test:2",
content: "second",
createdAt: "2026-04-02T00:00:00.000Z",
updatedAt: "2026-04-02T00:00:00.000Z",
});
insertMemoryRow({
id: "pg-3",
+ key: "page:test:3",
content: "third",
createdAt: "2026-04-03T00:00:00.000Z",
updatedAt: "2026-04-03T00:00:00.000Z",
@@ -279,18 +285,21 @@ test("listMemories supports page-based pagination (page 1)", async () => {
test("listMemories supports page-based pagination (page 2 returns remainder)", async () => {
insertMemoryRow({
id: "pg-1",
+ key: "page:test:1",
content: "first",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
});
insertMemoryRow({
id: "pg-2",
+ key: "page:test:2",
content: "second",
createdAt: "2026-04-02T00:00:00.000Z",
updatedAt: "2026-04-02T00:00:00.000Z",
});
insertMemoryRow({
id: "pg-3",
+ key: "page:test:3",
content: "third",
createdAt: "2026-04-03T00:00:00.000Z",
updatedAt: "2026-04-03T00:00:00.000Z",
@@ -307,6 +316,7 @@ test("listMemories supports page-based pagination (page 2 returns remainder)", a
test("listMemories returns empty data for a page beyond the result set", async () => {
insertMemoryRow({
id: "pg-1",
+ key: "page:test:1",
content: "only entry",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
@@ -320,12 +330,14 @@ test("listMemories returns empty data for a page beyond the result set", async (
test("listMemories page parameter defaults to page 1 when omitted with limit", async () => {
insertMemoryRow({
id: "pg-1",
+ key: "page:test:1",
content: "first",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
});
insertMemoryRow({
id: "pg-2",
+ key: "page:test:2",
content: "second",
createdAt: "2026-04-02T00:00:00.000Z",
updatedAt: "2026-04-02T00:00:00.000Z",