From 0e6d3eddb6be2ec9d2e127afce4e0f87cc9a94c2 Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Tue, 17 Feb 2026 18:44:30 +0000 Subject: [PATCH] Fix: add missing getOpencodeSessionTokenUsage function to resolve redo button error --- chat/server.js | 130 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/chat/server.js b/chat/server.js index ef07bed..07b063a 100644 --- a/chat/server.js +++ b/chat/server.js @@ -7543,6 +7543,136 @@ function extractTokenUsageFromResult(result, messages, options = {}) { return estimated; } +async function getOpencodeSessionTokenUsage(sessionId, cwd) { + if (!sessionId || !cwd) { + log('⚠️ getOpencodeSessionTokenUsage: Missing required parameters', { hasSessionId: !!sessionId, hasCwd: !!cwd }); + return 0; + } + + const cliCommand = resolveCliCommand('opencode'); + const candidates = [ + ['session', 'info', '--id', sessionId, '--json'], + ['sessions', 'info', '--id', sessionId, '--json'], + ['session', 'info', sessionId, '--json'], + ['session', 'usage', '--id', sessionId, '--json'], + ['session', 'show', '--id', sessionId, '--json'], + ]; + + log('🔍 getOpencodeSessionTokenUsage: Starting session token query', { + sessionId, + cwd, + cliCommand, + candidateCount: candidates.length, + candidates: candidates.map(c => c.join(' ')) + }); + + const attemptResults = []; + + for (const args of candidates) { + const cmdStr = args.join(' '); + try { + log(` → Trying: ${cliCommand} ${cmdStr}`, { sessionId }); + const { stdout, stderr } = await runCommand(cliCommand, args, { timeout: 10000, cwd }); + + const hasStdout = stdout && stdout.trim(); + const hasStderr = stderr && stderr.trim(); + + log(` ← Response received`, { + args: cmdStr, + hasStdout, + hasStderr, + stdoutLength: stdout?.length || 0, + stderrLength: stderr?.length || 0, + stdoutSample: stdout?.substring(0, 300), + stderrSample: stderr?.substring(0, 200) + }); + + if (hasStdout) { + // Try JSON parsing first + try { + const parsed = JSON.parse(stdout); + log(' ✓ JSON parse successful', { + args: cmdStr, + parsedKeys: Object.keys(parsed), + hasUsage: !!parsed.usage, + hasTokens: !!parsed.tokens, + hasTokensUsed: !!parsed.tokensUsed, + hasSession: !!parsed.session + }); + + const extracted = extractTokenUsage(parsed) || extractTokenUsage(parsed.session) || null; + const tokens = extracted?.tokens || 0; + + if (typeof tokens === 'number' && tokens > 0) { + log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from JSON', { + sessionId, + tokens, + command: cmdStr, + extractionPath: extracted?.source || 'unknown' + }); + attemptResults.push({ command: cmdStr, success: true, tokens, source: 'json' }); + return tokens; + } else { + const reason = typeof tokens !== 'number' ? `tokens is ${typeof tokens}, not number` : 'tokens is 0 or negative'; + log(' ✗ JSON parsed but no valid token count', { args: cmdStr, tokens, reason }); + attemptResults.push({ command: cmdStr, success: false, reason, parsedTokens: tokens, source: 'json' }); + } + } catch (jsonErr) { + log(' ✗ JSON parse failed, trying text parse', { + args: cmdStr, + error: jsonErr.message, + stdoutSample: stdout.substring(0, 200) + }); + + // Try to parse token count from text output + const tokenMatch = stdout.match(/total[_\s-]?tokens?\s*[:=]?\s*(\d+)/i) || + stdout.match(/tokens?\s*[:=]?\s*(\d+)/i) || + stdout.match(/token\s*count\s*[:=]?\s*(\d+)/i); + if (tokenMatch) { + const tokens = parseInt(tokenMatch[1], 10); + if (tokens > 0) { + log('✅ getOpencodeSessionTokenUsage: Successfully extracted tokens from text', { + sessionId, + tokens, + command: cmdStr, + pattern: tokenMatch[0] + }); + attemptResults.push({ command: cmdStr, success: true, tokens, source: 'text', pattern: tokenMatch[0] }); + return tokens; + } else { + log(' ✗ Text pattern matched but tokens <= 0', { args: cmdStr, tokens, pattern: tokenMatch[0] }); + attemptResults.push({ command: cmdStr, success: false, reason: 'matched text pattern but tokens <= 0', parsedTokens: tokens, source: 'text' }); + } + } else { + log(' ✗ No text patterns matched', { args: cmdStr, stdoutSample: stdout.substring(0, 200) }); + attemptResults.push({ command: cmdStr, success: false, reason: 'no text patterns matched', source: 'text' }); + } + } + } else { + const reason = !stdout ? 'no stdout' : 'stdout is empty'; + log(' ✗ No stdout to parse', { args: cmdStr, reason, hasStderr }); + attemptResults.push({ command: cmdStr, success: false, reason, stderr: stderr?.substring(0, 200) }); + } + } catch (err) { + const errorDetails = { + message: err.message, + stderr: err.stderr?.substring(0, 200), + stdout: err.stdout?.substring(0, 200), + code: err.code + }; + log(' ✗ Command execution failed', { args: cmdStr, error: errorDetails }); + attemptResults.push({ command: cmdStr, success: false, error: errorDetails }); + } + } + + log('❌ getOpencodeSessionTokenUsage: All commands failed', { + sessionId, + totalAttempts: attemptResults.length, + attemptResults + }); + return 0; +} + function getProviderUsageSnapshot(providerList = null) { const providers = (providerList && providerList.length) ? providerList.map((p) => normalizeProviderName(p))