From 75d5f7f23075fd4fa87e1c873d03ec3c1bb0c005 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 7 Mar 2026 14:36:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8D=E5=8C=B9=E9=85=8D=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=8F=90=E4=BE=9B=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=8B=E5=A5=BD=E7=9A=84=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=20fix:=20=E6=9B=B4=E6=96=B0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E7=B1=BB=E5=9E=8B=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=88=B3=E4=BD=BF=E7=94=A8REAL=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=20refactor:=20=E4=BC=98=E5=8C=96DeepWiki=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=92=8C=E5=AD=98=E5=82=A8=E7=9A=84=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=A4=84=E7=90=86=20refactor:=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=97=A0=E7=94=A8=E7=9A=84worker=5Fagent=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=EF=BC=8C=E7=AE=80=E5=8C=96=E5=9B=A2=E9=98=9F=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team-arch-opt/specs/team-config.json | 1 - ccw/src/services/deepwiki-service.ts | 84 +++++++++++++------ ccw/src/tools/cli-executor-core.ts | 35 +++++++- ccw/src/tools/edit-file.ts | 36 ++++++++ .../src/codexlens/storage/deepwiki_store.py | 81 ++++++++++-------- .../src/codexlens/tools/deepwiki_generator.py | 7 +- 6 files changed, 179 insertions(+), 65 deletions(-) diff --git a/.claude/skills/team-arch-opt/specs/team-config.json b/.claude/skills/team-arch-opt/specs/team-config.json index 82ca6878..73265da8 100644 --- a/.claude/skills/team-arch-opt/specs/team-config.json +++ b/.claude/skills/team-arch-opt/specs/team-config.json @@ -4,7 +4,6 @@ "team_display_name": "Architecture Optimization", "skill_name": "team-arch-opt", "skill_path": ".claude/skills/team-arch-opt/", - "worker_agent": "team-worker", "pipeline_type": "Linear with Review-Fix Cycle (Parallel-Capable)", "completion_action": "interactive", "has_inline_discuss": true, diff --git a/ccw/src/services/deepwiki-service.ts b/ccw/src/services/deepwiki-service.ts index 503fc1ab..c4d9c038 100644 --- a/ccw/src/services/deepwiki-service.ts +++ b/ccw/src/services/deepwiki-service.ts @@ -27,6 +27,7 @@ export interface DeepWikiSymbol { end_line: number; created_at: number | null; updated_at: number | null; + staleness_score: number; } /** @@ -281,28 +282,40 @@ export class DeepWikiService { staleness_score: number; }>>(); - const stmt = db.prepare(` - SELECT name, type, doc_file, anchor, start_line, end_line, staleness_score + if (paths.length === 0) { + return result; + } + + const normalizedPaths = paths.map(p => p.replace(/\\/g, '/')); + const placeholders = normalizedPaths.map(() => '?').join(','); + const rows = db.prepare(` + SELECT source_file, name, type, doc_file, anchor, start_line, end_line, staleness_score FROM deepwiki_symbols - WHERE source_file = ? - ORDER BY start_line - `); + WHERE source_file IN (${placeholders}) + ORDER BY source_file, start_line + `).all(...normalizedPaths) as Array<{ + source_file: string; + name: string; + type: string; + doc_file: string; + anchor: string; + start_line: number; + end_line: number; + staleness_score: number; + }>; - for (const filePath of paths) { - const normalizedPath = filePath.replace(/\\/g, '/'); - const rows = stmt.all(normalizedPath) as Array<{ - name: string; - type: string; - doc_file: string; - anchor: string; - start_line: number; - end_line: number; - staleness_score: number; - }>; - - if (rows.length > 0) { - result.set(normalizedPath, rows); - } + for (const row of rows) { + const existing = result.get(row.source_file) || []; + existing.push({ + name: row.name, + type: row.type, + doc_file: row.doc_file, + anchor: row.anchor, + start_line: row.start_line, + end_line: row.end_line, + staleness_score: row.staleness_score, + }); + result.set(row.source_file, existing); } return result; @@ -328,17 +341,36 @@ export class DeepWikiService { } try { - const stmt = db.prepare('SELECT content_hash FROM deepwiki_files WHERE path = ?'); const stale: Array<{ path: string; stored_hash: string | null; current_hash: string }> = []; + if (files.length === 0) { + return stale; + } + // Build lookup: normalizedPath -> original file + const lookup = new Map(); + const normalizedPaths: string[] = []; for (const file of files) { - const normalizedPath = file.path.replace(/\\/g, '/'); - const row = stmt.get(normalizedPath) as { content_hash: string } | undefined; + const np = file.path.replace(/\\/g, '/'); + lookup.set(np, file); + normalizedPaths.push(np); + } - if (row && row.content_hash !== file.hash) { - stale.push({ path: file.path, stored_hash: row.content_hash, current_hash: file.hash }); - } else if (!row) { + const placeholders = normalizedPaths.map(() => '?').join(','); + const rows = db.prepare( + `SELECT path, content_hash FROM deepwiki_files WHERE path IN (${placeholders})` + ).all(...normalizedPaths) as Array<{ path: string; content_hash: string }>; + + const stored = new Map(); + for (const row of rows) { + stored.set(row.path, row.content_hash); + } + + for (const [np, file] of lookup) { + const storedHash = stored.get(np); + if (storedHash === undefined) { stale.push({ path: file.path, stored_hash: null, current_hash: file.hash }); + } else if (storedHash !== file.hash) { + stale.push({ path: file.path, stored_hash: storedHash, current_hash: file.hash }); } } diff --git a/ccw/src/tools/cli-executor-core.ts b/ccw/src/tools/cli-executor-core.ts index ff70444c..508d8934 100644 --- a/ccw/src/tools/cli-executor-core.ts +++ b/ccw/src/tools/cli-executor-core.ts @@ -38,20 +38,36 @@ import { // Track all running child processes for cleanup on interruption (multi-process support) const runningChildProcesses = new Set(); +// Debug logging for parallel execution testing +const DEBUG_SESSION_ID = 'DBG-parallel-ccw-cli-test-2026-03-07'; +const DEBUG_LOG_PATH = path.join(process.cwd(), '.workflow', '.debug', DEBUG_SESSION_ID, 'debug.log'); + +function writeDebugLog(event: string, data: Record): void { + try { + const logEntry = JSON.stringify({ event, ...data, timestamp: new Date().toISOString() }) + '\n'; + fs.appendFileSync(DEBUG_LOG_PATH, logEntry, 'utf8'); + } catch (err) { + // Silently ignore logging errors to avoid disrupting execution + } +} + + /** * Kill all running CLI child processes * Called when parent process receives SIGINT/SIGTERM */ export function killAllCliProcesses(): boolean { if (runningChildProcesses.size === 0) return false; + writeDebugLog('KILL_ALL_START', { initial_set_size: runningChildProcesses.size }); const processesToKill = Array.from(runningChildProcesses); debugLog('KILL', `Killing ${processesToKill.length} child process(es)`, { pids: processesToKill.map(p => p.pid) }); + writeDebugLog('KILL_ALL_COPY', { pids_to_kill: processesToKill.map(p => p.pid) }); // 1. SIGTERM for graceful shutdown for (const child of processesToKill) { if (!child.killed) { - try { child.kill('SIGTERM'); } catch { /* Ignore kill errors */ } + try { child.kill('SIGTERM'); } catch (e: any) { writeDebugLog('KILL_SIGTERM_ERROR', { pid: child.pid, error: e.message }); } } } @@ -59,12 +75,13 @@ export function killAllCliProcesses(): boolean { const killTimeout = setTimeout(() => { for (const child of processesToKill) { if (!child.killed) { - try { child.kill('SIGKILL'); } catch { /* Ignore kill errors */ } + try { child.kill('SIGKILL'); } catch (e: any) { writeDebugLog('KILL_SIGKILL_ERROR', { pid: child.pid, error: e.message }); } } } }, 2000); killTimeout.unref(); + writeDebugLog('KILL_ALL_CLEAR', { set_size_before: runningChildProcesses.size, pids_in_set: Array.from(runningChildProcesses).map(p => p.pid) }); runningChildProcesses.clear(); return true; } @@ -240,6 +257,7 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom // Track child process for cleanup (multi-process support) runningChildProcesses.add(child); + writeDebugLog('PROCESS_ADD', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeClaudeWithSettings' }); let stdout = ''; let stderr = ''; @@ -279,6 +297,7 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom child.on('close', (code) => { runningChildProcesses.delete(child); + writeDebugLog('PROCESS_DELETE', { pid: child.pid, exit_code: code, set_size_after: runningChildProcesses.size, function: 'executeClaudeWithSettings', handler: 'close' }); const endTime = Date.now(); const duration = endTime - startTime; @@ -319,8 +338,10 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom // Save to history try { + writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conversation.id, pid: child.pid, function: 'executeClaudeWithSettings' }); saveConversation(workingDir, conversation); } catch (err) { + writeDebugLog('SAVE_CONVERSATION_ERROR', { conversationId: conversation.id, pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeClaudeWithSettings' }); console.error('[CLI Executor] Failed to save CLI封装 history:', (err as Error).message); } @@ -335,6 +356,7 @@ async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Prom child.on('error', (error) => { runningChildProcesses.delete(child); + writeDebugLog('PROCESS_DELETE', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeClaudeWithSettings', handler: 'error' }); reject(new Error(`Failed to spawn claude: ${error.message}`)); }); }); @@ -997,6 +1019,7 @@ async function executeCliTool( // Track child process for cleanup on interruption (multi-process support) runningChildProcesses.add(child); + writeDebugLog('PROCESS_ADD', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeCliTool', tool }); debugLog('SPAWN', `Process spawned`, { pid: child.pid }); @@ -1048,6 +1071,7 @@ async function executeCliTool( child.on('close', async (code) => { // Remove from running processes runningChildProcesses.delete(child); + writeDebugLog('PROCESS_DELETE', { pid: child.pid, exit_code: code, set_size_after: runningChildProcesses.size, function: 'executeCliTool', handler: 'close', tool }); // Flush remaining buffer from parser const remainingUnits = parser.flush(); @@ -1176,9 +1200,11 @@ async function executeCliTool( // Save all source conversations try { for (const conv of savedConversations) { + writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conv.id, pid: child.pid, function: 'executeCliTool', context: 'merge-loop', tool }); saveConversation(workingDir, conv); } } catch (err) { + writeDebugLog('SAVE_CONVERSATION_ERROR', { pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeCliTool', context: 'merge-loop', tool }); console.error('[CLI Executor] Failed to save merged histories:', (err as Error).message); } } else if (isMerge && mergeResult && customId) { @@ -1218,8 +1244,10 @@ async function executeCliTool( }; // Save merged conversation try { + writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conversation.id, pid: child.pid, function: 'executeCliTool', context: 'merge-with-id', tool }); saveConversation(workingDir, conversation); } catch (err) { + writeDebugLog('SAVE_CONVERSATION_ERROR', { conversationId: conversation.id, pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeCliTool', context: 'merge-with-id', tool }); console.error('[CLI Executor] Failed to save merged conversation:', (err as Error).message); } } else { @@ -1249,8 +1277,10 @@ async function executeCliTool( }; // Try to save conversation to history try { + writeDebugLog('SAVE_CONVERSATION_START', { conversationId: conversation.id, pid: child.pid, function: 'executeCliTool', context: 'normal', tool }); saveConversation(workingDir, conversation); } catch (err) { + writeDebugLog('SAVE_CONVERSATION_ERROR', { conversationId: conversation.id, pid: child.pid, error: (err as Error).message, stack: (err as Error).stack, function: 'executeCliTool', context: 'normal', tool }); // Non-fatal: continue even if history save fails console.error('[CLI Executor] Failed to save history:', (err as Error).message); } @@ -1311,6 +1341,7 @@ async function executeCliTool( child.on('error', (error) => { // Remove from running processes runningChildProcesses.delete(child); + writeDebugLog('PROCESS_DELETE', { pid: child.pid, set_size_after: runningChildProcesses.size, function: 'executeCliTool', handler: 'error', tool }); errorLog('SPAWN', `Failed to spawn process`, error, { tool, diff --git a/ccw/src/tools/edit-file.ts b/ccw/src/tools/edit-file.ts index 9db16c0a..d2f4eaf8 100644 --- a/ccw/src/tools/edit-file.ts +++ b/ccw/src/tools/edit-file.ts @@ -573,8 +573,44 @@ interface CompactEditResult { dryRun?: boolean; } +/** + * Detect parameter mode mismatch and provide helpful error message + * This helps users understand when they're using wrong parameters for the selected mode + */ +function detectModeMismatch(params: Record): string | null { + const hasLineParams = ['operation', 'line', 'end_line'].some(p => params[p] !== undefined); + const hasUpdateParams = ['oldText', 'newText', 'edits', 'replaceAll'].some(p => params[p] !== undefined); + const currentMode = params.mode as string | undefined; + + // User passed line-mode params but mode is not "line" + if (hasLineParams && currentMode !== 'line') { + if (currentMode === 'update' || currentMode === undefined) { + const modeHint = currentMode === undefined ? '(default)' : ''; + return `Parameter mismatch: detected line-mode parameters (operation/line/end_line) ` + + `but mode="${currentMode || 'update'}"${modeHint}. ` + + `Add \`mode: "line"\` to use operation/line parameters, ` + + `or use oldText/newText/edits for update mode.`; + } + } + + // User passed update-mode params but mode is "line" + if (hasUpdateParams && currentMode === 'line') { + return `Parameter mismatch: detected update-mode parameters (oldText/newText/edits/replaceAll) ` + + `but mode="line". ` + + `Remove \`mode: "line"\` or use operation/line parameters instead.`; + } + + return null; +} + // Handler function export async function handler(params: Record): Promise> { + // Check for mode mismatch before validation + const mismatchError = detectModeMismatch(params); + if (mismatchError) { + return { success: false, error: mismatchError }; + } + // Apply default mode before discriminatedUnion check (Zod doesn't apply defaults on discriminator) const normalizedParams = params.mode === undefined ? { ...params, mode: 'update' } : params; const parsed = ParamsSchema.safeParse(normalizedParams); diff --git a/codex-lens/src/codexlens/storage/deepwiki_store.py b/codex-lens/src/codexlens/storage/deepwiki_store.py index 763c0e4c..9121b910 100644 --- a/codex-lens/src/codexlens/storage/deepwiki_store.py +++ b/codex-lens/src/codexlens/storage/deepwiki_store.py @@ -209,9 +209,9 @@ class DeepWikiStore: attempts INTEGER DEFAULT 0, last_tool TEXT, last_error TEXT, - generated_at TEXT, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP + generated_at REAL, + created_at REAL, + updated_at REAL ) """ ) @@ -873,25 +873,33 @@ class DeepWikiStore: """ with self._lock: conn = self._get_connection() - stale = [] + if not files: + return [] + + # Build lookup: normalized_path -> original file dict + lookup: dict[str, dict[str, str]] = {} + normalized: list[str] = [] for f in files: path_str = self._normalize_path(f["path"]) - row = conn.execute( - "SELECT content_hash FROM deepwiki_files WHERE path=?", - (path_str,), - ).fetchone() - if row and row["content_hash"] != f["hash"]: - stale.append({ - "path": f["path"], - "stored_hash": row["content_hash"], - "current_hash": f["hash"], - }) - elif not row: - stale.append({ - "path": f["path"], - "stored_hash": None, - "current_hash": f["hash"], - }) + lookup[path_str] = f + normalized.append(path_str) + + placeholders = ",".join("?" * len(normalized)) + rows = conn.execute( + f"SELECT path, content_hash FROM deepwiki_files WHERE path IN ({placeholders})", + normalized, + ).fetchall() + + stored: dict[str, str] = {row["path"]: row["content_hash"] for row in rows} + + stale = [] + for path_str, f in lookup.items(): + stored_hash = stored.get(path_str) + if stored_hash is None: + stale.append({"path": f["path"], "stored_hash": None, "current_hash": f["hash"]}) + elif stored_hash != f["hash"]: + stale.append({"path": f["path"], "stored_hash": stored_hash, "current_hash": f["hash"]}) + return stale def get_symbols_for_paths( @@ -909,20 +917,25 @@ class DeepWikiStore: conn = self._get_connection() result: dict[str, list[DeepWikiSymbol]] = {} - for path in paths: - path_str = self._normalize_path(path) - rows = conn.execute( - """ - SELECT * FROM deepwiki_symbols - WHERE source_file=? - ORDER BY start_line - """, - (path_str,), - ).fetchall() - if rows: - result[path_str] = [ - self._row_to_deepwiki_symbol(row) for row in rows - ] + if not paths: + return result + + normalized = [self._normalize_path(p) for p in paths] + placeholders = ",".join("?" * len(normalized)) + rows = conn.execute( + f""" + SELECT * FROM deepwiki_symbols + WHERE source_file IN ({placeholders}) + ORDER BY source_file, start_line + """, + normalized, + ).fetchall() + + for row in rows: + sf = row["source_file"] + result.setdefault(sf, []).append( + self._row_to_deepwiki_symbol(row) + ) return result diff --git a/codex-lens/src/codexlens/tools/deepwiki_generator.py b/codex-lens/src/codexlens/tools/deepwiki_generator.py index 87981643..d2d4a6b1 100644 --- a/codex-lens/src/codexlens/tools/deepwiki_generator.py +++ b/codex-lens/src/codexlens/tools/deepwiki_generator.py @@ -509,14 +509,17 @@ Keep it minimal. Format as clean Markdown.""" Returns: True if content passes validation, False otherwise. """ + import re + if not content or len(content.strip()) < 20: return False required = REQUIRED_SECTIONS.get(layer, ["Description"]) - content_lower = content.lower() for section in required: - if section.lower() not in content_lower: + # Match markdown headers (##, ###, **Bold**) or standalone section names + pattern = rf"^\s*(?:#{1,6}\s+|\*\*){re.escape(section)}" + if not re.search(pattern, content, re.IGNORECASE | re.MULTILINE): return False return True