From 1987ce3cbe6c19fff950a2bae606738a1b648dfb Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 30 Mar 2026 20:17:54 +0100 Subject: [PATCH 1/3] [#634] Await agent DB writes, handle 23505 conflicts, add cache retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentRegister: await DB write, show warning if cache fails - AgentManage: await all three DB writes (URI update, wallet bind, wallet unbind), show warning on failure - agent-register route: handle 23505 unique violation by re-querying and updating the conflicting row - agents/page.tsx + AgentDashboard: retry cacheAgentById once on failure On-chain success is never blocked by DB failure — failures show a user-friendly warning instead of silent swallowing. Fixes realproject7/plotlink#634 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/agents/page.tsx | 4 +- src/app/api/user/agent-register/route.ts | 18 +++++- src/components/AgentDashboard.tsx | 4 +- src/components/AgentManage.tsx | 82 +++++++++++++++--------- src/components/AgentRegister.tsx | 33 ++++++---- 5 files changed, 93 insertions(+), 48 deletions(-) diff --git a/src/app/agents/page.tsx b/src/app/agents/page.tsx index 8176508e..cc86517f 100644 --- a/src/app/agents/page.tsx +++ b/src/app/agents/page.tsx @@ -93,7 +93,9 @@ export default function AgentsPage() { useEffect(() => { if (!dbDetected && hasExistingAgent && address && detectedAgentId && !cachedRef.current) { cachedRef.current = true; - cacheAgentById(address, detectedAgentId.toString()).catch(() => {}); + cacheAgentById(address, detectedAgentId.toString()).catch(() => + cacheAgentById(address, detectedAgentId.toString()).catch(() => {}), + ); } }, [dbDetected, hasExistingAgent, address, detectedAgentId]); diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index 000ac66c..a62b7c32 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -63,10 +63,26 @@ export async function POST(request: NextRequest) { if (existingUser) { await supabase.from("users").update(agentFields).eq("id", existingUser.id); } else { - await supabase.from("users").insert({ + const { error: insertError } = await supabase.from("users").insert({ primary_address: normalized, ...agentFields, }); + + // 23505: row was created concurrently — update it instead + if (insertError?.code === "23505") { + const { data: raceUser } = await supabase + .from("users") + .select("id") + .or(`primary_address.eq.${normalized},agent_wallet.eq.${normalized},agent_owner.eq.${normalized}`) + .limit(1) + .single(); + + if (raceUser) { + await supabase.from("users").update(agentFields).eq("id", raceUser.id); + } + } else if (insertError) { + throw insertError; + } } return NextResponse.json({ ok: true }); diff --git a/src/components/AgentDashboard.tsx b/src/components/AgentDashboard.tsx index d08c3d16..955f31f5 100644 --- a/src/components/AgentDashboard.tsx +++ b/src/components/AgentDashboard.tsx @@ -103,7 +103,9 @@ export function AgentDashboard() { useEffect(() => { if (!dbDetected && isAgent && address && agentId && !cachedRef.current) { cachedRef.current = true; - cacheAgentById(address, agentId.toString()).catch(() => {}); + cacheAgentById(address, agentId.toString()).catch(() => + cacheAgentById(address, agentId.toString()).catch(() => {}), + ); } }, [dbDetected, isAgent, address, agentId]); diff --git a/src/components/AgentManage.tsx b/src/components/AgentManage.tsx index fef77471..b87918ce 100644 --- a/src/components/AgentManage.tsx +++ b/src/components/AgentManage.tsx @@ -164,21 +164,27 @@ export function AgentManage({ agentId, role }: AgentManageProps) { await publicClient.waitForTransactionReceipt({ hash }); const parsed = JSON.parse(editUri); setMetadata({ ...metadata!, ...parsed }); - setSuccessMessage("Agent profile updated"); // Persist URI update to DB - fetch("/api/user/agent-update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - walletAddress: address, - fields: { - agent_name: parsed.name, - agent_description: parsed.description, - agent_genre: parsed.genre || null, - agent_llm_model: parsed.llmModel || null, - }, - }), - }).catch(() => {}); + try { + const cacheRes = await fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { + agent_name: parsed.name, + agent_description: parsed.description, + agent_genre: parsed.genre || null, + agent_llm_model: parsed.llmModel || null, + }, + }), + }); + setSuccessMessage(cacheRes.ok + ? "Agent profile updated" + : "On-chain OK, but cache failed — will sync on next visit"); + } catch { + setSuccessMessage("On-chain OK, but cache failed — will sync on next visit"); + } setEditing(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update URI"); @@ -200,16 +206,22 @@ export function AgentManage({ agentId, role }: AgentManageProps) { }); setTxHash(hash); await publicClient.waitForTransactionReceipt({ hash }); - setSuccessMessage("Agent wallet removed"); // Persist unset to DB - fetch("/api/user/agent-update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - walletAddress: address, - fields: { agent_wallet: null }, - }), - }).catch(() => {}); + try { + const cacheRes = await fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { agent_wallet: null }, + }), + }); + setSuccessMessage(cacheRes.ok + ? "Agent wallet removed" + : "On-chain OK, but cache failed — will sync on next visit"); + } catch { + setSuccessMessage("On-chain OK, but cache failed — will sync on next visit"); + } } catch (err) { setError(err instanceof Error ? err.message : "Failed to unset wallet"); } finally { @@ -260,15 +272,21 @@ export function AgentManage({ agentId, role }: AgentManageProps) { setTxHash(hash); await publicClient.waitForTransactionReceipt({ hash }); // Persist new wallet binding to DB - fetch("/api/user/agent-update", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - walletAddress: address, - fields: { agent_wallet: newWalletAddr.toLowerCase() }, - }), - }).catch(() => {}); - setSuccessMessage("Agent wallet bound successfully"); + try { + const cacheRes = await fetch("/api/user/agent-update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + fields: { agent_wallet: newWalletAddr.toLowerCase() }, + }), + }); + setSuccessMessage(cacheRes.ok + ? "Agent wallet bound successfully" + : "On-chain OK, but cache failed — will sync on next visit"); + } catch { + setSuccessMessage("On-chain OK, but cache failed — will sync on next visit"); + } setWalletStep(null); setChangingWallet(false); setNewWalletAddr(""); diff --git a/src/components/AgentRegister.tsx b/src/components/AgentRegister.tsx index 1d093410..eacaadba 100644 --- a/src/components/AgentRegister.tsx +++ b/src/components/AgentRegister.tsx @@ -102,19 +102,26 @@ export function AgentRegister() { setOwnerAddress(address); // Persist agent data to DB const meta = JSON.parse(agentURI); - fetch("/api/user/agent-register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - walletAddress: address, - agentId: newAgentId?.toString(), - name: meta.name, - description: meta.description, - genre: meta.genre, - llmModel: meta.llmModel, - agentOwner: address, - }), - }).catch(() => {}); + try { + const cacheRes = await fetch("/api/user/agent-register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + walletAddress: address, + agentId: newAgentId?.toString(), + name: meta.name, + description: meta.description, + genre: meta.genre, + llmModel: meta.llmModel, + agentOwner: address, + }), + }); + if (!cacheRes.ok) { + setError("On-chain OK, but cache failed — will sync on next visit"); + } + } catch { + setError("On-chain OK, but cache failed — will sync on next visit"); + } setStep("3a"); } catch (err) { setError(err instanceof Error ? err.message : "Registration failed"); From ed2ec16cfdec9dc25a82ec78e39126321f480385 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 30 Mar 2026 20:20:29 +0100 Subject: [PATCH 2/3] [#634] Check Supabase update errors and return non-2xx on failure Both agent-register and agent-update routes now check the error from Supabase update/insert calls and return 500 on failure, so the client-side warning UI can actually trigger. Addresses T2a review feedback on PR #651. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/user/agent-register/route.ts | 12 +++++++++--- src/app/api/user/agent-update/route.ts | 6 +++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index a62b7c32..235a26a3 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -61,7 +61,10 @@ export async function POST(request: NextRequest) { }; if (existingUser) { - await supabase.from("users").update(agentFields).eq("id", existingUser.id); + const { error: updateError } = await supabase.from("users").update(agentFields).eq("id", existingUser.id); + if (updateError) { + return NextResponse.json({ error: updateError.message }, { status: 500 }); + } } else { const { error: insertError } = await supabase.from("users").insert({ primary_address: normalized, @@ -78,10 +81,13 @@ export async function POST(request: NextRequest) { .single(); if (raceUser) { - await supabase.from("users").update(agentFields).eq("id", raceUser.id); + const { error: updateError } = await supabase.from("users").update(agentFields).eq("id", raceUser.id); + if (updateError) { + return NextResponse.json({ error: updateError.message }, { status: 500 }); + } } } else if (insertError) { - throw insertError; + return NextResponse.json({ error: insertError.message }, { status: 500 }); } } diff --git a/src/app/api/user/agent-update/route.ts b/src/app/api/user/agent-update/route.ts index 32f76f76..13a71545 100644 --- a/src/app/api/user/agent-update/route.ts +++ b/src/app/api/user/agent-update/route.ts @@ -77,7 +77,11 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } - await supabase.from("users").update(sanitized).eq("id", existingUser.id); + const { error: updateError } = await supabase.from("users").update(sanitized).eq("id", existingUser.id); + + if (updateError) { + return NextResponse.json({ error: updateError.message }, { status: 500 }); + } return NextResponse.json({ ok: true }); } catch (err) { From 12391ff1c6684b19b940a08fb5b6b86f79de4123 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 30 Mar 2026 20:22:14 +0100 Subject: [PATCH 3/3] [#634] Return 500 when 23505 conflict retry cannot find row If the concurrent insert conflict re-query finds no matching row, return 500 instead of falling through to { ok: true }. Addresses T2a review feedback on PR #651. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/user/agent-register/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/api/user/agent-register/route.ts b/src/app/api/user/agent-register/route.ts index 235a26a3..6e1c38fc 100644 --- a/src/app/api/user/agent-register/route.ts +++ b/src/app/api/user/agent-register/route.ts @@ -85,6 +85,8 @@ export async function POST(request: NextRequest) { if (updateError) { return NextResponse.json({ error: updateError.message }, { status: 500 }); } + } else { + return NextResponse.json({ error: "Conflict but user not found on retry" }, { status: 500 }); } } else if (insertError) { return NextResponse.json({ error: insertError.message }, { status: 500 });