From 9523d1328e9ff116500532f43d637c18d38141f6 Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Tue, 17 Mar 2026 10:38:45 +0900 Subject: [PATCH] =?UTF-8?q?fix(ext):=20workspaceUri=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=20+=20WS-only=20=EC=A0=84=EC=86=A1=20+=20user=20msg=20dedup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/src/extension.ts | 14 +++++++++---- extension/src/step-probe.ts | 39 +++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 86a8039..4259907 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -103,7 +103,7 @@ const recentDiscordSentTexts: Map = new Map(); function writeChatSnapshot(text: string) { try { - // WS route (preferred) + // WS route (preferred) — skip file write to prevent duplicate Discord delivery if (wsBridge && wsBridge.isConnected()) { wsBridge.sendChat({ content: text, @@ -111,8 +111,10 @@ function writeChatSnapshot(text: string) { project_name: projectName, }); logToFile(`[SNAPSHOT-WS] sent (${text.length} chars)`); + if (activeSessionId) { writeRegistration(activeSessionId); } + return; } - // File route (fallback / Phase 0 dual-write) + // File route (fallback — only when WS is NOT connected) const snapshotDir = path.join(bridgePath, 'chat_snapshots'); if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); } const id = Date.now().toString(); @@ -136,7 +138,7 @@ function writeChatSnapshot(text: string) { function writeChatSnapshotWithFiles(text: string, files: Array<{name: string, content: string}>) { try { - // WS route (preferred) + // WS route (preferred) — skip file write to prevent duplicate Discord delivery if (wsBridge && wsBridge.isConnected()) { wsBridge.sendChat({ content: text, @@ -145,8 +147,10 @@ function writeChatSnapshotWithFiles(text: string, files: Array<{name: string, co project_name: projectName, }); logToFile(`[SNAPSHOT-WS] sent with ${files.length} files (${text.length} chars)`); + if (activeSessionId) { writeRegistration(activeSessionId); } + return; } - // File route (fallback) + // File route (fallback — only when WS is NOT connected) const snapshotDir = path.join(bridgePath, 'chat_snapshots'); if (!fs.existsSync(snapshotDir)) { fs.mkdirSync(snapshotDir, { recursive: true }); } const id = Date.now().toString(); @@ -1141,6 +1145,8 @@ export async function activate(context: vscode.ExtensionContext) { sawRunningAfterPending, clickTrigger, logToFile, + workspaceUri, + diffReviewMetadata: new Map(), recentDiscordSentTexts, writeChatSnapshot, writeChatSnapshotWithFiles, diff --git a/extension/src/step-probe.ts b/extension/src/step-probe.ts index 44fafed..255dd58 100644 --- a/extension/src/step-probe.ts +++ b/extension/src/step-probe.ts @@ -684,12 +684,21 @@ function setupMonitor() { ctx.recentDiscordSentTexts.delete(trimmed); ctx.logToFile(`[USER-MSG] skipped echo relay (Discord origin, ${Math.round((Date.now()-sentAt)/1000)}s ago)`); } else if (umText.length > 2) { - const truncated = umText.length > 800 - ? umText.substring(0, 800) + '\n\n_(이하 생략)_' - : umText; - const source = isFromIDE ? 'AG 직접 입력' : 'API'; - ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`); - ctx.logToFile(`[USER-MSG] relayed ${umText.length} chars from step ${userInputIdx}`); + // Content-based dedup: AG can create multiple USER_INPUT steps for the same message + // (e.g. comment-while-working feature). Skip if same text relayed within 30s. + const dedupKey = `user_msg:${trimmed}`; + const lastRelayed = lastSnapshotText.get(dedupKey); + if (lastRelayed && (Date.now() - Number(lastRelayed)) < 30_000) { + ctx.logToFile(`[USER-MSG] skipped duplicate relay (same text ${Math.round((Date.now() - Number(lastRelayed))/1000)}s ago)`); + } else { + lastSnapshotText.set(dedupKey, String(Date.now())); + const truncated = umText.length > 800 + ? umText.substring(0, 800) + '\n\n_(이하 생략)_' + : umText; + const source = isFromIDE ? 'AG 직접 입력' : 'API'; + ctx.writeChatSnapshot(`👤 **사용자 (${source})**\n\n${truncated}`); + ctx.logToFile(`[USER-MSG] relayed ${umText.length} chars from step ${userInputIdx}`); + } } else { ctx.writeChatSnapshot(`👤 **사용자** — _(내용 없음)_`); ctx.logToFile(`[USER-MSG] step ${userInputIdx} text empty`); @@ -1220,7 +1229,7 @@ export function writePendingApproval(data: { conversation_id: string; command: s ...(data.modified_files ? { modified_files: data.modified_files } : {}), ...(data.edit_step_indices && data.edit_step_indices.length > 0 ? { edit_step_indices: data.edit_step_indices } : {}), }; - // WS route (preferred) — send pending to Hub before file write + // WS route (preferred) — skip file write to prevent duplicate Discord delivery if (ctx.wsBridge && ctx.wsBridge.isConnected()) { ctx.wsBridge.sendPending({ request_id: id, @@ -1234,8 +1243,22 @@ export function writePendingApproval(data: { conversation_id: string; command: s edit_step_indices: data.edit_step_indices, }); ctx.logToFile(`[PENDING-WS] sent pending ${id} cmd="${data.command.substring(0, 60)}"`); + // Cache diff_review metadata in-memory (needed for RPC acknowledgement) + if (data.step_type === 'diff_review' && (data.edit_step_indices?.length || data.modified_files?.length)) { + ctx.diffReviewMetadata.set(id, { + edit_step_indices: data.edit_step_indices || [], + modified_files: data.modified_files || [], + }); + ctx.logToFile(`[DIFF-REVIEW-CACHE] stored metadata for rid=${id}`); + } + // Record in memory dedup + if (data.step_index !== undefined && data.conversation_id) { + recentPendingSteps.set(`${data.conversation_id}:${data.step_index}`, nowMs); + } + if (data.conversation_id) { writeRegistration(data.conversation_id); } + return; } - // File route (fallback / Phase 0 dual-write) + // File route (fallback — only when WS is NOT connected) fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8'); console.log(`Gravity Bridge: pending approval written → ${id}.json`); // Cache diff_review metadata in-memory (survives pending file deletion by Collector/Bot)