fix: resolve build issues and implement memory UPSERT logic (#1763)

* fix: resolve build issues for v3.7.5 and apply memory/translation fixes

1. antigravityHeaders.ts: restore ANTIGRAVITY_LOAD_CODE_ASSIST_* exports for oauth.ts compatibility
2. next.config.mjs: add @ngrok/ngrok to serverExternalPackages and webpack externals to handle native .node modules
3. Memory system: UPSERT logic to prevent duplicate entries with same apiKeyId + key
4. Chinese translations: complete CLI tools and memory dashboard localizations
5. Test fixes: unique keys for pagination tests to comply with unique constraint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address Gemini Code Assist review feedback

1. store.ts: add expires_at to UPDATE statement in UPSERT logic
   - Previously, expires_at was not being persisted to database on update
   - This caused state mismatch between returned Memory object and actual DB row

2. package-lock.json: revert react-markdown registry to official npmjs.org
   - Mirror-specific registry URL (npmmirror.com) should not be in lockfile

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
clousky2020
2026-04-29 19:53:02 +08:00
committed by GitHub
parent 3c63566d57
commit 2aaba386ec
7 changed files with 131 additions and 43 deletions

View File

@@ -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

View File

@@ -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 };

12
package-lock.json generated
View File

@@ -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": [

View File

@@ -205,17 +205,21 @@ export default function MemoryPage() {
{health !== null && (
<span
className={`inline-block w-3 h-3 rounded-full ${health.working ? "bg-green-500" : "bg-red-500"}`}
title={health.working ? `Pipeline OK (${health.latencyMs}ms)` : "Pipeline error"}
title={
health.working
? t("pipelineOk", { latencyMs: health.latencyMs })
: t("pipelineError")
}
/>
)}
{health === null && !checkingHealth && (
<span
className="inline-block w-3 h-3 rounded-full bg-gray-400"
title="Health unknown"
title={t("healthUnknown")}
/>
)}
<Button variant="outline" size="sm" onClick={checkHealth} disabled={checkingHealth}>
{checkingHealth ? "Checking..." : "Check Health"}
{checkingHealth ? t("checkingHealth") : t("checkHealth")}
</Button>
</div>
</div>
@@ -321,7 +325,7 @@ export default function MemoryPage() {
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-gray-500">
Page {page} of {totalPages} ({total} total)
{t("pageInfo", { page, totalPages, total })}
</div>
<div className="flex gap-2">
<Button
@@ -330,7 +334,7 @@ export default function MemoryPage() {
disabled={page === 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
Previous
{t("previous")}
</Button>
<Button
variant="outline"
@@ -338,7 +342,7 @@ export default function MemoryPage() {
disabled={page >= totalPages}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
>
Next
{t("next")}
</Button>
</div>
</div>
@@ -356,47 +360,47 @@ export default function MemoryPage() {
onClick={() => setAddDialogOpen(false)}
disabled={isSubmitting}
>
Cancel
{t("cancel")}
</Button>
<Button
onClick={handleAddMemory}
loading={isSubmitting}
disabled={!newMemory.key || !newMemory.content}
>
Save
{t("save")}
</Button>
</>
}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Type</label>
<label className="block text-sm font-medium mb-1">{t("type")}</label>
<Select
value={newMemory.type}
onChange={(e) => setNewMemory({ ...newMemory, type: e.target.value as any })}
className="w-full"
>
<option value="factual">Factual</option>
<option value="episodic">Episodic</option>
<option value="procedural">Procedural</option>
<option value="semantic">Semantic</option>
<option value="factual">{t("factual")}</option>
<option value="episodic">{t("episodic")}</option>
<option value="procedural">{t("procedural")}</option>
<option value="semantic">{t("semantic")}</option>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Key</label>
<label className="block text-sm font-medium mb-1">{t("key")}</label>
<Input
value={newMemory.key}
onChange={(e) => setNewMemory({ ...newMemory, key: e.target.value })}
placeholder="e.g., user_preference_theme"
placeholder={t("keyPlaceholder")}
className="w-full"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Content</label>
<label className="block text-sm font-medium mb-1">{t("content")}</label>
<Input
value={newMemory.content}
onChange={(e) => setNewMemory({ ...newMemory, content: e.target.value })}
placeholder="e.g., Prefers dark mode"
placeholder={t("contentPlaceholder")}
className="w-full"
/>
</div>

View File

@@ -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": "技能",

View File

@@ -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<typeof getDbInstance>,
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<Memory, "id" | "createdAt" | "updatedAt">
): Promise<Memory> {
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"

View File

@@ -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",